From 533ffd17b8ece9fcc594283171ec63a682cdaa8b Mon Sep 17 00:00:00 2001 From: Tim Nunamaker Date: Wed, 22 Jul 2026 16:57:25 -0500 Subject: [PATCH 01/36] chore(deps): upgrade biome and ultracite Assisted-by: AI Signed-off-by: Tim Nunamaker --- package.json | 4 +- pnpm-lock.yaml | 697 ++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 695 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 61d9b6d8d..be1f5fe14 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,7 @@ "license": "Apache-2.0", "packageManager": "pnpm@10.33.0", "devDependencies": { - "@biomejs/biome": "^2.4.12", + "@biomejs/biome": "^2.5.5", "@pdpp/brand": "workspace:*", "@semantic-release/exec": "^7.1.0", "@types/better-sqlite3": "^7.6.13", @@ -103,6 +103,6 @@ "semantic-release": "^25.0.3", "tsx": "^4.21.0", "typescript": "^6.0.3", - "ultracite": "^7.6.0" + "ultracite": "^7.9.4" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a73b513e5..f409c47e1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,8 +13,8 @@ importers: .: devDependencies: '@biomejs/biome': - specifier: ^2.4.12 - version: 2.4.12 + specifier: ^2.5.5 + version: 2.5.5 '@pdpp/brand': specifier: workspace:* version: link:packages/pdpp-brand @@ -46,8 +46,8 @@ importers: specifier: ^6.0.3 version: 6.0.3 ultracite: - specifier: ^7.6.0 - version: 7.6.0 + specifier: ^7.9.4 + version: 7.9.4(eslint@10.7.0(jiti@2.6.1))(typescript@6.0.3) apps/console: dependencies: @@ -768,18 +768,35 @@ packages: engines: {node: '>=14.21.3'} hasBin: true + '@biomejs/biome@2.5.5': + resolution: {integrity: sha512-r1S8nFsAG1MY+vJFZALzIvwXAJv6ejDQ0mxP21Tgr9YK3ZFtjrvbBwDdNhx1rUqvccEIeNg20cYCNzl6Cr69pQ==} + engines: {node: '>=14.21.3'} + hasBin: true + '@biomejs/cli-darwin-arm64@2.4.12': resolution: {integrity: sha512-BnMU4Pc3ciEVteVpZ0BK33MLr7X57F5w1dwDLDn+/iy/yTrA4Q/N2yftidFtsA4vrDh0FMXDpacNV/Tl3fbmng==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [darwin] + '@biomejs/cli-darwin-arm64@2.5.5': + resolution: {integrity: sha512-kUrAhXVWUrwmAUnV2iXSK7umxKFysTwvqK+Ty6ptUcLY/7T3SnCAjUowE4uvwaEej6nXZ7hu/dTtbokKdsPeag==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + '@biomejs/cli-darwin-x64@2.4.12': resolution: {integrity: sha512-x9uJ0bI1rJsWICp3VH8w/5PnAVD3A7SqzDpbrfoUQX1QyWrK5jSU4fRLo/wSgGeplCivbxBRKmt5Xq4/nWvq8A==} engines: {node: '>=14.21.3'} cpu: [x64] os: [darwin] + '@biomejs/cli-darwin-x64@2.5.5': + resolution: {integrity: sha512-DamiYc5bUYZ2uxlfc+RLEPtz1Abb6PO5eTbOkufLpSGwd/7AMQAdxhFYiXmwwkJL8IsT8S7GvdgwDHqaMFAvKw==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + '@biomejs/cli-linux-arm64-musl@2.4.12': resolution: {integrity: sha512-FhfpkAAlKL6kwvcVap0Hgp4AhZmtd3YImg0kK1jd7C/aSoh4SfsB2f++yG1rU0lr8Y5MCFJrcSkmssiL9Xnnig==} engines: {node: '>=14.21.3'} @@ -787,6 +804,13 @@ packages: os: [linux] libc: [musl] + '@biomejs/cli-linux-arm64-musl@2.5.5': + resolution: {integrity: sha512-U4WMl/sy/E/Q73vf15VspakLRRs2LDFcCeBxJnQfXzssb88zpV6PJPaQ3ezhQ7H6Ht2/8bvuZeHgJWzmoxllZg==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [musl] + '@biomejs/cli-linux-arm64@2.4.12': resolution: {integrity: sha512-tOwuCuZZtKi1jVzbk/5nXmIsziOB6yqN8c9r9QM0EJYPU6DpQWf11uBOSCfFKKM4H3d9ZoarvlgMfbcuD051Pw==} engines: {node: '>=14.21.3'} @@ -794,6 +818,13 @@ packages: os: [linux] libc: [glibc] + '@biomejs/cli-linux-arm64@2.5.5': + resolution: {integrity: sha512-lRKF/pH/1RiYiBKExi3TCZVAtvzEm77aifrvcNiDFrR9WxeAnDUjDnseb6y2XV85mjitLs6SILGm2XG77cHtSQ==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [glibc] + '@biomejs/cli-linux-x64-musl@2.4.12': resolution: {integrity: sha512-dwTIgZrGutzhkQCuvHynCkyW6hJxUuyZqKKO0YNfaS2GUoRO+tOvxXZqZB6SkWAOdfZTzwaw8IEdUnIkHKHoew==} engines: {node: '>=14.21.3'} @@ -801,6 +832,13 @@ packages: os: [linux] libc: [musl] + '@biomejs/cli-linux-x64-musl@2.5.5': + resolution: {integrity: sha512-m7wC7tjX5Lrmo69dc4md8FeKpPU1NTCY1v7xUoQQ2vadWwNnBS0KZOG8471otFPHrTHihQJAjQPgMObpLvDe6A==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [musl] + '@biomejs/cli-linux-x64@2.4.12': resolution: {integrity: sha512-8pFeAnLU9QdW9jCIslB/v82bI0lhBmz2ZAKc8pVMFPO0t0wAHsoEkrUQUbMkIorTRIjbqyNZHA3lEXavsPWYSw==} engines: {node: '>=14.21.3'} @@ -808,18 +846,37 @@ packages: os: [linux] libc: [glibc] + '@biomejs/cli-linux-x64@2.5.5': + resolution: {integrity: sha512-H/O39nJEw/2Zm/fm7hrmxxoF8kK/aU1uCoPp70ruXVbomaAdLpJJnCmL11Q2JotT8QVHH06So04Oq53lCSwSwQ==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [glibc] + '@biomejs/cli-win32-arm64@2.4.12': resolution: {integrity: sha512-B0DLnx0vA9ya/3v7XyCaP+/lCpnbWbMOfUFFve+xb5OxyYvdHaS55YsSddr228Y+JAFk58agCuZTsqNiw2a6ig==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [win32] + '@biomejs/cli-win32-arm64@2.5.5': + resolution: {integrity: sha512-7BryINPuYypLUAH3o/o5ZdgomJ4zn3EDR0ChZJst7n32S6ZhKbgHXuYydLu+YAnx59ehGFR0z/MG6qnzQi3Yyw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + '@biomejs/cli-win32-x64@2.4.12': resolution: {integrity: sha512-yMckRzTyZ83hkk8iDFWswqSdU8tvZxspJKnYNh7JZr/zhZNOlzH13k4ecboU6MurKExCe2HUkH75pGI/O2JwGA==} engines: {node: '>=14.21.3'} cpu: [x64] os: [win32] + '@biomejs/cli-win32-x64@2.5.5': + resolution: {integrity: sha512-bIBFo+n6MIxdNcVFy5CrurbKiZQiUciK3bt8+O9I4wjFZNTfXLpi+giq47522eXqW5NBc9ulx7dR1SlZKi2J5g==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + '@bramus/specificity@2.4.2': resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} hasBin: true @@ -827,9 +884,17 @@ packages: '@clack/core@1.2.0': resolution: {integrity: sha512-qfxof/3T3t9DPU/Rj3OmcFyZInceqj/NVtO9rwIuJqCUgh32gwPjpFQQp/ben07qKlhpwq7GzfWpST4qdJ5Drg==} + '@clack/core@1.4.3': + resolution: {integrity: sha512-/kr3UWNtdJfxZtPgDqUOmG2pvwlmcLGheex5yiZKdwbzZJxhV+HMNR9QNmyY5cGwTNV6LrR7Jtp+KjhUAP1qBQ==} + engines: {node: '>= 20.12.0'} + '@clack/prompts@1.2.0': resolution: {integrity: sha512-4jmztR9fMqPMjz6H/UZXj0zEmE43ha1euENwkckKKel4XpSfokExPo5AiVStdHSAlHekz4d0CA/r45Ok1E4D3w==} + '@clack/prompts@1.7.0': + resolution: {integrity: sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A==} + engines: {node: '>= 20.12.0'} + '@colors/colors@1.5.0': resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} @@ -1052,6 +1117,36 @@ packages: cpu: [x64] os: [win32] + '@eslint-community/eslint-utils@4.10.1': + resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.23.5': + resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/config-helpers@0.6.0': + resolution: {integrity: sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/core@1.2.1': + resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/object-schema@3.0.5': + resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/plugin-kit@0.7.2': + resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + '@exodus/bytes@1.15.1': resolution: {integrity: sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} @@ -1133,6 +1228,26 @@ packages: '@huggingface/transformers@4.2.0': resolution: {integrity: sha512-8BRCoBMH0XsWaEIamuR0LrJGAfftgHAfb2Vrffy0VKlSAE/MnUJ5/h/zTfEP3fDIft+nk7TqB8xXEyABGitBjQ==} + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + '@img/colour@1.1.0': resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} engines: {node: '>=18'} @@ -2427,6 +2542,9 @@ packages: '@types/debug@4.1.13': resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} + '@types/esrecurse@4.3.1': + resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + '@types/estree-jsx@1.0.5': resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} @@ -2436,6 +2554,9 @@ packages: '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} @@ -2474,6 +2595,43 @@ packages: '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + '@typescript-eslint/project-service@8.65.0': + resolution: {integrity: sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.65.0': + resolution: {integrity: sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.65.0': + resolution: {integrity: sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.65.0': + resolution: {integrity: sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.65.0': + resolution: {integrity: sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.65.0': + resolution: {integrity: sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.65.0': + resolution: {integrity: sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} deprecated: Potential CWE-502 - Update to 1.3.1 or higher @@ -2538,6 +2696,9 @@ packages: ajv: optional: true + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + ajv@8.18.0: resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} @@ -2827,6 +2988,10 @@ packages: resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} engines: {node: '>=20'} + commander@15.0.0: + resolution: {integrity: sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==} + engines: {node: '>=22.12.0'} + compare-func@2.0.0: resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==} @@ -2977,6 +3142,9 @@ packages: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} @@ -3179,11 +3347,49 @@ packages: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} + eslint-scope@9.1.2: + resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@10.7.0: + resolution: {integrity: sha512-GVTD7s1vdIl6UYvAfriOPeY1Df8LIZjfofLvHwde+erDHGGuHyuM6xoxRxmHiebhYuD2p1vN4wWh0XzPARSGDQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@11.2.0: + resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} hasBin: true + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + estree-util-attach-comments@3.0.0: resolution: {integrity: sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==} @@ -3208,6 +3414,10 @@ packages: estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + etag@1.8.1: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} @@ -3268,9 +3478,15 @@ packages: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + fast-json-stringify@6.3.0: resolution: {integrity: sha512-oRCntNDY/329HJPlmdNLIdogNtt6Vyjb1WuT01Soss3slIdyUp8kAcDU3saQTOquEK8KFVfwIIF7FebxUAu+yA==} + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-querystring@1.1.2: resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} @@ -3280,15 +3496,24 @@ packages: fast-string-truncated-width@1.2.1: resolution: {integrity: sha512-Q9acT/+Uu3GwGj+5w/zsGuQjh9O1TyywhIwAxHudtWrgF09nHOPrvTLhQevPbttcxjr/SNN7mJmfOw/B1bXgow==} + fast-string-truncated-width@3.0.3: + resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} + fast-string-width@1.1.0: resolution: {integrity: sha512-O3fwIVIH5gKB38QNbdg+3760ZmGz0SZMgvwJbA1b2TGXceKE6A2cOlfogh1iw8lr049zPyd7YADHy+B7U4W9bQ==} + fast-string-width@3.0.2: + resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} + fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} fast-wrap-ansi@0.1.6: resolution: {integrity: sha512-HlUwET7a5gqjURj70D5jl7aC3Zmy4weA1SHUfM0JFI0Ptq987NH2TwbBFLoERhfwk+E+eaq4EK3jXoT+R3yp3w==} + fast-wrap-ansi@0.2.2: + resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} + fastify-plugin@5.1.0: resolution: {integrity: sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==} @@ -3319,6 +3544,10 @@ packages: resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} engines: {node: '>=18'} + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + file-uri-to-path@1.0.0: resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} @@ -3342,13 +3571,24 @@ packages: resolution: {integrity: sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==} engines: {node: '>=4'} + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + find-versions@6.0.0: resolution: {integrity: sha512-2kCCtc+JvcZ86IGAz3Z2Y0A1baIz9fL31pH/0S1IqZr9Iwnjq8izfPtrCyQKO6TLMPELLsQMre7VDqeIKCsHkA==} engines: {node: '>=18'} + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + flatbuffers@25.9.23: resolution: {integrity: sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==} + flatted@3.4.3: + resolution: {integrity: sha512-/zipXxyO6rGvuNGDiULY9MvEGSkb2gaG4GGH4ygMi0ZZzyMHdUZBmntJmx5x1G2VuPytCwGN4xsJP6cw+sK+vQ==} + follow-redirects@1.16.0: resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} engines: {node: '>=4.0'} @@ -3594,6 +3834,10 @@ packages: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + glob@13.0.6: resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} engines: {node: 18 || 20 || >=22} @@ -3784,6 +4028,10 @@ packages: import-meta-resolve@4.2.0: resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + indent-string@4.0.0: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} @@ -3973,6 +4221,9 @@ packages: engines: {node: '>=6'} hasBin: true + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + json-parse-better-errors@1.0.2: resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==} @@ -3982,12 +4233,18 @@ packages: json-schema-ref-resolver@3.0.0: resolution: {integrity: sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==} + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} json-schema-typed@8.0.2: resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + json-stringify-safe@5.0.1: resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} @@ -4011,6 +4268,9 @@ packages: jws@4.0.1: resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + kleur@3.0.3: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} @@ -4073,6 +4333,10 @@ packages: resolution: {integrity: sha512-w9sBoR0mdN+kJc3SB85VzpiAAl451/rxdCRcZlwW71QLjkeH3EBQFgc4VMj5apePychYDHAlqEWTB8J8JK/j1Q==} hasBin: true + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + libbase64@1.3.0: resolution: {integrity: sha512-GgOXd0Eo6phYgh0DJtjQ2tO8dc0IVINtZJeARPeiIJqge+HdsWSuaDTe8ztQ7j/cONByDZ3zeB325AHiv5O0dg==} @@ -4179,6 +4443,10 @@ packages: resolution: {integrity: sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==} engines: {node: '>=4'} + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + lodash-es@4.18.1: resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==} @@ -4536,6 +4804,9 @@ packages: napi-build-utils@2.0.0: resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + negotiator@1.0.0: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} @@ -4704,6 +4975,11 @@ packages: engines: {node: '>=18'} hasBin: true + nypm@0.6.8: + resolution: {integrity: sha512-Q9K4Diu6l5u6xJQogeFSs/zKtyMSgFKFtRQV+tHP4kL7KPm2grpBU0dFIwFaXwNxN0MtfKWc43VpCugAa+LPsw==} + engines: {node: '>=18'} + hasBin: true + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -4769,6 +5045,10 @@ packages: resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==} engines: {node: '>=20'} + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + ora@8.2.0: resolution: {integrity: sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==} engines: {node: '>=18'} @@ -4803,10 +5083,18 @@ packages: resolution: {integrity: sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==} engines: {node: '>=4'} + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + p-locate@2.0.0: resolution: {integrity: sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==} engines: {node: '>=4'} + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + p-map@7.0.4: resolution: {integrity: sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==} engines: {node: '>=18'} @@ -4897,6 +5185,10 @@ packages: resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} engines: {node: '>=4'} + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -5063,6 +5355,10 @@ packages: deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. hasBin: true + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + prettier@2.8.8: resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} engines: {node: '>=10.13.0'} @@ -5717,6 +6013,10 @@ packages: resolution: {integrity: sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==} engines: {node: '>=18'} + tinyexec@1.2.4: + resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} + engines: {node: '>=18'} + tinyglobby@0.2.16: resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} engines: {node: '>=12.0.0'} @@ -5761,6 +6061,12 @@ packages: trough@2.2.0: resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + ts-morph@26.0.0: resolution: {integrity: sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug==} @@ -5786,6 +6092,10 @@ packages: tw-animate-css@1.4.0: resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==} + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + type-fest@0.13.1: resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} engines: {node: '>=10'} @@ -5835,6 +6145,18 @@ packages: oxlint: optional: true + ultracite@7.9.4: + resolution: {integrity: sha512-efAEbQJoii3wH0aZ+jwbcGXQYBwC1E6TvC4//EIV3ei84RDGHDo9fSB6h/TBTNk/+/psQVGm56trVvA+2VwRbg==} + hasBin: true + peerDependencies: + oxfmt: '>=0.1.0' + oxlint: ^1.0.0 + peerDependenciesMeta: + oxfmt: + optional: true + oxlint: + optional: true + undici-types@7.19.2: resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==} @@ -5910,6 +6232,9 @@ packages: peerDependencies: browserslist: '>= 4.21.0' + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + url-join@5.0.0: resolution: {integrity: sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -6018,6 +6343,10 @@ packages: engines: {node: ^16.13.0 || >=18.0.0} hasBin: true + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + wordwrap@1.0.0: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} @@ -6083,6 +6412,11 @@ packages: engines: {node: '>= 14.6'} hasBin: true + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} + hasBin: true + yargs-parser@20.2.9: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} engines: {node: '>=10'} @@ -6107,6 +6441,10 @@ packages: resolution: {integrity: sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==} engines: {node: ^20.19.0 || ^22.12.0 || >=23} + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + yocto-spinner@1.1.0: resolution: {integrity: sha512-/BY0AUXnS7IKO354uLLA2eRcWiqDifEbd6unXCsOxkFDAkhgUL3PH9X2bFoaU0YchnDXsF+iKleeTLJGckbXfA==} engines: {node: '>=18.19'} @@ -6130,6 +6468,9 @@ packages: zod@4.3.6: resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -6494,30 +6835,65 @@ snapshots: '@biomejs/cli-win32-arm64': 2.4.12 '@biomejs/cli-win32-x64': 2.4.12 + '@biomejs/biome@2.5.5': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 2.5.5 + '@biomejs/cli-darwin-x64': 2.5.5 + '@biomejs/cli-linux-arm64': 2.5.5 + '@biomejs/cli-linux-arm64-musl': 2.5.5 + '@biomejs/cli-linux-x64': 2.5.5 + '@biomejs/cli-linux-x64-musl': 2.5.5 + '@biomejs/cli-win32-arm64': 2.5.5 + '@biomejs/cli-win32-x64': 2.5.5 + '@biomejs/cli-darwin-arm64@2.4.12': optional: true + '@biomejs/cli-darwin-arm64@2.5.5': + optional: true + '@biomejs/cli-darwin-x64@2.4.12': optional: true + '@biomejs/cli-darwin-x64@2.5.5': + optional: true + '@biomejs/cli-linux-arm64-musl@2.4.12': optional: true + '@biomejs/cli-linux-arm64-musl@2.5.5': + optional: true + '@biomejs/cli-linux-arm64@2.4.12': optional: true + '@biomejs/cli-linux-arm64@2.5.5': + optional: true + '@biomejs/cli-linux-x64-musl@2.4.12': optional: true + '@biomejs/cli-linux-x64-musl@2.5.5': + optional: true + '@biomejs/cli-linux-x64@2.4.12': optional: true + '@biomejs/cli-linux-x64@2.5.5': + optional: true + '@biomejs/cli-win32-arm64@2.4.12': optional: true + '@biomejs/cli-win32-arm64@2.5.5': + optional: true + '@biomejs/cli-win32-x64@2.4.12': optional: true + '@biomejs/cli-win32-x64@2.5.5': + optional: true + '@bramus/specificity@2.4.2': dependencies: css-tree: 3.2.1 @@ -6527,6 +6903,11 @@ snapshots: fast-wrap-ansi: 0.1.6 sisteransi: 1.0.5 + '@clack/core@1.4.3': + dependencies: + fast-wrap-ansi: 0.2.2 + sisteransi: 1.0.5 + '@clack/prompts@1.2.0': dependencies: '@clack/core': 1.2.0 @@ -6534,6 +6915,13 @@ snapshots: fast-wrap-ansi: 0.1.6 sisteransi: 1.0.5 + '@clack/prompts@1.7.0': + dependencies: + '@clack/core': 1.4.3 + fast-string-width: 3.0.2 + fast-wrap-ansi: 0.2.2 + sisteransi: 1.0.5 + '@colors/colors@1.5.0': optional: true @@ -6685,6 +7073,36 @@ snapshots: '@esbuild/win32-x64@0.27.7': optional: true + '@eslint-community/eslint-utils@4.10.1(eslint@10.7.0(jiti@2.6.1))': + dependencies: + eslint: 10.7.0(jiti@2.6.1) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.23.5': + dependencies: + '@eslint/object-schema': 3.0.5 + debug: 4.4.3 + minimatch: 10.2.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.6.0': + dependencies: + '@eslint/core': 1.2.1 + + '@eslint/core@1.2.1': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/object-schema@3.0.5': {} + + '@eslint/plugin-kit@0.7.2': + dependencies: + '@eslint/core': 1.2.1 + levn: 0.4.1 + '@exodus/bytes@1.15.1(@noble/hashes@1.8.0)': optionalDependencies: '@noble/hashes': 1.8.0 @@ -6766,6 +7184,22 @@ snapshots: onnxruntime-web: 1.26.0-dev.20260416-b7804b056c sharp: 0.34.5 + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + '@img/colour@1.1.0': {} '@img/sharp-darwin-arm64@0.34.5': @@ -7915,6 +8349,8 @@ snapshots: dependencies: '@types/ms': 2.1.0 + '@types/esrecurse@4.3.1': {} + '@types/estree-jsx@1.0.5': dependencies: '@types/estree': 1.0.8 @@ -7925,6 +8361,8 @@ snapshots: dependencies: '@types/unist': 3.0.3 + '@types/json-schema@7.0.15': {} + '@types/mdast@4.0.4': dependencies: '@types/unist': 3.0.3 @@ -7959,6 +8397,57 @@ snapshots: dependencies: '@types/node': 25.6.0 + '@typescript-eslint/project-service@8.65.0(typescript@6.0.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.65.0(typescript@6.0.3) + '@typescript-eslint/types': 8.65.0 + debug: 4.4.3 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.65.0': + dependencies: + '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/visitor-keys': 8.65.0 + + '@typescript-eslint/tsconfig-utils@8.65.0(typescript@6.0.3)': + dependencies: + typescript: 6.0.3 + + '@typescript-eslint/types@8.65.0': {} + + '@typescript-eslint/typescript-estree@8.65.0(typescript@6.0.3)': + dependencies: + '@typescript-eslint/project-service': 8.65.0(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.65.0(typescript@6.0.3) + '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/visitor-keys': 8.65.0 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.7.4 + tinyglobby: 0.2.16 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.65.0(eslint@10.7.0(jiti@2.6.1))(typescript@6.0.3)': + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@10.7.0(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.65.0 + '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/typescript-estree': 8.65.0(typescript@6.0.3) + eslint: 10.7.0(jiti@2.6.1) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.65.0': + dependencies: + '@typescript-eslint/types': 8.65.0 + eslint-visitor-keys: 5.0.1 + '@ungap/structured-clone@1.3.0': {} '@vue/compiler-sfc@2.7.16': @@ -8016,6 +8505,13 @@ snapshots: optionalDependencies: ajv: 8.18.0 + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + ajv@8.18.0: dependencies: fast-deep-equal: 3.1.3 @@ -8311,6 +8807,8 @@ snapshots: commander@14.0.3: {} + commander@15.0.0: {} + compare-func@2.0.0: dependencies: array-ify: 1.0.0 @@ -8438,6 +8936,8 @@ snapshots: deep-extend@0.6.0: {} + deep-is@0.1.4: {} + deepmerge@4.3.1: {} default-browser-id@5.0.1: {} @@ -8642,8 +9142,72 @@ snapshots: escape-string-regexp@5.0.0: {} + eslint-scope@9.1.2: + dependencies: + '@types/esrecurse': 4.3.1 + '@types/estree': 1.0.8 + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@10.7.0(jiti@2.6.1): + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@10.7.0(jiti@2.6.1)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.23.5 + '@eslint/config-helpers': 0.6.0 + '@eslint/core': 1.2.1 + '@eslint/plugin-kit': 0.7.2 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + ajv: 6.15.0 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 9.1.2 + eslint-visitor-keys: 5.0.1 + espree: 11.2.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + minimatch: 10.2.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.6.1 + transitivePeerDependencies: + - supports-color + + espree@11.2.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 5.0.1 + esprima@4.0.1: {} + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + estree-util-attach-comments@3.0.0: dependencies: '@types/estree': 1.0.8 @@ -8681,6 +9245,8 @@ snapshots: dependencies: '@types/estree': 1.0.8 + esutils@2.0.3: {} + etag@1.8.1: {} eventemitter3@5.0.4: {} @@ -8788,6 +9354,8 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 + fast-json-stable-stringify@2.1.0: {} + fast-json-stringify@6.3.0: dependencies: '@fastify/merge-json-schemas': 0.2.1 @@ -8797,6 +9365,8 @@ snapshots: json-schema-ref-resolver: 3.0.0 rfdc: 1.4.1 + fast-levenshtein@2.0.6: {} + fast-querystring@1.1.2: dependencies: fast-decode-uri-component: 1.0.1 @@ -8805,16 +9375,26 @@ snapshots: fast-string-truncated-width@1.2.1: {} + fast-string-truncated-width@3.0.3: {} + fast-string-width@1.1.0: dependencies: fast-string-truncated-width: 1.2.1 + fast-string-width@3.0.2: + dependencies: + fast-string-truncated-width: 3.0.3 + fast-uri@3.1.0: {} fast-wrap-ansi@0.1.6: dependencies: fast-string-width: 1.1.0 + fast-wrap-ansi@0.2.2: + dependencies: + fast-string-width: 3.0.2 + fastify-plugin@5.1.0: {} fastify@5.8.5: @@ -8856,6 +9436,10 @@ snapshots: dependencies: is-unicode-supported: 2.1.0 + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + file-uri-to-path@1.0.0: {} fill-range@7.1.1: @@ -8885,13 +9469,25 @@ snapshots: dependencies: locate-path: 2.0.0 + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + find-versions@6.0.0: dependencies: semver-regex: 4.0.5 super-regex: 1.1.0 + flat-cache@4.0.1: + dependencies: + flatted: 3.4.3 + keyv: 4.5.4 + flatbuffers@25.9.23: {} + flatted@3.4.3: {} + follow-redirects@1.16.0: {} form-data@4.0.5: @@ -9125,6 +9721,10 @@ snapshots: dependencies: is-glob: 4.0.3 + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + glob@13.0.6: dependencies: minimatch: 10.2.5 @@ -9401,6 +10001,8 @@ snapshots: import-meta-resolve@4.2.0: {} + imurmurhash@0.1.4: {} + indent-string@4.0.0: {} indent-string@5.0.0: {} @@ -9543,6 +10145,8 @@ snapshots: jsesc@3.1.0: {} + json-buffer@3.0.1: {} + json-parse-better-errors@1.0.2: {} json-parse-even-better-errors@2.3.1: {} @@ -9551,10 +10155,14 @@ snapshots: dependencies: dequal: 2.0.3 + json-schema-traverse@0.4.1: {} + json-schema-traverse@1.0.0: {} json-schema-typed@8.0.2: {} + json-stable-stringify-without-jsonify@1.0.1: {} + json-stringify-safe@5.0.1: {} json-with-bigint@3.5.8: {} @@ -9580,6 +10188,10 @@ snapshots: jwa: 2.0.1 safe-buffer: 5.2.1 + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + kleur@3.0.3: {} kleur@4.1.5: {} @@ -9627,6 +10239,11 @@ snapshots: lefthook-windows-arm64: 2.1.6 lefthook-windows-x64: 2.1.6 + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + libbase64@1.3.0: {} libmime@5.3.8: @@ -9715,6 +10332,10 @@ snapshots: p-locate: 2.0.0 path-exists: 3.0.0 + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + lodash-es@4.18.1: {} lodash.capitalize@4.2.1: {} @@ -10312,6 +10933,8 @@ snapshots: napi-build-utils@2.0.0: {} + natural-compare@1.4.0: {} + negotiator@1.0.0: {} neo-async@2.6.2: {} @@ -10412,6 +11035,12 @@ snapshots: pathe: 2.0.3 tinyexec: 1.1.1 + nypm@0.6.8: + dependencies: + citty: 0.2.2 + pathe: 2.0.3 + tinyexec: 1.2.4 + object-assign@4.1.1: {} object-inspect@1.13.4: {} @@ -10482,6 +11111,15 @@ snapshots: powershell-utils: 0.1.0 wsl-utils: 0.3.1 + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + ora@8.2.0: dependencies: chalk: 5.6.2 @@ -10566,10 +11204,18 @@ snapshots: dependencies: p-try: 1.0.0 + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + p-locate@2.0.0: dependencies: p-limit: 1.3.0 + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + p-map@7.0.4: {} p-queue@9.2.0: @@ -10655,6 +11301,8 @@ snapshots: path-exists@3.0.0: {} + path-exists@4.0.0: {} + path-key@3.1.1: {} path-key@4.0.0: {} @@ -10831,6 +11479,8 @@ snapshots: tar-fs: 2.1.4 tunnel-agent: 0.6.0 + prelude-ls@1.2.1: {} + prettier@2.8.8: optional: true @@ -11664,6 +12314,8 @@ snapshots: tinyexec@1.1.1: {} + tinyexec@1.2.4: {} + tinyglobby@0.2.16: dependencies: fdir: 6.5.0(picomatch@4.0.4) @@ -11699,6 +12351,10 @@ snapshots: trough@2.2.0: {} + ts-api-utils@2.5.0(typescript@6.0.3): + dependencies: + typescript: 6.0.3 + ts-morph@26.0.0: dependencies: '@ts-morph/common': 0.27.0 @@ -11727,6 +12383,10 @@ snapshots: tw-animate-css@1.4.0: {} + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + type-fest@0.13.1: {} type-fest@1.4.0: {} @@ -11764,6 +12424,23 @@ snapshots: yaml: 2.8.3 zod: 4.3.6 + ultracite@7.9.4(eslint@10.7.0(jiti@2.6.1))(typescript@6.0.3): + dependencies: + '@clack/prompts': 1.7.0 + '@typescript-eslint/utils': 8.65.0(eslint@10.7.0(jiti@2.6.1))(typescript@6.0.3) + commander: 15.0.0 + cross-spawn: 7.0.6 + deepmerge: 4.3.1 + glob: 13.0.6 + jsonc-parser: 3.3.1 + nypm: 0.6.8 + yaml: 2.9.0 + zod: 4.4.3 + transitivePeerDependencies: + - eslint + - supports-color + - typescript + undici-types@7.19.2: {} undici@6.25.0: {} @@ -11838,6 +12515,10 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + url-join@5.0.0: {} use-callback-ref@1.3.3(@types/react@19.2.14)(react@19.2.5): @@ -11939,6 +12620,8 @@ snapshots: dependencies: isexe: 3.1.5 + word-wrap@1.2.5: {} + wordwrap@1.0.0: {} wrap-ansi@6.2.0: @@ -11987,6 +12670,8 @@ snapshots: yaml@2.8.3: {} + yaml@2.9.0: {} + yargs-parser@20.2.9: {} yargs-parser@21.1.1: {} @@ -12022,6 +12707,8 @@ snapshots: y18n: 5.0.8 yargs-parser: 22.0.0 + yocto-queue@0.1.0: {} + yocto-spinner@1.1.0: dependencies: yoctocolors: 2.1.2 @@ -12038,4 +12725,6 @@ snapshots: zod@4.3.6: {} + zod@4.4.3: {} + zwitch@2.0.4: {} From c0b37b6d2b2e1eb3700822c082203cc96328004d Mon Sep 17 00:00:00 2001 From: Tim Nunamaker Date: Wed, 22 Jul 2026 16:57:45 -0500 Subject: [PATCH 02/36] style: reformat per biome 2.5 Assisted-by: AI Signed-off-by: Tim Nunamaker --- apps/console/next.config.mjs | 64 +- apps/console/postcss.config.mjs | 2 +- .../launch/recovery-classification.ts | 7 +- .../(console)/explore/page.invariants.test.ts | 3 +- .../sources/[connector]/sync-now-button.tsx | 6 +- .../sources/last-known-sync-start.ts | 5 +- .../app/(console)/sources/sources-view.css | 4 +- .../(console)/syncs/[runId]/stream/page.tsx | 9 +- .../stream/stream-viewer-active-presses.ts | 5 +- .../stream/stream-viewer-input-router.test.ts | 32 +- apps/console/src/app/globals.css | 142 +- apps/console/src/app/icon.svg | 8 +- .../src/app/well-known/skills/route.test.ts | 4 +- apps/console/src/lib/pdpp-cli-command.test.ts | 5 +- apps/console/tailwind.config.ts | 8 +- apps/site/next.config.mjs | 128 +- apps/site/postcss.config.mjs | 2 +- apps/site/source.config.ts | 8 +- apps/site/src/app/design/page.tsx | 4 +- apps/site/src/app/globals.css | 144 +- apps/site/src/app/icon.svg | 8 +- apps/site/src/lib/pdpp-cli-command.test.ts | 5 +- apps/site/tailwind.config.ts | 8 +- deploy/railway/core-first-boot.mjs | 54 +- deploy/railway/core-supervisor.mjs | 40 +- docker/neko/policies.json | 9 +- .../project/Ink Carbon - System Sheet.html | 196 +- .../ink-carbon/project/PDPP Explorer.html | 62 +- .../ink-carbon/project/PDPP Round 3.html | 53 +- .../project/PDPP Vibe Studies v2.html | 55 +- .../ink-carbon/project/PDPP Vibe Studies.html | 55 +- .../project/Recordroom - Console.html | 87 +- .../Watcher - What Longview Can See.html | 213 +- .../project/_adherence.oxlintrc.json | 18 +- .../ink-carbon/project/_ds_manifest.json | 1062 +++++++++- .../ink-carbon/project/assets/logo-mark.svg | 15 +- .../ink-carbon/project/assets/wordmark.svg | 37 +- .../ink-carbon/project/colors_and_type.css | 132 +- .../ink-carbon/project/explorer/app.jsx | 740 +++---- .../project/explorer/assets/logo-mark.svg | 15 +- .../project/explorer/colors_and_type.css | 133 +- .../ink-carbon/project/explorer/data.js | 1756 ++++++++++++++--- .../ink-carbon/project/explorer/discover.jsx | 812 ++++---- .../ink-carbon/project/explorer/dispatch.js | 37 +- .../ink-carbon/project/explorer/explorer.css | 1361 ++++++++++--- .../ink-carbon/project/explorer/feed.jsx | 730 +++---- .../ink-carbon/project/explorer/peek.jsx | 218 +- .../project/explorer/primitives.jsx | 414 ++-- .../ink-carbon/project/explorer/query-bar.jsx | 347 ++-- .../ink-carbon/project/explorer/query.js | 461 +++-- .../project/explorer/tweaks-panel.jsx | 251 ++- .../ink-carbon/project/explorer/views-1.jsx | 500 ++--- .../ink-carbon/project/explorer/views-2.jsx | 606 +++--- .../ink-carbon/project/labs/LivingGrant.jsx | 297 ++- .../ink-carbon/project/labs/SpecElements.jsx | 223 ++- .../ink-carbon/project/labs/TheAtlas.jsx | 175 +- .../ink-carbon/project/labs/TheContract.jsx | 178 +- .../ink-carbon/project/labs/ThePurposes.jsx | 19 +- .../ink-carbon/project/labs/TheSpecimen.jsx | 176 +- .../ink-carbon/project/labs/ThermalField.jsx | 135 +- .../ink-carbon/project/labs/labs.css | 192 +- .../project/preview/brand-logo.html | 127 +- .../project/preview/colors-primary.html | 85 +- .../project/preview/colors-semantic.html | 82 +- .../project/preview/colors-washes.html | 80 +- .../project/preview/components-badges.html | 70 +- .../project/preview/components-buttons.html | 56 +- .../project/preview/components-cards.html | 90 +- .../project/preview/components-code.html | 48 +- .../preview/components-consent-row.html | 60 +- .../project/preview/components-dataflow.html | 96 +- .../preview/components-grant-card.html | 83 +- .../project/preview/components-inputs.html | 50 +- .../project/preview/iconography.html | 84 +- .../project/preview/layout-grid.html | 91 +- .../ink-carbon/project/preview/radii.html | 84 +- .../ink-carbon/project/preview/shadows.html | 73 +- .../project/preview/spacing-scale.html | 91 +- .../ink-carbon/project/preview/type-body.html | 54 +- .../project/preview/type-display.html | 45 +- .../ink-carbon/project/preview/type-mono.html | 63 +- .../ink-carbon/project/recordroom/console.css | 331 +++- .../project/recordroom/console2.css | 434 +++- .../ink-carbon/project/recordroom/explore.css | 449 ++++- .../project/recordroom/image-slot.js | 513 ++--- .../ink-carbon/project/recordroom/mobile.css | 354 +++- .../project/recordroom/overview.css | 129 +- .../ink-carbon/project/recordroom/rr-app.jsx | 1090 ++++++---- .../project/recordroom/rr-components.jsx | 736 +++---- .../ink-carbon/project/recordroom/rr-data.js | 232 ++- .../project/recordroom/rr-explore-data.js | 729 +++++-- .../project/recordroom/rr-explore.jsx | 891 +++++---- .../project/recordroom/rr-overview.jsx | 377 ++-- .../project/recordroom/rr-record.jsx | 328 +-- .../project/recordroom/rr-sources.jsx | 319 +-- .../project/recordroom/rr-syncs.jsx | 668 ++++--- .../project/recordroom/rr-views2.jsx | 863 ++++---- .../ink-carbon/project/recordroom/sources.css | 138 +- .../ink-carbon/project/recordroom/syncs.css | 308 ++- .../project/recordroom/tweaks-panel.jsx | 251 ++- .../ink-carbon/project/recordroom/watcher.css | 212 +- .../project/reinvention/boards-envelope.jsx | 371 ++-- .../project/reinvention/boards-round2.jsx | 594 +++--- .../project/reinvention/boards-round3.jsx | 315 +-- .../project/reinvention/design-canvas.jsx | 1079 +++++++--- .../project/reinvention/envelope.css | 381 +++- .../ink-carbon/project/reinvention/round2.css | 913 +++++++-- .../ink-carbon/project/reinvention/round3.css | 636 +++++- .../project/ui_kits/web/ConsentCard.jsx | 143 +- .../project/ui_kits/web/GrantInspector.jsx | 91 +- .../project/ui_kits/web/GrantsList.jsx | 133 +- .../ink-carbon/project/ui_kits/web/Hero.jsx | 106 +- .../project/ui_kits/web/SiteHeader.jsx | 90 +- .../project/ui_kits/web/StreamInventory.jsx | 85 +- .../project/ui_kits/web/Teaching.jsx | 79 +- .../ink-carbon/project/ui_kits/web/index.html | 166 +- .../ink-carbon/project/ui_kits/web/kit.css | 114 +- docs/explorer/uat/harness/capture.mjs | 5 +- docs/explorer/uat/harness/seed-and-serve.mjs | 11 +- packages/cli/bin/pdpp.js | 2 +- packages/cli/src/cache-layout.js | 16 +- packages/cli/src/collector/commands.js | 10 +- packages/cli/src/collector/errors.js | 2 +- packages/cli/src/collector/runner.js | 50 +- packages/cli/src/connect/flow.js | 200 +- packages/cli/src/index.js | 96 +- packages/cli/src/owner-agent/command.js | 125 +- packages/cli/src/owner-agent/control.js | 53 +- .../cli/src/owner-agent/credential-store.js | 22 +- packages/cli/src/owner-agent/device-flow.js | 62 +- packages/cli/src/owner-agent/discovery.js | 71 +- packages/cli/src/owner-agent/errors.js | 2 +- packages/cli/src/owner-agent/lifecycle.js | 54 +- packages/cli/src/owner-agent/setup.js | 231 +-- packages/cli/src/package-info.js | 12 +- packages/cli/src/read/commands.js | 183 +- packages/cli/src/ref/args.js | 12 +- packages/cli/src/ref/auth.js | 80 +- packages/cli/src/ref/commands/call.js | 68 +- packages/cli/src/ref/commands/connectors.js | 50 +- .../src/ref/commands/event-subscriptions.js | 153 +- packages/cli/src/ref/commands/grant.js | 22 +- packages/cli/src/ref/commands/login.js | 84 +- packages/cli/src/ref/commands/run.js | 22 +- packages/cli/src/ref/commands/trace.js | 22 +- packages/cli/src/ref/errors.js | 6 +- packages/cli/src/ref/fetch.js | 30 +- packages/cli/src/ref/output.js | 35 +- packages/cli/src/ref/session.js | 36 +- .../bin/pdpp-local-collector.ts | 90 +- .../scripts/pack-install-run.mjs | 77 +- packages/local-collector/src/runner.ts | 12 +- packages/mcp-server/bin/pdpp-mcp-server.js | 2 +- packages/mcp-server/src/credentials.js | 41 +- packages/mcp-server/src/index.js | 35 +- packages/mcp-server/src/rs-client.js | 64 +- packages/mcp-server/src/server.js | 60 +- packages/mcp-server/src/tools.js | 1248 ++++++------ packages/mcp-server/test/smoke-stdio.mjs | 74 +- packages/pdpp-brand/base.css | 224 ++- packages/pdpp-brand/docs.css | 40 +- packages/pdpp-brand/ink-carbon.css | 51 +- packages/read-core/src/index.js | 149 +- reference-implementation/lib/spine.ts | 4 +- .../remote-surface-optional.ts | 4 +- .../connector-instance-write-coordinator.ts | 5 +- .../server/connector-summary-read-model.ts | 38 +- .../server/explore-timeline-substrate.ts | 23 +- .../server/record-source-checkpoint.ts | 5 +- .../server/ref-control.ts | 5 +- .../server/routes/ref-dataset.ts | 3 +- .../routes/ref-static-secret-setup-status.ts | 7 +- .../server/routes/rs-hosted-mcp.ts | 7 +- .../server/streaming/cdp-companion.ts | 16 +- scripts/check-console-health-surfaces.mjs | 3 +- scripts/check-dist-tag-posture.mjs | 68 +- scripts/check-flyio-deploy-env.mjs | 56 +- scripts/check-package-release-policy.mjs | 99 +- scripts/check-railway-deploy-env.mjs | 217 +- scripts/check-railway-ghcr-public.mjs | 62 +- scripts/ci-mode.mjs | 211 +- scripts/cli-acceptance-smoke.mjs | 119 +- scripts/dev.mjs | 2 +- scripts/event-subscription-test-receiver.mjs | 11 +- scripts/lib/owner-session.mjs | 6 +- scripts/manual-action-stream-smoke.mjs | 68 +- scripts/openspec-archive-check.mjs | 3 +- scripts/openspec-status.mjs | 5 +- scripts/owner-journey-acceptance/harness.mjs | 12 +- scripts/owner-journey-acceptance/live.mjs | 43 +- scripts/owner-journey-acceptance/report.mjs | 4 +- scripts/owner-journey-acceptance/scan.mjs | 16 +- .../surface-manifest.mjs | 13 +- scripts/perf/bench.mjs | 34 +- scripts/railway-mcp-query-smoke.mjs | 280 ++- scripts/read-surface-smoke.mjs | 1502 +++++++------- scripts/spec-check.mjs | 8 +- scripts/stream-debug-summary.mjs | 6 +- scripts/stream-evidence-inventory.mjs | 4 +- scripts/stream-health-audit/live.mjs | 7 +- scripts/stream-mint-probe.mjs | 59 +- .../stream-playground-parity-calibration.mjs | 35 +- 202 files changed, 23320 insertions(+), 12379 deletions(-) diff --git a/apps/console/next.config.mjs b/apps/console/next.config.mjs index ad1135f83..a9884cff6 100644 --- a/apps/console/next.config.mjs +++ b/apps/console/next.config.mjs @@ -1,13 +1,13 @@ // Copyright The PDP-Connect Contributors // SPDX-License-Identifier: Apache-2.0 -import path from 'path'; -import { fileURLToPath } from 'url'; -import { collectAllowedDevOrigins } from './scripts/dev-origins.mjs'; +import path from "path"; +import { fileURLToPath } from "url"; +import { collectAllowedDevOrigins } from "./scripts/dev-origins.mjs"; -const __dirname = fileURLToPath(new URL('.', import.meta.url)); +const __dirname = fileURLToPath(new URL(".", import.meta.url)); -const allowedDevOrigins = process.env.NODE_ENV === 'production' ? [] : collectAllowedDevOrigins(); +const allowedDevOrigins = process.env.NODE_ENV === "production" ? [] : collectAllowedDevOrigins(); function parseBuildWorkers(value) { if (!value) { @@ -18,29 +18,21 @@ function parseBuildWorkers(value) { } const buildWorkers = parseBuildWorkers(process.env.PDPP_WEB_BUILD_WORKERS); -const manualUploadBodyLimit = '1024mb'; +const manualUploadBodyLimit = "1024mb"; /** @type {import('next').NextConfig} */ const nextConfig = { ...(allowedDevOrigins.length > 0 ? { allowedDevOrigins } : {}), - output: 'standalone', - outputFileTracingRoot: path.join(__dirname, '../..'), + output: "standalone", + outputFileTracingRoot: path.join(__dirname, "../.."), outputFileTracingIncludes: { - '/well-known/skills/**': [ - '../../docs/agent-skills/**/*.md', - '../../openspec/README.md', - '../../pnpm-workspace.yaml', - ], - '/llms-full.txt': [ - '../../docs/agent-skills/**/*.md', - '../../openspec/README.md', - '../../pnpm-workspace.yaml', - ], - '/llms.txt': [ - '../../docs/agent-skills/**/*.md', - '../../openspec/README.md', - '../../pnpm-workspace.yaml', + "/well-known/skills/**": [ + "../../docs/agent-skills/**/*.md", + "../../openspec/README.md", + "../../pnpm-workspace.yaml", ], + "/llms-full.txt": ["../../docs/agent-skills/**/*.md", "../../openspec/README.md", "../../pnpm-workspace.yaml"], + "/llms.txt": ["../../docs/agent-skills/**/*.md", "../../openspec/README.md", "../../pnpm-workspace.yaml"], }, reactStrictMode: true, experimental: { @@ -54,12 +46,12 @@ const nextConfig = { // consume its TypeScript sources directly once shim pairs (.js + .d.ts) // collapse into single .ts exports. Without this, Next's bundler would // reject .ts entries from a node_modules-resolved workspace package. - transpilePackages: ['pdpp-reference-implementation', '@pdpp/brand', '@pdpp/brand-react', '@pdpp/operator-ui'], + transpilePackages: ["pdpp-reference-implementation", "@pdpp/brand", "@pdpp/brand-react", "@pdpp/operator-ui"], async redirects() { return [ { - source: '/favicon.ico', - destination: '/brand/pdpp-favicon.svg', + source: "/favicon.ico", + destination: "/brand/pdpp-favicon.svg", permanent: false, }, ]; @@ -67,31 +59,31 @@ const nextConfig = { async rewrites() { return [ { - source: '/.well-known/oauth-authorization-server', - destination: '/well-known/oauth-authorization-server', + source: "/.well-known/oauth-authorization-server", + destination: "/well-known/oauth-authorization-server", }, { - source: '/.well-known/oauth-protected-resource/:path*', - destination: '/well-known/oauth-protected-resource/:path*', + source: "/.well-known/oauth-protected-resource/:path*", + destination: "/well-known/oauth-protected-resource/:path*", }, { - source: '/.well-known/oauth-protected-resource', - destination: '/well-known/oauth-protected-resource', + source: "/.well-known/oauth-protected-resource", + destination: "/well-known/oauth-protected-resource", }, { - source: '/.well-known/skills/:path*', - destination: '/well-known/skills/:path*', + source: "/.well-known/skills/:path*", + destination: "/well-known/skills/:path*", }, { - source: '/.well-known/llms.txt', - destination: '/llms.txt', + source: "/.well-known/llms.txt", + destination: "/llms.txt", }, ]; }, webpack(config) { config.resolve.alias = { ...config.resolve.alias, - '@': path.join(__dirname, 'src'), + "@": path.join(__dirname, "src"), }; return config; }, diff --git a/apps/console/postcss.config.mjs b/apps/console/postcss.config.mjs index 6e71b3344..95974dcd4 100644 --- a/apps/console/postcss.config.mjs +++ b/apps/console/postcss.config.mjs @@ -3,7 +3,7 @@ const config = { plugins: { - '@tailwindcss/postcss': {}, + "@tailwindcss/postcss": {}, }, }; diff --git a/apps/console/src/app/(console)/connect/browser-session/[connectorId]/launch/recovery-classification.ts b/apps/console/src/app/(console)/connect/browser-session/[connectorId]/launch/recovery-classification.ts index 55afb6651..0c6cc098c 100644 --- a/apps/console/src/app/(console)/connect/browser-session/[connectorId]/launch/recovery-classification.ts +++ b/apps/console/src/app/(console)/connect/browser-session/[connectorId]/launch/recovery-classification.ts @@ -3,7 +3,12 @@ import type { RunSummary } from "../../../../lib/ref-client.ts"; -const RECOVERABLE_BROWSER_RUN_STATUSES = new Set(["started", "in_progress", "starting_surface", "waiting_for_browser_surface"]); +const RECOVERABLE_BROWSER_RUN_STATUSES = new Set([ + "started", + "in_progress", + "starting_surface", + "waiting_for_browser_surface", +]); export function isRecoverableBrowserSessionRun(run: Pick): boolean { return RECOVERABLE_BROWSER_RUN_STATUSES.has(run.status); diff --git a/apps/console/src/app/(console)/explore/page.invariants.test.ts b/apps/console/src/app/(console)/explore/page.invariants.test.ts index 5ae13fbf0..7fbe2f548 100644 --- a/apps/console/src/app/(console)/explore/page.invariants.test.ts +++ b/apps/console/src/app/(console)/explore/page.invariants.test.ts @@ -60,7 +60,8 @@ const ROW_ACTION_LABEL_RE = /\{actionLabel\}< // as "ingested", and still renders a `precision="time"` Timestamp. Keep the // assertions loose enough to tolerate JSX reshaping. const ROW_TIME_CONTAINER_RE = //; -const ROW_TIME_QUALIFIER_RE = /entry\.displayIsSemantic \? null : ingested <\/span>/; +const ROW_TIME_QUALIFIER_RE = + /entry\.displayIsSemantic \? null : ingested <\/span>/; const ROW_TIME_TIMESTAMP_RE = //; const ROW_TIME_CSS_RE = /\.rr-x-row__time\s*\{/; // Future-dated records (e.g. YNAB future budget months) must NOT sit above today: diff --git a/apps/console/src/app/(console)/sources/[connector]/sync-now-button.tsx b/apps/console/src/app/(console)/sources/[connector]/sync-now-button.tsx index 402f009ba..ce87c3425 100644 --- a/apps/console/src/app/(console)/sources/[connector]/sync-now-button.tsx +++ b/apps/console/src/app/(console)/sources/[connector]/sync-now-button.tsx @@ -9,11 +9,7 @@ import { useRouter } from "next/navigation"; import { useCallback, useEffect, useState, useTransition } from "react"; import { syncStartFailureLead } from "../../lib/connection-evidence.ts"; import { type RunNowResult, runConnectorNowAction } from "../actions.ts"; -import { - markSyncStartToast, - readSyncStartToast, - syncStartToastDismissDelayMs, -} from "../last-known-sync-start.ts"; +import { markSyncStartToast, readSyncStartToast, syncStartToastDismissDelayMs } from "../last-known-sync-start.ts"; const RUNNING_POLL_MS = 3000; const TOAST_TTL_MS = 15_000; diff --git a/apps/console/src/app/(console)/sources/last-known-sync-start.ts b/apps/console/src/app/(console)/sources/last-known-sync-start.ts index 87a44d238..993a0077e 100644 --- a/apps/console/src/app/(console)/sources/last-known-sync-start.ts +++ b/apps/console/src/app/(console)/sources/last-known-sync-start.ts @@ -68,10 +68,7 @@ export function markSyncStartToast( } } -export function syncStartToastDismissDelayMs( - toast: { readonly expiresAt?: number }, - now: number = Date.now() -): number { +export function syncStartToastDismissDelayMs(toast: { readonly expiresAt?: number }, now: number = Date.now()): number { if (typeof toast.expiresAt === "number" && Number.isFinite(toast.expiresAt)) { return Math.max(0, toast.expiresAt - now); } diff --git a/apps/console/src/app/(console)/sources/sources-view.css b/apps/console/src/app/(console)/sources/sources-view.css index 8a1199392..63a66fde1 100644 --- a/apps/console/src/app/(console)/sources/sources-view.css +++ b/apps/console/src/app/(console)/sources/sources-view.css @@ -58,7 +58,9 @@ a.rr-s-item { /* B-2 unified active treatment: left-2 --primary accent + tint fill */ .rr-s-item.is-on { background: var(--primary-tint); - box-shadow: inset 3px 0 0 var(--primary), inset 0 0 0 1px var(--border-strong); + box-shadow: + inset 3px 0 0 var(--primary), + inset 0 0 0 1px var(--border-strong); } .rr-s-item__name { diff --git a/apps/console/src/app/(console)/syncs/[runId]/stream/page.tsx b/apps/console/src/app/(console)/syncs/[runId]/stream/page.tsx index e0ba76ec6..6c4962abc 100644 --- a/apps/console/src/app/(console)/syncs/[runId]/stream/page.tsx +++ b/apps/console/src/app/(console)/syncs/[runId]/stream/page.tsx @@ -44,7 +44,10 @@ interface ConnectorContext { function RunDetailLink({ children, runId }: { children: string; runId: string }) { return ( - + {children} ); @@ -327,8 +330,8 @@ function ExternalApprovalSurface({

Approve the prompt outside PDPP.

{assistance.message}

- {subject} will continue automatically after the provider confirms the approval. No browser controls are waiting - on this page. + {subject} will continue automatically after the provider confirms the approval. No browser controls are + waiting on this page.

Open run timeline diff --git a/apps/console/src/app/(console)/syncs/[runId]/stream/stream-viewer-active-presses.ts b/apps/console/src/app/(console)/syncs/[runId]/stream/stream-viewer-active-presses.ts index 2c484e42d..7a3d2803f 100644 --- a/apps/console/src/app/(console)/syncs/[runId]/stream/stream-viewer-active-presses.ts +++ b/apps/console/src/app/(console)/syncs/[runId]/stream/stream-viewer-active-presses.ts @@ -24,10 +24,7 @@ export function createActiveViewerPresses(): ActiveViewerPresses { * before disposing the viewer, because a DOM terminal event can arrive after * the old viewer has gone away. */ -export function trackActiveViewerPress( - presses: ActiveViewerPresses, - intent: RemotePointerIntent -): void { +export function trackActiveViewerPress(presses: ActiveViewerPresses, intent: RemotePointerIntent): void { if (intent.action === "pointerdown") { presses.set(intent.pointerId ?? 0, { button: intent.button ?? 0, diff --git a/apps/console/src/app/(console)/syncs/[runId]/stream/stream-viewer-input-router.test.ts b/apps/console/src/app/(console)/syncs/[runId]/stream/stream-viewer-input-router.test.ts index d75ca6767..5d81f16e1 100644 --- a/apps/console/src/app/(console)/syncs/[runId]/stream/stream-viewer-input-router.test.ts +++ b/apps/console/src/app/(console)/syncs/[runId]/stream/stream-viewer-input-router.test.ts @@ -12,11 +12,7 @@ import { NekoSurfaceAdapter, } from "@opendatalabs/remote-surface/client"; import type { RemoteSurfaceInputPayload } from "@opendatalabs/remote-surface/protocol"; -import { - completeNekoTouchScrollGesture, - deliverNekoFallbackTap, - deliverNekoTouchScrollSteps, -} from "./neko-client.ts"; +import { completeNekoTouchScrollGesture, deliverNekoFallbackTap, deliverNekoTouchScrollSteps } from "./neko-client.ts"; import { cancelActiveViewerPresses, createActiveViewerPresses, @@ -34,11 +30,16 @@ const VIEWER_WHEEL_DISPATCH_RE = /viewer\.dispatchInput\(\{\s*action:\s*"wheel"/ const DIRECT_ADAPTER_POINTER_DISPATCH_RE = /adapter\.sendPointer\(/; const TOUCH_BRIDGE_VIEWER_DISPATCH_RE = /startMobileTouchScrollBridge\(neko, options\.dispatchInput\)/; const TOUCH_SCROLL_WHEEL_INTENT_RE = /if \(options\.dispatchInput\) \{[\s\S]{0,400}source:\s*"touch-gesture"/; -const TOUCH_SCROLL_BOUNDARY_RE = /function dispatchNekoTouchScrollBoundary\([\s\S]{0,500}deltaX:\s*0,[\s\S]{0,100}deltaY:\s*0,[\s\S]{0,100}gestureBoundary:\s*true/; -const TOUCH_SCROLL_TERMINAL_HANDLER_RE = /function completeNekoTouchScrollGesture\([\s\S]{0,700}dispatchNekoTouchScrollBoundary\(state, dispatchInput\)/; -const FALLBACK_TAP_DELIVERY_RE = /function deliverNekoFallbackTap\([\s\S]{0,1000}if \(!dispatchInput\) \{[\s\S]{0,300}clickNekoAt\([\s\S]{0,1000}action: "pointerdown"[\s\S]{0,500}action: "pointerup"/; -const MOUNTED_FALLBACK_TAP_RE = /deliverNekoFallbackTap\(\{ interactionSeq: state\.interactionSeq, pointerId: state\.id \}, touch, dispatchInput\)/; -const REMOUNT_CANCEL_RE = /cancelActiveViewerPresses\(activeViewerPressesRef\.current, \(intent\) => mountedViewer\.dispatchInput\(intent\)\)/; +const TOUCH_SCROLL_BOUNDARY_RE = + /function dispatchNekoTouchScrollBoundary\([\s\S]{0,500}deltaX:\s*0,[\s\S]{0,100}deltaY:\s*0,[\s\S]{0,100}gestureBoundary:\s*true/; +const TOUCH_SCROLL_TERMINAL_HANDLER_RE = + /function completeNekoTouchScrollGesture\([\s\S]{0,700}dispatchNekoTouchScrollBoundary\(state, dispatchInput\)/; +const FALLBACK_TAP_DELIVERY_RE = + /function deliverNekoFallbackTap\([\s\S]{0,1000}if \(!dispatchInput\) \{[\s\S]{0,300}clickNekoAt\([\s\S]{0,1000}action: "pointerdown"[\s\S]{0,500}action: "pointerup"/; +const MOUNTED_FALLBACK_TAP_RE = + /deliverNekoFallbackTap\(\{ interactionSeq: state\.interactionSeq, pointerId: state\.id \}, touch, dispatchInput\)/; +const REMOUNT_CANCEL_RE = + /cancelActiveViewerPresses\(activeViewerPressesRef\.current, \(intent\) => mountedViewer\.dispatchInput\(intent\)\)/; const VIEWER_INPUT_DIAGNOSTIC_RE = /onInputDiagnostic:\s*\(event\)[\s\S]{0,400}remote_surface_viewer\.input/; interface TouchGesture { @@ -304,7 +305,10 @@ test("production delivery parity isolates touch residuals while the viewer owns }, ] ); - assert.equal(routedIntents.some((intent) => intent.action === "wheel" && intent.source !== "touch-gesture"), true); + assert.equal( + routedIntents.some((intent) => intent.action === "wheel" && intent.source !== "touch-gesture"), + true + ); }); test("console-dispatched input stays held until the viewer router settles and flushes", () => { @@ -398,11 +402,7 @@ test("rotation settle gate prevents taps landing off-target after rotation throu const router = createNekoViewerInputRouter({ adapter: routed.adapter, isSettled: () => settled }); const viewer = { dispatchInput: (intent: RemoteSurfaceInputPayload) => router.dispatch(intent) }; - deliverNekoFallbackTap( - { interactionSeq: 1, pointerId: 1 }, - tap, - (intent) => viewer.dispatchInput(intent) - ); + deliverNekoFallbackTap({ interactionSeq: 1, pointerId: 1 }, tap, (intent) => viewer.dispatchInput(intent)); assert.equal(router.queueSize(), 2); assert.deepEqual(routed.calls, []); diff --git a/apps/console/src/app/globals.css b/apps/console/src/app/globals.css index 8ec92e9fe..5d9ef96d5 100644 --- a/apps/console/src/app/globals.css +++ b/apps/console/src/app/globals.css @@ -55,7 +55,9 @@ */ @source "../../../../packages/pdpp-brand-react/src/**/*.{ts,tsx}"; -@custom-variant dark (&:where(.dark, .dark *, [data-theme=dark], [data-theme=dark] *)); +@custom-variant dark ( + &:where(.dark, .dark *, [data-theme="dark"], [data-theme="dark"] *) +); @theme inline { --font-sans: var(--font-sans); @@ -235,7 +237,8 @@ table { font-variant-numeric: var(--numeric); } - td, th { + td, + th { font-variant-numeric: inherit; } } @@ -393,7 +396,10 @@ html:has(.pdpp-stream-dialog) { .pdpp-stream-corner-controls { inset-block-start: auto; inset-inline-end: 50%; - inset-block-end: calc(env(safe-area-inset-bottom) + var(--pdpp-stream-control-offset)); + inset-block-end: calc( + env(safe-area-inset-bottom) + + var(--pdpp-stream-control-offset) + ); translate: 50% 0; align-items: center; } @@ -465,7 +471,10 @@ html:has(.pdpp-stream-dialog) { pointer-events: none; position: absolute; inset-inline: 0; - inset-block-end: calc(env(safe-area-inset-bottom) + var(--pdpp-stream-toast-offset)); + inset-block-end: calc( + env(safe-area-inset-bottom) + + var(--pdpp-stream-toast-offset) + ); z-index: 10; display: flex; max-inline-size: 24rem; @@ -645,25 +654,106 @@ html:has(.pdpp-stream-dialog) { .docs-prose { --docs-prose-spacing: 1rem; } -.docs-prose > * + * { margin-top: var(--docs-prose-spacing); } -.docs-prose h1 { font-size: 1.6rem; font-weight: 600; letter-spacing: -0.01em; margin-top: 1.75rem; } -.docs-prose h2 { font-size: 1.25rem; font-weight: 600; letter-spacing: -0.005em; margin-top: 1.75rem; padding-top: 0.5rem; border-top: 1px solid var(--border); } -.docs-prose h3 { font-size: 1.05rem; font-weight: 600; margin-top: 1.25rem; } -.docs-prose h4, .docs-prose h5, .docs-prose h6 { font-weight: 600; margin-top: 1rem; } -.docs-prose p { color: var(--foreground); } -.docs-prose a { color: var(--foreground); text-decoration: underline; text-underline-offset: 2px; } -.docs-prose a:hover { text-decoration-thickness: 2px; } -.docs-prose ul { list-style: disc; padding-left: 1.25rem; } -.docs-prose ol { list-style: decimal; padding-left: 1.25rem; } -.docs-prose li + li { margin-top: 0.25rem; } -.docs-prose li > p { margin: 0; } -.docs-prose code { font-family: var(--font-mono); font-size: 0.85em; background: var(--muted); padding: 0.1em 0.35em; border-radius: 0.3em; } -.docs-prose pre { background: var(--card); border: 1px solid var(--border); border-radius: 0.5rem; padding: 0.875rem 1rem; overflow-x: auto; font-size: 0.85em; } -.docs-prose pre code { background: transparent; padding: 0; border-radius: 0; } -.docs-prose blockquote { border-left: 2px solid var(--border); padding: 0.25rem 0 0.25rem 0.875rem; color: var(--muted-foreground); } -.docs-prose hr { border: 0; border-top: 1px solid var(--border); margin: 2rem 0; } -.docs-prose table { width: 100%; font-size: 0.9em; border-collapse: collapse; } -.docs-prose th, .docs-prose td { border-bottom: 1px solid var(--border); padding: 0.45rem 0.6rem; text-align: left; } -.docs-prose th { font-weight: 600; } -.docs-prose input[type="checkbox"] { margin-right: 0.4rem; } -.docs-prose strong { font-weight: 600; } +.docs-prose > * + * { + margin-top: var(--docs-prose-spacing); +} +.docs-prose h1 { + font-size: 1.6rem; + font-weight: 600; + letter-spacing: -0.01em; + margin-top: 1.75rem; +} +.docs-prose h2 { + font-size: 1.25rem; + font-weight: 600; + letter-spacing: -0.005em; + margin-top: 1.75rem; + padding-top: 0.5rem; + border-top: 1px solid var(--border); +} +.docs-prose h3 { + font-size: 1.05rem; + font-weight: 600; + margin-top: 1.25rem; +} +.docs-prose h4, +.docs-prose h5, +.docs-prose h6 { + font-weight: 600; + margin-top: 1rem; +} +.docs-prose p { + color: var(--foreground); +} +.docs-prose a { + color: var(--foreground); + text-decoration: underline; + text-underline-offset: 2px; +} +.docs-prose a:hover { + text-decoration-thickness: 2px; +} +.docs-prose ul { + list-style: disc; + padding-left: 1.25rem; +} +.docs-prose ol { + list-style: decimal; + padding-left: 1.25rem; +} +.docs-prose li + li { + margin-top: 0.25rem; +} +.docs-prose li > p { + margin: 0; +} +.docs-prose code { + font-family: var(--font-mono); + font-size: 0.85em; + background: var(--muted); + padding: 0.1em 0.35em; + border-radius: 0.3em; +} +.docs-prose pre { + background: var(--card); + border: 1px solid var(--border); + border-radius: 0.5rem; + padding: 0.875rem 1rem; + overflow-x: auto; + font-size: 0.85em; +} +.docs-prose pre code { + background: transparent; + padding: 0; + border-radius: 0; +} +.docs-prose blockquote { + border-left: 2px solid var(--border); + padding: 0.25rem 0 0.25rem 0.875rem; + color: var(--muted-foreground); +} +.docs-prose hr { + border: 0; + border-top: 1px solid var(--border); + margin: 2rem 0; +} +.docs-prose table { + width: 100%; + font-size: 0.9em; + border-collapse: collapse; +} +.docs-prose th, +.docs-prose td { + border-bottom: 1px solid var(--border); + padding: 0.45rem 0.6rem; + text-align: left; +} +.docs-prose th { + font-weight: 600; +} +.docs-prose input[type="checkbox"] { + margin-right: 0.4rem; +} +.docs-prose strong { + font-weight: 600; +} diff --git a/apps/console/src/app/icon.svg b/apps/console/src/app/icon.svg index 806cc2c68..cb3fac5eb 100644 --- a/apps/console/src/app/icon.svg +++ b/apps/console/src/app/icon.svg @@ -1,6 +1,6 @@ - - - - + + + + diff --git a/apps/console/src/app/well-known/skills/route.test.ts b/apps/console/src/app/well-known/skills/route.test.ts index c4153d831..94364461c 100644 --- a/apps/console/src/app/well-known/skills/route.test.ts +++ b/apps/console/src/app/well-known/skills/route.test.ts @@ -41,7 +41,9 @@ test("console .well-known skills route serves the catalog with forwarded origin" const skills = body.skills as Array<{ files: Array<{ url: string }> }>; assert.equal(skills.length, 2); assert.ok( - skills[0]?.files.some((file) => file.url === "https://pdpp.example.com/.well-known/skills/pdpp-data-access/SKILL.md") + skills[0]?.files.some( + (file) => file.url === "https://pdpp.example.com/.well-known/skills/pdpp-data-access/SKILL.md" + ) ); assert.ok( skills.some((skill) => diff --git a/apps/console/src/lib/pdpp-cli-command.test.ts b/apps/console/src/lib/pdpp-cli-command.test.ts index 8675095b0..19afed42e 100644 --- a/apps/console/src/lib/pdpp-cli-command.test.ts +++ b/apps/console/src/lib/pdpp-cli-command.test.ts @@ -43,7 +43,10 @@ test("connect command uses npx + package specifier", () => { }); test("connect command can be rendered for a concrete provider URL", () => { - assert.equal(pdppCliConnectCommandFor("https://pdpp.example.com"), "npx -y @pdpp/cli connect https://pdpp.example.com"); + assert.equal( + pdppCliConnectCommandFor("https://pdpp.example.com"), + "npx -y @pdpp/cli connect https://pdpp.example.com" + ); }); test("install command uses npx + --help", () => { diff --git a/apps/console/tailwind.config.ts b/apps/console/tailwind.config.ts index e2d038dab..0a9471ff4 100644 --- a/apps/console/tailwind.config.ts +++ b/apps/console/tailwind.config.ts @@ -1,13 +1,13 @@ // Copyright The PDP-Connect Contributors // SPDX-License-Identifier: Apache-2.0 -import type { Config } from 'tailwindcss'; +import type { Config } from "tailwindcss"; const config: Config = { content: [ - './src/pages/**/*.{js,ts,jsx,tsx,mdx}', - './src/components/**/*.{js,ts,jsx,tsx,mdx}', - './src/app/**/*.{js,ts,jsx,tsx,mdx}', + "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", + "./src/components/**/*.{js,ts,jsx,tsx,mdx}", + "./src/app/**/*.{js,ts,jsx,tsx,mdx}", ], }; diff --git a/apps/site/next.config.mjs b/apps/site/next.config.mjs index d32e32cb4..561cf664a 100644 --- a/apps/site/next.config.mjs +++ b/apps/site/next.config.mjs @@ -1,15 +1,15 @@ // Copyright The PDP-Connect Contributors // SPDX-License-Identifier: Apache-2.0 -import { createMDX } from 'fumadocs-mdx/next'; -import path from 'path'; -import { fileURLToPath } from 'url'; -import { collectAllowedDevOrigins } from './scripts/dev-origins.mjs'; +import { createMDX } from "fumadocs-mdx/next"; +import path from "path"; +import { fileURLToPath } from "url"; +import { collectAllowedDevOrigins } from "./scripts/dev-origins.mjs"; -const __dirname = fileURLToPath(new URL('.', import.meta.url)); +const __dirname = fileURLToPath(new URL(".", import.meta.url)); const withMDX = createMDX(); -const allowedDevOrigins = process.env.NODE_ENV === 'production' ? [] : collectAllowedDevOrigins(); +const allowedDevOrigins = process.env.NODE_ENV === "production" ? [] : collectAllowedDevOrigins(); function parseBuildWorkers(value) { if (!value) { @@ -24,8 +24,8 @@ const buildWorkers = parseBuildWorkers(process.env.PDPP_WEB_BUILD_WORKERS); /** @type {import('next').NextConfig} */ const nextConfig = { ...(allowedDevOrigins.length > 0 ? { allowedDevOrigins } : {}), - output: 'standalone', - outputFileTracingRoot: path.join(__dirname, '../..'), + output: "standalone", + outputFileTracingRoot: path.join(__dirname, "../.."), // Runtime file reads outside the bundled output need explicit tracing // includes so Next copies them into the standalone deploy. Without these, // /reference/coverage, the well-known agent-skill catalog, and /llms-full.txt @@ -34,21 +34,13 @@ const nextConfig = { // resolveRepoRoot() looks for a directory containing both // pnpm-workspace.yaml and openspec/, so a stub openspec marker is needed // even for routes that only read docs/agent-skills. - '/well-known/skills/**': [ - '../../docs/agent-skills/**/*.md', - '../../openspec/README.md', - '../../pnpm-workspace.yaml', - ], - '/llms-full.txt': [ - '../../docs/agent-skills/**/*.md', - '../../openspec/README.md', - '../../pnpm-workspace.yaml', - ], - '/llms.txt': [ - '../../docs/agent-skills/**/*.md', - '../../openspec/README.md', - '../../pnpm-workspace.yaml', + "/well-known/skills/**": [ + "../../docs/agent-skills/**/*.md", + "../../openspec/README.md", + "../../pnpm-workspace.yaml", ], + "/llms-full.txt": ["../../docs/agent-skills/**/*.md", "../../openspec/README.md", "../../pnpm-workspace.yaml"], + "/llms.txt": ["../../docs/agent-skills/**/*.md", "../../openspec/README.md", "../../pnpm-workspace.yaml"], }, reactStrictMode: true, experimental: { @@ -62,84 +54,84 @@ const nextConfig = { // consume its TypeScript sources directly once shim pairs (.js + .d.ts) // collapse into single .ts exports. Without this, Next's bundler would // reject .ts entries from a node_modules-resolved workspace package. - transpilePackages: ['pdpp-reference-implementation', '@pdpp/brand', '@pdpp/operator-ui'], + transpilePackages: ["pdpp-reference-implementation", "@pdpp/brand", "@pdpp/operator-ui"], async redirects() { return [ { - source: '/spec-core', - destination: '/docs/spec-core', + source: "/spec-core", + destination: "/docs/spec-core", permanent: true, }, { - source: '/spec-collection-profile', - destination: '/docs/spec-collection-profile', + source: "/spec-collection-profile", + destination: "/docs/spec-collection-profile", permanent: true, }, { - source: '/spec-architecture', - destination: '/docs/spec-architecture', + source: "/spec-architecture", + destination: "/docs/spec-architecture", permanent: true, }, { - source: '/spec-auth-design', - destination: '/docs/spec-auth-design', + source: "/spec-auth-design", + destination: "/docs/spec-auth-design", permanent: true, }, { - source: '/spec-change-tracking', - destination: '/docs/spec-change-tracking', + source: "/spec-change-tracking", + destination: "/docs/spec-change-tracking", permanent: true, }, { - source: '/spec-connector-ecosystem', - destination: '/docs/spec-connector-ecosystem', + source: "/spec-connector-ecosystem", + destination: "/docs/spec-connector-ecosystem", permanent: true, }, { - source: '/spec-data-query-api', - destination: '/docs/spec-data-query-api', + source: "/spec-data-query-api", + destination: "/docs/spec-data-query-api", permanent: true, }, { - source: '/spec-deferred', - destination: '/docs/spec-deferred', + source: "/spec-deferred", + destination: "/docs/spec-deferred", permanent: true, }, { - source: '/spec-dti-alignment', - destination: '/docs/spec-dti-alignment', + source: "/spec-dti-alignment", + destination: "/docs/spec-dti-alignment", permanent: true, }, { - source: '/spec-e2e-examples', - destination: '/docs/reference-implementation-examples', + source: "/spec-e2e-examples", + destination: "/docs/reference-implementation-examples", permanent: true, }, { - source: '/spec-reference-implementation-examples', - destination: '/docs/reference-implementation-examples', + source: "/spec-reference-implementation-examples", + destination: "/docs/reference-implementation-examples", permanent: true, }, { - source: '/e2e', - destination: '/docs/reference-implementation', + source: "/e2e", + destination: "/docs/reference-implementation", permanent: true, }, { - source: '/e2e/:path*', - destination: '/docs/reference-implementation', + source: "/e2e/:path*", + destination: "/docs/reference-implementation", permanent: true, }, { - source: '/reference-implementation', - destination: '/docs/reference-implementation', + source: "/reference-implementation", + destination: "/docs/reference-implementation", permanent: true, }, // Sandbox IA parity: sandbox/records/timeline redirects to sandbox/explore // now that the sandbox Explore canvas exists. { - source: '/sandbox/records/timeline', - destination: '/sandbox/explore', + source: "/sandbox/records/timeline", + destination: "/sandbox/explore", permanent: false, }, // NOTE: old owner-console redirects were dropped when the operator @@ -150,25 +142,25 @@ const nextConfig = { async rewrites() { return [ { - source: '/docs.mdx', - destination: '/llms.mdx/docs', + source: "/docs.mdx", + destination: "/llms.mdx/docs", }, { - source: '/docs/:path*.mdx', - destination: '/llms.mdx/docs/:path*', + source: "/docs/:path*.mdx", + destination: "/llms.mdx/docs/:path*", }, // Root agent skill discovery. Keep handlers under a filesystem-safe // internal path and expose the standards-shaped public .well-known URL. { - source: '/.well-known/skills/:path*', - destination: '/well-known/skills/:path*', + source: "/.well-known/skills/:path*", + destination: "/well-known/skills/:path*", }, // Agents that probe the well-known namespace first should still land on // the canonical agent-readable entrypoint, so there is no single // universal URL to guess. Compact alias, not duplicate content. { - source: '/.well-known/llms.txt', - destination: '/llms.txt', + source: "/.well-known/llms.txt", + destination: "/llms.txt", }, // NOTE: the top-level `/.well-known/oauth-authorization-server` and // `/.well-known/oauth-protected-resource` rewrites were dropped with the @@ -178,27 +170,27 @@ const nextConfig = { // Sandbox demo well-known metadata uses the same internal-path adapter: // handlers live under `well-known/**`; public URLs stay .well-known. { - source: '/sandbox/.well-known/oauth-authorization-server', - destination: '/sandbox/well-known/oauth-authorization-server', + source: "/sandbox/.well-known/oauth-authorization-server", + destination: "/sandbox/well-known/oauth-authorization-server", }, { - source: '/sandbox/.well-known/oauth-protected-resource', - destination: '/sandbox/well-known/oauth-protected-resource', + source: "/sandbox/.well-known/oauth-protected-resource", + destination: "/sandbox/well-known/oauth-protected-resource", }, // Sandbox demo reference-only inspection routes. Public path uses // the underscore convention from the live reference (`/_ref/`); the // handlers live under `ref/**` so Next's "private folder" rule does // not exclude them from routing. { - source: '/sandbox/_ref/:path*', - destination: '/sandbox/ref/:path*', + source: "/sandbox/_ref/:path*", + destination: "/sandbox/ref/:path*", }, ]; }, webpack(config) { config.resolve.alias = { ...config.resolve.alias, - '@': path.join(__dirname, 'src'), + "@": path.join(__dirname, "src"), }; return config; }, diff --git a/apps/site/postcss.config.mjs b/apps/site/postcss.config.mjs index 6e71b3344..95974dcd4 100644 --- a/apps/site/postcss.config.mjs +++ b/apps/site/postcss.config.mjs @@ -3,7 +3,7 @@ const config = { plugins: { - '@tailwindcss/postcss': {}, + "@tailwindcss/postcss": {}, }, }; diff --git a/apps/site/source.config.ts b/apps/site/source.config.ts index 2004e76ea..9b83995ea 100644 --- a/apps/site/source.config.ts +++ b/apps/site/source.config.ts @@ -1,12 +1,12 @@ // Copyright The PDP-Connect Contributors // SPDX-License-Identifier: Apache-2.0 -import { defineConfig, defineDocs } from 'fumadocs-mdx/config'; -import { metaSchema, pageSchema } from 'fumadocs-core/source/schema'; -import { remarkLegacyHeadingIds } from '@/lib/remark-legacy-heading-ids'; +import { defineConfig, defineDocs } from "fumadocs-mdx/config"; +import { metaSchema, pageSchema } from "fumadocs-core/source/schema"; +import { remarkLegacyHeadingIds } from "@/lib/remark-legacy-heading-ids"; export const docs = defineDocs({ - dir: 'content/docs', + dir: "content/docs", docs: { schema: pageSchema, postprocess: { diff --git a/apps/site/src/app/design/page.tsx b/apps/site/src/app/design/page.tsx index 22fcb90f5..af67f4f9b 100644 --- a/apps/site/src/app/design/page.tsx +++ b/apps/site/src/app/design/page.tsx @@ -3558,8 +3558,8 @@ function DocsSection() {
Docs primitives — DocsLayout, ArtifactLink, ProsePage, SourceLink

- Composite primitives used by /docs and intended for any future - repo-as-docs surface. DocsLayout provides the 13rem sidebar + main grid.{" "} + Composite primitives used by /docs and intended for any future repo-as-docs + surface. DocsLayout provides the 13rem sidebar + main grid.{" "} ArtifactLink is the canonical linked-row for markdown-backed indexes.{" "} ProsePage wraps raw markdown in a tinted reading surface using the{" "} .docs-prose class. SourceLink surfaces diff --git a/apps/site/src/app/globals.css b/apps/site/src/app/globals.css index decf0dccc..8672ef167 100644 --- a/apps/site/src/app/globals.css +++ b/apps/site/src/app/globals.css @@ -20,7 +20,9 @@ @import "@demodesk/neko/dist/neko.css"; @import "@pdpp/brand/docs.css"; -@custom-variant dark (&:where(.dark, .dark *, [data-theme=dark], [data-theme=dark] *)); +@custom-variant dark ( + &:where(.dark, .dark *, [data-theme="dark"], [data-theme="dark"] *) +); @theme inline { --font-sans: var(--font-sans); @@ -166,7 +168,10 @@ @apply bg-background text-foreground; font-family: var(--font-sans); } - html, body { height: 100%; } + html, + body { + height: 100%; + } } html { @@ -258,7 +263,10 @@ html > body[data-scroll-locked] { .pdpp-stream-corner-controls { inset-block-start: auto; inset-inline-end: 50%; - inset-block-end: calc(env(safe-area-inset-bottom) + var(--pdpp-stream-control-offset)); + inset-block-end: calc( + env(safe-area-inset-bottom) + + var(--pdpp-stream-control-offset) + ); translate: 50% 0; align-items: center; } @@ -316,7 +324,10 @@ html > body[data-scroll-locked] { pointer-events: none; position: absolute; inset-inline: 0; - inset-block-end: calc(env(safe-area-inset-bottom) + var(--pdpp-stream-toast-offset)); + inset-block-end: calc( + env(safe-area-inset-bottom) + + var(--pdpp-stream-toast-offset) + ); z-index: 10; display: flex; max-inline-size: 24rem; @@ -465,25 +476,106 @@ html > body[data-scroll-locked] { .docs-prose { --docs-prose-spacing: 1rem; } -.docs-prose > * + * { margin-top: var(--docs-prose-spacing); } -.docs-prose h1 { font-size: 1.6rem; font-weight: 600; letter-spacing: -0.01em; margin-top: 1.75rem; } -.docs-prose h2 { font-size: 1.25rem; font-weight: 600; letter-spacing: -0.005em; margin-top: 1.75rem; padding-top: 0.5rem; border-top: 1px solid var(--border); } -.docs-prose h3 { font-size: 1.05rem; font-weight: 600; margin-top: 1.25rem; } -.docs-prose h4, .docs-prose h5, .docs-prose h6 { font-weight: 600; margin-top: 1rem; } -.docs-prose p { color: var(--foreground); } -.docs-prose a { color: var(--foreground); text-decoration: underline; text-underline-offset: 2px; } -.docs-prose a:hover { text-decoration-thickness: 2px; } -.docs-prose ul { list-style: disc; padding-left: 1.25rem; } -.docs-prose ol { list-style: decimal; padding-left: 1.25rem; } -.docs-prose li + li { margin-top: 0.25rem; } -.docs-prose li > p { margin: 0; } -.docs-prose code { font-family: var(--font-mono); font-size: 0.85em; background: var(--muted); padding: 0.1em 0.35em; border-radius: 0.3em; } -.docs-prose pre { background: var(--card); border: 1px solid var(--border); border-radius: 0.5rem; padding: 0.875rem 1rem; overflow-x: auto; font-size: 0.85em; } -.docs-prose pre code { background: transparent; padding: 0; border-radius: 0; } -.docs-prose blockquote { border-left: 2px solid var(--border); padding: 0.25rem 0 0.25rem 0.875rem; color: var(--muted-foreground); } -.docs-prose hr { border: 0; border-top: 1px solid var(--border); margin: 2rem 0; } -.docs-prose table { width: 100%; font-size: 0.9em; border-collapse: collapse; } -.docs-prose th, .docs-prose td { border-bottom: 1px solid var(--border); padding: 0.45rem 0.6rem; text-align: left; } -.docs-prose th { font-weight: 600; } -.docs-prose input[type="checkbox"] { margin-right: 0.4rem; } -.docs-prose strong { font-weight: 600; } +.docs-prose > * + * { + margin-top: var(--docs-prose-spacing); +} +.docs-prose h1 { + font-size: 1.6rem; + font-weight: 600; + letter-spacing: -0.01em; + margin-top: 1.75rem; +} +.docs-prose h2 { + font-size: 1.25rem; + font-weight: 600; + letter-spacing: -0.005em; + margin-top: 1.75rem; + padding-top: 0.5rem; + border-top: 1px solid var(--border); +} +.docs-prose h3 { + font-size: 1.05rem; + font-weight: 600; + margin-top: 1.25rem; +} +.docs-prose h4, +.docs-prose h5, +.docs-prose h6 { + font-weight: 600; + margin-top: 1rem; +} +.docs-prose p { + color: var(--foreground); +} +.docs-prose a { + color: var(--foreground); + text-decoration: underline; + text-underline-offset: 2px; +} +.docs-prose a:hover { + text-decoration-thickness: 2px; +} +.docs-prose ul { + list-style: disc; + padding-left: 1.25rem; +} +.docs-prose ol { + list-style: decimal; + padding-left: 1.25rem; +} +.docs-prose li + li { + margin-top: 0.25rem; +} +.docs-prose li > p { + margin: 0; +} +.docs-prose code { + font-family: var(--font-mono); + font-size: 0.85em; + background: var(--muted); + padding: 0.1em 0.35em; + border-radius: 0.3em; +} +.docs-prose pre { + background: var(--card); + border: 1px solid var(--border); + border-radius: 0.5rem; + padding: 0.875rem 1rem; + overflow-x: auto; + font-size: 0.85em; +} +.docs-prose pre code { + background: transparent; + padding: 0; + border-radius: 0; +} +.docs-prose blockquote { + border-left: 2px solid var(--border); + padding: 0.25rem 0 0.25rem 0.875rem; + color: var(--muted-foreground); +} +.docs-prose hr { + border: 0; + border-top: 1px solid var(--border); + margin: 2rem 0; +} +.docs-prose table { + width: 100%; + font-size: 0.9em; + border-collapse: collapse; +} +.docs-prose th, +.docs-prose td { + border-bottom: 1px solid var(--border); + padding: 0.45rem 0.6rem; + text-align: left; +} +.docs-prose th { + font-weight: 600; +} +.docs-prose input[type="checkbox"] { + margin-right: 0.4rem; +} +.docs-prose strong { + font-weight: 600; +} diff --git a/apps/site/src/app/icon.svg b/apps/site/src/app/icon.svg index 806cc2c68..cb3fac5eb 100644 --- a/apps/site/src/app/icon.svg +++ b/apps/site/src/app/icon.svg @@ -1,6 +1,6 @@ - - - - + + + + diff --git a/apps/site/src/lib/pdpp-cli-command.test.ts b/apps/site/src/lib/pdpp-cli-command.test.ts index 9fc7c6306..2e2303c61 100644 --- a/apps/site/src/lib/pdpp-cli-command.test.ts +++ b/apps/site/src/lib/pdpp-cli-command.test.ts @@ -34,7 +34,10 @@ test("connect command uses npx + package specifier", () => { }); test("connect command can be rendered for a concrete provider URL", () => { - assert.equal(pdppCliConnectCommandFor("https://pdpp.example.com"), "npx -y @pdpp/cli connect https://pdpp.example.com"); + assert.equal( + pdppCliConnectCommandFor("https://pdpp.example.com"), + "npx -y @pdpp/cli connect https://pdpp.example.com" + ); }); test("install command uses npx + --help", () => { diff --git a/apps/site/tailwind.config.ts b/apps/site/tailwind.config.ts index e2d038dab..0a9471ff4 100644 --- a/apps/site/tailwind.config.ts +++ b/apps/site/tailwind.config.ts @@ -1,13 +1,13 @@ // Copyright The PDP-Connect Contributors // SPDX-License-Identifier: Apache-2.0 -import type { Config } from 'tailwindcss'; +import type { Config } from "tailwindcss"; const config: Config = { content: [ - './src/pages/**/*.{js,ts,jsx,tsx,mdx}', - './src/components/**/*.{js,ts,jsx,tsx,mdx}', - './src/app/**/*.{js,ts,jsx,tsx,mdx}', + "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", + "./src/components/**/*.{js,ts,jsx,tsx,mdx}", + "./src/app/**/*.{js,ts,jsx,tsx,mdx}", ], }; diff --git a/deploy/railway/core-first-boot.mjs b/deploy/railway/core-first-boot.mjs index ba42ff092..54075df2e 100644 --- a/deploy/railway/core-first-boot.mjs +++ b/deploy/railway/core-first-boot.mjs @@ -30,19 +30,19 @@ // The password appears exactly once, in the first-boot banner on stdout // (`docker logs`), and in the mode-0600 file on the data volume. It is never // logged anywhere else and never passed via argv. -import { randomBytes } from 'node:crypto'; -import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; -import path from 'node:path'; +import { randomBytes } from "node:crypto"; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import path from "node:path"; -export const DEFAULT_DATA_DIR = '/var/lib/pdpp'; -export const OWNER_PASSWORD_FILENAME = 'owner-password'; -export const CREDENTIAL_KEY_FILENAME = 'credential-encryption-key'; +export const DEFAULT_DATA_DIR = "/var/lib/pdpp"; +export const OWNER_PASSWORD_FILENAME = "owner-password"; +export const CREDENTIAL_KEY_FILENAME = "credential-encryption-key"; -const LOG_PREFIX = '[railway-core]'; -const BANNER_RULE = '─'.repeat(64); +const LOG_PREFIX = "[railway-core]"; +const BANNER_RULE = "─".repeat(64); function trimmedValue(value) { - if (typeof value !== 'string') return null; + if (typeof value !== "string") return null; const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : null; } @@ -52,7 +52,7 @@ function trimmedValue(value) { // so "keep the database" and "keep the credentials" are the same operator act. export function resolveDataDir(env = process.env) { const dbPath = trimmedValue(env.PDPP_DB_PATH); - if (dbPath && dbPath !== ':memory:') { + if (dbPath && dbPath !== ":memory:") { return path.dirname(dbPath); } return DEFAULT_DATA_DIR; @@ -63,15 +63,15 @@ export function resolveDataDir(env = process.env) { // PDPP_STORAGE_BACKEND wins, otherwise a database URL selects Postgres. function usesPostgresStorage(env) { const explicit = trimmedValue(env.PDPP_STORAGE_BACKEND)?.toLowerCase(); - if (explicit === 'postgres') return true; - if (explicit === 'sqlite') return false; + if (explicit === "postgres") return true; + if (explicit === "sqlite") return false; return Boolean(trimmedValue(env.PDPP_DATABASE_URL) ?? trimmedValue(env.DATABASE_URL)); } function readPersistedSecret(file) { if (!existsSync(file)) return null; try { - return trimmedValue(readFileSync(file, 'utf8')); + return trimmedValue(readFileSync(file, "utf8")); } catch { return null; } @@ -85,23 +85,23 @@ function persistSecret(dataDir, file, secret) { export function buildFirstBootBanner({ origin, password, passwordFile, persisted }) { const lines = [ BANNER_RULE, - 'First boot — generated an owner password for this instance.', - '', + "First boot — generated an owner password for this instance.", + "", ` Dashboard: ${origin}/`, ` Owner password: ${password}`, - '', + "", ]; if (persisted) { lines.push( `Saved to ${passwordFile} (on the data volume), so restarts keep`, - 'this password. To change it, set the PDPP_OWNER_PASSWORD environment', - 'variable and restart; the environment variable always wins.', - 'This password is printed only on first boot.', + "this password. To change it, set the PDPP_OWNER_PASSWORD environment", + "variable and restart; the environment variable always wins.", + "This password is printed only on first boot." ); } else { lines.push( - 'WARNING: the password could not be persisted and will change on the', - 'next boot. Set PDPP_OWNER_PASSWORD to keep a stable password.', + "WARNING: the password could not be persisted and will change on the", + "next boot. Set PDPP_OWNER_PASSWORD to keep a stable password." ); } lines.push(BANNER_RULE); @@ -128,17 +128,17 @@ export function prepareFirstBoot({ if (password) { log(`${LOG_PREFIX} owner password loaded from ${passwordFile}`); } else { - password = randomBytes(18).toString('base64url'); + password = randomBytes(18).toString("base64url"); let persisted = true; try { persistSecret(dataDir, passwordFile, password); } catch (err) { persisted = false; warn( - `${LOG_PREFIX} warning: could not persist the generated owner password to ${passwordFile} (${err?.code || err?.message}); it will be regenerated on the next boot`, + `${LOG_PREFIX} warning: could not persist the generated owner password to ${passwordFile} (${err?.code || err?.message}); it will be regenerated on the next boot` ); } - const origin = (trimmedValue(env.PDPP_REFERENCE_ORIGIN) || 'http://localhost:3000').replace(/\/+$/, ''); + const origin = (trimmedValue(env.PDPP_REFERENCE_ORIGIN) || "http://localhost:3000").replace(/\/+$/, ""); bannerLines = buildFirstBootBanner({ origin, password, passwordFile, persisted }); } envAdditions.PDPP_OWNER_PASSWORD = password; @@ -152,15 +152,15 @@ export function prepareFirstBoot({ const keyFile = path.join(dataDir, CREDENTIAL_KEY_FILENAME); try { if (!readPersistedSecret(keyFile)) { - persistSecret(dataDir, keyFile, randomBytes(32).toString('hex')); + persistSecret(dataDir, keyFile, randomBytes(32).toString("hex")); log( - `${LOG_PREFIX} generated a credential encryption key at ${keyFile} (never printed; keep the data volume to keep sealed credentials readable)`, + `${LOG_PREFIX} generated a credential encryption key at ${keyFile} (never printed; keep the data volume to keep sealed credentials readable)` ); } envAdditions.PDPP_CREDENTIAL_ENCRYPTION_KEY_FILE = keyFile; } catch (err) { warn( - `${LOG_PREFIX} warning: could not provision a credential encryption key at ${keyFile} (${err?.code || err?.message}); static-secret connector setup stays fail-closed until PDPP_CREDENTIAL_ENCRYPTION_KEY is configured`, + `${LOG_PREFIX} warning: could not provision a credential encryption key at ${keyFile} (${err?.code || err?.message}); static-secret connector setup stays fail-closed until PDPP_CREDENTIAL_ENCRYPTION_KEY is configured` ); } } diff --git a/deploy/railway/core-supervisor.mjs b/deploy/railway/core-supervisor.mjs index 23872a39f..a69ecabda 100644 --- a/deploy/railway/core-supervisor.mjs +++ b/deploy/railway/core-supervisor.mjs @@ -2,10 +2,10 @@ // Copyright The PDP-Connect Contributors // SPDX-License-Identifier: Apache-2.0 -import { spawn } from 'node:child_process'; -import process from 'node:process'; +import { spawn } from "node:child_process"; +import process from "node:process"; -import { prepareFirstBoot } from './core-first-boot.mjs'; +import { prepareFirstBoot } from "./core-first-boot.mjs"; const children = new Map(); let shuttingDown = false; @@ -14,10 +14,10 @@ let exitCode = 0; function start(name, command, args, options) { const child = spawn(command, args, { ...options, - stdio: 'inherit', + stdio: "inherit", }); children.set(name, child); - child.on('exit', (code, signal) => { + child.on("exit", (code, signal) => { children.delete(name); if (code !== 0 || signal) { exitCode = code ?? 1; @@ -31,7 +31,7 @@ function start(name, command, args, options) { shuttingDown = true; for (const [otherName, other] of children.entries()) { console.error(`[railway-core] ${name} exited; stopping ${otherName}`); - other.kill('SIGTERM'); + other.kill("SIGTERM"); } if (children.size === 0) { process.exit(exitCode); @@ -53,8 +53,8 @@ function stop(signal) { } } -process.on('SIGTERM', () => stop('SIGTERM')); -process.on('SIGINT', () => stop('SIGINT')); +process.on("SIGTERM", () => stop("SIGTERM")); +process.on("SIGINT", () => stop("SIGINT")); // Standalone-image credential bootstrap: generate (first boot) or load // (subsequent boots) an owner password when the platform did not supply one, @@ -64,30 +64,30 @@ const firstBoot = prepareFirstBoot(); const referenceEnv = { ...process.env, ...firstBoot.env, - AS_PORT: '7662', - RS_PORT: '7663', - PDPP_AS_URL: 'http://127.0.0.1:7662', - PDPP_RS_URL: 'http://127.0.0.1:7663', + AS_PORT: "7662", + RS_PORT: "7663", + PDPP_AS_URL: "http://127.0.0.1:7662", + PDPP_RS_URL: "http://127.0.0.1:7663", }; const consoleEnv = { ...process.env, ...firstBoot.env, - HOSTNAME: process.env.HOSTNAME || '0.0.0.0', - PORT: process.env.PORT || '3000', - PDPP_AS_URL: 'http://127.0.0.1:7662', - PDPP_RS_URL: 'http://127.0.0.1:7663', + HOSTNAME: process.env.HOSTNAME || "0.0.0.0", + PORT: process.env.PORT || "3000", + PDPP_AS_URL: "http://127.0.0.1:7662", + PDPP_RS_URL: "http://127.0.0.1:7663", }; for (const line of firstBoot.bannerLines) { console.log(line); } -start('reference', process.execPath, ['/app/reference-implementation/server/index.js'], { - cwd: '/app', +start("reference", process.execPath, ["/app/reference-implementation/server/index.js"], { + cwd: "/app", env: referenceEnv, }); -start('console', process.execPath, ['/console/apps/console/server.js'], { - cwd: '/console', +start("console", process.execPath, ["/console/apps/console/server.js"], { + cwd: "/console", env: consoleEnv, }); diff --git a/docker/neko/policies.json b/docker/neko/policies.json index cdaf3465d..f846ea62f 100644 --- a/docker/neko/policies.json +++ b/docker/neko/policies.json @@ -20,11 +20,6 @@ "PasswordManagerEnabled": false, "BrowserLabsEnabled": false, "RestoreOnStartup": 1, - "URLAllowlist": [ - "file:///home/user/Downloads" - ], - "URLBlocklist": [ - "file://*", - "chrome://policy" - ] + "URLAllowlist": ["file:///home/user/Downloads"], + "URLBlocklist": ["file://*", "chrome://policy"] } diff --git a/docs/design-system/ink-carbon/project/Ink Carbon - System Sheet.html b/docs/design-system/ink-carbon/project/Ink Carbon - System Sheet.html index cbbe7f02d..0f7443570 100644 --- a/docs/design-system/ink-carbon/project/Ink Carbon - System Sheet.html +++ b/docs/design-system/ink-carbon/project/Ink Carbon - System Sheet.html @@ -1,48 +1,154 @@ - - - -PDPP — Ink Carbon, the system sheet - - - - -

-

Ink Carbon

-

The committed PDPP design system. The carbon duplicate marks what the server retains for you; the typed voice separates what the protocol writes from what humans read; color is spent, never sprayed; paper is square. Full doctrine in README.md — these are the live cards.

-
-
-
Brand
-
Sheets & carbon
-
Grant record
-
Data list
-
Consent scopes
-
Endorsements
-
Buttons
-
Inputs
-
Type · display
-
Type · typed voice
-
Type · body
-
Colors · core
-
Colors · state
-
Spent color
-
Elevation = carbon
-
Corners
-
Glyphs, not icons
-
Layout
-
Code
-
Spacing
-
- + + + + PDPP — Ink Carbon, the system sheet + + + + +
+

Ink Carbon

+

+ The committed PDPP design system. The carbon duplicate marks what the server retains for you; + the typed voice separates what the protocol writes from what humans read; color is spent, never + sprayed; paper is square. Full doctrine in README.md — these are the live cards. +

+
+
+
+
Brand
+ +
+
+
Sheets & carbon
+ +
+
+
Grant record
+ +
+
+
Data list
+ +
+
+
Consent scopes
+ +
+
+
Endorsements
+ +
+
+
Buttons
+ +
+
+
Inputs
+ +
+
+
Type · display
+ +
+
+
Type · typed voice
+ +
+
+
Type · body
+ +
+
+
Colors · core
+ +
+
+
Colors · state
+ +
+
+
Spent color
+ +
+
+
Elevation = carbon
+ +
+
+
Corners
+ +
+
+
Glyphs, not icons
+ +
+
+
Layout
+ +
+
+
Code
+ +
+
+
Spacing
+ +
+
+ diff --git a/docs/design-system/ink-carbon/project/PDPP Explorer.html b/docs/design-system/ink-carbon/project/PDPP Explorer.html index 199b02aa7..3c06bf489 100644 --- a/docs/design-system/ink-carbon/project/PDPP Explorer.html +++ b/docs/design-system/ink-carbon/project/PDPP Explorer.html @@ -1,32 +1,44 @@ - - - - PDPP Explorer + + + + PDPP Explorer - - + + - - - + + + - - - + + + - - - - - - - - - - - -
- + + + + + + + + + + + +
+ diff --git a/docs/design-system/ink-carbon/project/PDPP Round 3.html b/docs/design-system/ink-carbon/project/PDPP Round 3.html index 9cced419e..e8e51a81e 100644 --- a/docs/design-system/ink-carbon/project/PDPP Round 3.html +++ b/docs/design-system/ink-carbon/project/PDPP Round 3.html @@ -1,26 +1,43 @@ - - - - PDPP — Round 3, on the real surfaces + + + + PDPP — Round 3, on the real surfaces - + - - - + + + - - + + - - - -
- - + diff --git a/docs/design-system/ink-carbon/project/PDPP Vibe Studies v2.html b/docs/design-system/ink-carbon/project/PDPP Vibe Studies v2.html index 4f9fde3e5..b6e45471a 100644 --- a/docs/design-system/ink-carbon/project/PDPP Vibe Studies v2.html +++ b/docs/design-system/ink-carbon/project/PDPP Vibe Studies v2.html @@ -1,27 +1,44 @@ - - - - PDPP — Vibe Studies, as real screens + + + + PDPP — Vibe Studies, as real screens - - + + - - - + + + - - + + - - - -
- - + diff --git a/docs/design-system/ink-carbon/project/PDPP Vibe Studies.html b/docs/design-system/ink-carbon/project/PDPP Vibe Studies.html index 9c426b85c..0ad6d44ff 100644 --- a/docs/design-system/ink-carbon/project/PDPP Vibe Studies.html +++ b/docs/design-system/ink-carbon/project/PDPP Vibe Studies.html @@ -1,27 +1,44 @@ - - - - PDPP — Vibe Studies (Round 2) + + + + PDPP — Vibe Studies (Round 2) - - + + - - - + + + - - + + - - - -
- - + diff --git a/docs/design-system/ink-carbon/project/Recordroom - Console.html b/docs/design-system/ink-carbon/project/Recordroom - Console.html index 4dfc16e2e..13a15d859 100644 --- a/docs/design-system/ink-carbon/project/Recordroom - Console.html +++ b/docs/design-system/ink-carbon/project/Recordroom - Console.html @@ -1,42 +1,59 @@ - - - - Recordroom — your data, on your terms + + + + Recordroom — your data, on your terms - - - - - - - - - + + + + + + + + + - - - + + + - - - - - - - - - - - - + + + + + + + + + + + + - - - -
- + + + +
+ diff --git a/docs/design-system/ink-carbon/project/Watcher - What Longview Can See.html b/docs/design-system/ink-carbon/project/Watcher - What Longview Can See.html index c0fcac4a1..23319ce49 100644 --- a/docs/design-system/ink-carbon/project/Watcher - What Longview Can See.html +++ b/docs/design-system/ink-carbon/project/Watcher - What Longview Can See.html @@ -1,102 +1,131 @@ - - - -What Longview can see — PDPP - - - - - - - -
- - - -
- The picture they can paint -

From two streams, Longview can assemble this about you.

- -
-
-

What Longview knows

- 3 inferences · 2 streams -
-
-
- You earn about $5,210 gross, $3,508 take-home, every month. - pay_statements.read - employer · period · gross_pay · net_pay + + + + What Longview can see — PDPP + + + + + + + +
+
+ + + +
+ The picture they can paint +

From two streams, Longview can assemble this about you.

-
-
-

What stays yours

- never crossed -
-
-
What you spend it ontransactions · never granted to Longview
-
What you owe in taxestax_docs · you declined this at consent
-
Where your money is heldbank_routing · projected out of every response
-
What you listen to, where you goother streams · outside this purpose
-
-

This is the product. Longview hired a planner's view of your income — and got exactly that, nothing adjacent. The boundary isn't a promise in a privacy policy; the server enforces it on every read.

-
+
+
+

What Longview knows

+ 3 inferences · 2 streams +
+
+
+ You earn about $5,210 gross, $3,508 take-home, every month. + pay_statements.read + employer · period · gross_pay · net_pay +
+
+ You've worked at Acme Co since April 2023 — about 3 years. + employment.read + employer · start_date · end_date +
+
+ Your income has been steady for 14 months — no gaps, no dips. + derived · pay history + a pattern across 14 statements, not a field you gave +
+
+
-
- Carbon — this portrait is your copy of the relationship - 5 of 13 fields cross +
+
+

What stays yours

+ never crossed +
+
+
+ What you spend it ontransactions · never granted to Longview +
+
+ What you owe in taxestax_docs · you declined this at consent +
+
+ Where your money is heldbank_routing · projected out of every response +
+
+ What you listen to, where you goother streams · outside this purpose +
+
+

+ This is the product. + Longview hired a planner's view of your income — and got exactly that, nothing adjacent. The boundary isn't + a promise in a privacy policy; the server enforces it on every read. +

+
+ +
+ Carbon — this portrait is your copy of the relationship + 5 of 13 fields cross +
+
- -
- - + + diff --git a/docs/design-system/ink-carbon/project/_adherence.oxlintrc.json b/docs/design-system/ink-carbon/project/_adherence.oxlintrc.json index 303309b88..980048a3c 100644 --- a/docs/design-system/ink-carbon/project/_adherence.oxlintrc.json +++ b/docs/design-system/ink-carbon/project/_adherence.oxlintrc.json @@ -1,8 +1,5 @@ { - "plugins": [ - "react", - "import" - ], + "plugins": ["react", "import"], "rules": { "react/forbid-elements": [ "warn", @@ -45,9 +42,7 @@ }, "overrides": [ { - "files": [ - "**/index.js" - ], + "files": ["**/index.js"], "rules": { "no-restricted-imports": "off" } @@ -223,11 +218,6 @@ "--ease-read": "other", "--ease-settle": "other" }, - "fontFamilies": [ - "Fraunces", - "Geist", - "JetBrains Mono", - "Schibsted Grotesk" - ] + "fontFamilies": ["Fraunces", "Geist", "JetBrains Mono", "Schibsted Grotesk"] } -} \ No newline at end of file +} diff --git a/docs/design-system/ink-carbon/project/_ds_manifest.json b/docs/design-system/ink-carbon/project/_ds_manifest.json index 97775ce50..4cb88bd53 100644 --- a/docs/design-system/ink-carbon/project/_ds_manifest.json +++ b/docs/design-system/ink-carbon/project/_ds_manifest.json @@ -1 +1,1061 @@ -{"namespace":"PDPPDesignSystem_b4e65a","components":[],"startingPoints":[],"cards":[{"path":"preview/brand-logo.html","group":"Brand","viewport":"700x230","subtitle":"Mark, wordmark, and the carbon signature","name":"Brand"},{"path":"preview/iconography.html","group":"Brand","viewport":"700x200","subtitle":"The system draws no pictures — status is typed","name":"Glyphs, not icons"},{"path":"preview/colors-primary.html","group":"Colors","viewport":"700x170","subtitle":"Paper, ink, protocol ink, human warmth","name":"Colors · Core"},{"path":"preview/colors-semantic.html","group":"Colors","viewport":"700x170","subtitle":"Recorded, expiring, destructive — each spent on one meaning","name":"Colors · State"},{"path":"preview/colors-washes.html","group":"Colors","viewport":"700x230","subtitle":"Where hue is allowed — and where it is not","name":"Spent color"},{"path":"preview/components-badges.html","group":"Components","viewport":"700x190","subtitle":"Status, typed by the protocol — and neutral tags","name":"Endorsements"},{"path":"preview/components-buttons.html","group":"Components","viewport":"700x150","subtitle":"Ink, human act, ghost, destructive","name":"Buttons"},{"path":"preview/components-cards.html","group":"Components","viewport":"700x260","subtitle":"The paper artifact — and the duplicate the server keeps","name":"Sheets & carbon"},{"path":"preview/components-code.html","group":"Components","viewport":"700x230","subtitle":"Protocol payloads — the typed voice at full width","name":"Code"},{"path":"preview/components-consent-row.html","group":"Components","viewport":"700x230","subtitle":"Scope rows — granted typed, declined struck","name":"Consent scopes"},{"path":"preview/components-dataflow.html","group":"Components","viewport":"700x270","subtitle":"The aligned list — shared column axis, mandatory headers","name":"Table"},{"path":"preview/components-grant-card.html","group":"Components","viewport":"700x330","subtitle":"The full sheet — kv block, endorsement, actions","name":"Grant record"},{"path":"preview/components-inputs.html","group":"Components","viewport":"700x200","subtitle":"Fields — mono values, grotesk labels","name":"Inputs"},{"path":"preview/layout-grid.html","group":"Spacing","viewport":"700x220","subtitle":"Content widths, sidebar, stat band","name":"Layout"},{"path":"preview/radii.html","group":"Spacing","viewport":"700x190","subtitle":"Paper is square; controls get a 2px ease","name":"Corners"},{"path":"preview/shadows.html","group":"Spacing","viewport":"700x230","subtitle":"Nothing floats — retention casts carbon instead of shadow","name":"Elevation"},{"path":"preview/spacing-scale.html","group":"Spacing","viewport":"700x170","subtitle":"4px grid — compose padding and gap from these steps","name":"Spacing"},{"path":"preview/type-body.html","group":"Type","viewport":"700x220","subtitle":"Body, title, label, caption","name":"Type · Body"},{"path":"preview/type-display.html","group":"Type","viewport":"700x230","subtitle":"Schibsted Grotesk — the human voice, set heavy","name":"Type · Display"},{"path":"preview/type-mono.html","group":"Type","viewport":"700x230","subtitle":"JetBrains Mono — everything the protocol writes","name":"Type · Typed voice"}],"templates":[],"globalCssPaths":["colors_and_type.css","ui_kits/web/kit.css","explorer/explorer.css","labs/labs.css","styles.css"],"tokens":[{"name":"--font-sans","value":"\"Geist\", ui-sans-serif, system-ui, sans-serif","kind":"font","definedIn":"labs/labs.css"},{"name":"--font-mono","value":"\"JetBrains Mono\", ui-monospace, monospace","kind":"font","definedIn":"labs/labs.css"},{"name":"--background","value":"oklch(0.985 0.004 90)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--foreground","value":"oklch(0.18 0.005 270)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--card","value":"oklch(1 0 0)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--card-foreground","value":"oklch(0.18 0.005 270)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--popover","value":"oklch(1 0 0)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--popover-foreground","value":"oklch(0.18 0.005 270)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--primary","value":"oklch(0.46 0.11 255)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--primary-foreground","value":"oklch(0.99 0.002 90)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--primary-tint","value":"oklch(0.46 0.11 255 / 0.08)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--human","value":"oklch(0.52 0.11 45)","kind":"color","definedIn":"labs/labs.css"},{"name":"--human-foreground","value":"oklch(0.99 0.005 90)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--human-tint","value":"oklch(0.55 0.11 45 / 0.08)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--carbon-outline","value":"oklch(0.46 0.11 255 / 0.40)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--carbon-fill","value":"oklch(0.46 0.11 255 / 0.09)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--carbon-offset","value":"9px","kind":"spacing","definedIn":"colors_and_type.css"},{"name":"--secondary","value":"oklch(0.955 0.004 270)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--secondary-foreground","value":"oklch(0.22 0.005 270)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--muted","value":"oklch(0.955 0.004 270)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--muted-foreground","value":"oklch(0.47 0.008 270)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--accent","value":"oklch(0.955 0.004 270)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--accent-foreground","value":"oklch(0.22 0.005 270)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--border","value":"oklch(0.905 0.005 270)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--border-strong","value":"oklch(0.79 0.006 270)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--input","value":"oklch(0.79 0.006 270)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--ring","value":"oklch(0.46 0.11 255)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--success","value":"oklch(0.50 0.11 158)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--success-tint","value":"oklch(0.50 0.11 158 / 0.10)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--warning","value":"oklch(0.58 0.13 70)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--warning-tint","value":"oklch(0.58 0.13 70 / 0.10)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--destructive","value":"oklch(0.52 0.16 27)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--destructive-foreground","value":"oklch(0.99 0.002 90)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--destructive-tint","value":"oklch(0.52 0.16 27 / 0.08)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--primary-wash","value":"var(--primary-tint)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--human-wash","value":"oklch(0.52 0.11 45 / 0.08)","kind":"color","definedIn":"labs/labs.css"},{"name":"--success-wash","value":"var(--success-tint)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--success-wash-strong","value":"oklch(0.50 0.11 158 / 0.16)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--warning-wash","value":"var(--warning-tint)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--verified-wash","value":"var(--success-tint)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--edu-fg","value":"oklch(0.50 0.06 210)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--radius","value":"0px","kind":"radius","definedIn":"colors_and_type.css"},{"name":"--radius-control","value":"2px","kind":"radius","definedIn":"colors_and_type.css"},{"name":"--duration-instant","value":"0ms","kind":"other","definedIn":"colors_and_type.css","annotation":"other"},{"name":"--duration-fast","value":"0.01ms","kind":"other","definedIn":"colors_and_type.css","annotation":"other"},{"name":"--duration-base","value":"0.01ms","kind":"other","definedIn":"colors_and_type.css","annotation":"other"},{"name":"--duration-moderate","value":"0.01ms","kind":"other","definedIn":"colors_and_type.css","annotation":"other"},{"name":"--duration-slow","value":"0.01ms","kind":"other","definedIn":"colors_and_type.css","annotation":"other"},{"name":"--ease-enter","value":"cubic-bezier(0, 0, 0.2, 1)","kind":"other","definedIn":"colors_and_type.css","annotation":"other"},{"name":"--ease-exit","value":"cubic-bezier(0.4, 0, 1, 1)","kind":"other","definedIn":"colors_and_type.css","annotation":"other"},{"name":"--ease-standard","value":"cubic-bezier(0.4, 0, 0.2, 1)","kind":"other","definedIn":"colors_and_type.css","annotation":"other"},{"name":"--motion-enter","value":"var(--duration-moderate) var(--ease-enter)","kind":"other","definedIn":"colors_and_type.css","annotation":"other"},{"name":"--motion-exit","value":"var(--duration-fast) var(--ease-exit)","kind":"other","definedIn":"colors_and_type.css","annotation":"other"},{"name":"--motion-state","value":"var(--duration-base) var(--ease-standard)","kind":"other","definedIn":"colors_and_type.css","annotation":"other"},{"name":"--stagger-fast","value":"30ms","kind":"other","definedIn":"colors_and_type.css","annotation":"other"},{"name":"--stagger-base","value":"50ms","kind":"other","definedIn":"colors_and_type.css","annotation":"other"},{"name":"--content-width","value":"760px","kind":"spacing","definedIn":"colors_and_type.css"},{"name":"--content-wide-width","value":"1200px","kind":"spacing","definedIn":"colors_and_type.css"},{"name":"--chrome-height","value":"3.5rem","kind":"spacing","definedIn":"colors_and_type.css"},{"name":"--pdpp-sidebar-width","value":"280px","kind":"spacing","definedIn":"colors_and_type.css"},{"name":"--background","value":"oklch(0.17 0.006 262)","kind":"color","definedIn":"colors_and_type.css","scope":"html[data-theme=\"dark\"])"},{"name":"--foreground","value":"oklch(0.95 0.005 262)","kind":"color","definedIn":"colors_and_type.css","scope":"html[data-theme=\"dark\"])"},{"name":"--card","value":"oklch(0.21 0.007 262)","kind":"color","definedIn":"colors_and_type.css","scope":"html[data-theme=\"dark\"])"},{"name":"--card-foreground","value":"oklch(0.95 0.005 262)","kind":"color","definedIn":"colors_and_type.css","scope":"html[data-theme=\"dark\"])"},{"name":"--popover","value":"oklch(0.24 0.007 262)","kind":"color","definedIn":"colors_and_type.css","scope":"html[data-theme=\"dark\"])"},{"name":"--popover-foreground","value":"oklch(0.95 0.005 262)","kind":"color","definedIn":"colors_and_type.css","scope":"html[data-theme=\"dark\"])"},{"name":"--primary","value":"oklch(0.74 0.13 255)","kind":"color","definedIn":"colors_and_type.css","scope":"html[data-theme=\"dark\"])"},{"name":"--primary-foreground","value":"oklch(0.15 0.01 262)","kind":"color","definedIn":"colors_and_type.css","scope":"html[data-theme=\"dark\"])"},{"name":"--primary-tint","value":"oklch(0.74 0.13 255 / 0.13)","kind":"color","definedIn":"colors_and_type.css","scope":"html[data-theme=\"dark\"])"},{"name":"--human","value":"oklch(0.76 0.12 45)","kind":"color","definedIn":"colors_and_type.css","scope":"html[data-theme=\"dark\"])"},{"name":"--human-foreground","value":"oklch(0.16 0.01 45)","kind":"color","definedIn":"colors_and_type.css","scope":"html[data-theme=\"dark\"])"},{"name":"--human-tint","value":"oklch(0.76 0.12 45 / 0.11)","kind":"color","definedIn":"colors_and_type.css","scope":"html[data-theme=\"dark\"])"},{"name":"--carbon-outline","value":"oklch(0.74 0.13 255 / 0.62)","kind":"color","definedIn":"colors_and_type.css","scope":"html[data-theme=\"dark\"])"},{"name":"--carbon-fill","value":"oklch(0.74 0.13 255 / 0.22)","kind":"color","definedIn":"colors_and_type.css","scope":"html[data-theme=\"dark\"])"},{"name":"--secondary","value":"oklch(0.26 0.007 262)","kind":"color","definedIn":"colors_and_type.css","scope":"html[data-theme=\"dark\"])"},{"name":"--secondary-foreground","value":"oklch(0.95 0.005 262)","kind":"color","definedIn":"colors_and_type.css","scope":"html[data-theme=\"dark\"])"},{"name":"--muted","value":"oklch(0.24 0.007 262)","kind":"color","definedIn":"colors_and_type.css","scope":"html[data-theme=\"dark\"])"},{"name":"--muted-foreground","value":"oklch(0.72 0.01 262)","kind":"color","definedIn":"colors_and_type.css","scope":"html[data-theme=\"dark\"])"},{"name":"--accent","value":"oklch(0.26 0.007 262)","kind":"color","definedIn":"colors_and_type.css","scope":"html[data-theme=\"dark\"])"},{"name":"--accent-foreground","value":"oklch(0.95 0.005 262)","kind":"color","definedIn":"colors_and_type.css","scope":"html[data-theme=\"dark\"])"},{"name":"--border","value":"oklch(0.29 0.008 262)","kind":"color","definedIn":"colors_and_type.css","scope":"html[data-theme=\"dark\"])"},{"name":"--border-strong","value":"oklch(0.40 0.008 262)","kind":"color","definedIn":"colors_and_type.css","scope":"html[data-theme=\"dark\"])"},{"name":"--input","value":"oklch(0.34 0.008 262)","kind":"color","definedIn":"colors_and_type.css","scope":"html[data-theme=\"dark\"])"},{"name":"--ring","value":"oklch(0.74 0.13 255)","kind":"color","definedIn":"colors_and_type.css","scope":"html[data-theme=\"dark\"])"},{"name":"--success","value":"oklch(0.76 0.13 158)","kind":"color","definedIn":"colors_and_type.css","scope":"html[data-theme=\"dark\"])"},{"name":"--success-tint","value":"oklch(0.76 0.13 158 / 0.14)","kind":"color","definedIn":"colors_and_type.css","scope":"html[data-theme=\"dark\"])"},{"name":"--warning","value":"oklch(0.80 0.14 75)","kind":"color","definedIn":"colors_and_type.css","scope":"html[data-theme=\"dark\"])"},{"name":"--warning-tint","value":"oklch(0.80 0.14 75 / 0.13)","kind":"color","definedIn":"colors_and_type.css","scope":"html[data-theme=\"dark\"])"},{"name":"--destructive","value":"oklch(0.70 0.16 27)","kind":"color","definedIn":"colors_and_type.css","scope":"html[data-theme=\"dark\"])"},{"name":"--destructive-foreground","value":"oklch(0.15 0.01 262)","kind":"color","definedIn":"colors_and_type.css","scope":"html[data-theme=\"dark\"])"},{"name":"--destructive-tint","value":"oklch(0.70 0.16 27 / 0.12)","kind":"color","definedIn":"colors_and_type.css","scope":"html[data-theme=\"dark\"])"},{"name":"--primary-wash","value":"var(--primary-tint)","kind":"color","definedIn":"colors_and_type.css","scope":"html[data-theme=\"dark\"])"},{"name":"--human-wash","value":"var(--human-tint)","kind":"color","definedIn":"colors_and_type.css","scope":"html[data-theme=\"dark\"])"},{"name":"--success-wash","value":"var(--success-tint)","kind":"color","definedIn":"colors_and_type.css","scope":"html[data-theme=\"dark\"])"},{"name":"--success-wash-strong","value":"oklch(0.76 0.13 158 / 0.20)","kind":"color","definedIn":"colors_and_type.css","scope":"html[data-theme=\"dark\"])"},{"name":"--warning-wash","value":"var(--warning-tint)","kind":"color","definedIn":"colors_and_type.css","scope":"html[data-theme=\"dark\"])"},{"name":"--verified-wash","value":"var(--success-tint)","kind":"color","definedIn":"colors_and_type.css","scope":"html[data-theme=\"dark\"])"},{"name":"--edu-fg","value":"oklch(0.78 0.06 210)","kind":"color","definedIn":"colors_and_type.css","scope":"html[data-theme=\"dark\"])"},{"name":"--font-serif","value":"\"Fraunces\", Georgia, serif","kind":"font","definedIn":"labs/labs.css"},{"name":"--human-deep","value":"oklch(0.38 0.09 40)","kind":"color","definedIn":"labs/labs.css"},{"name":"--human-glow","value":"oklch(0.68 0.14 45)","kind":"color","definedIn":"labs/labs.css"},{"name":"--protocol","value":"oklch(0.58 0.18 253)","kind":"color","definedIn":"labs/labs.css"},{"name":"--protocol-deep","value":"oklch(0.42 0.14 253)","kind":"color","definedIn":"labs/labs.css"},{"name":"--protocol-wash","value":"oklch(0.58 0.18 253 / 0.08)","kind":"color","definedIn":"labs/labs.css"},{"name":"--protocol-glow","value":"oklch(0.72 0.16 253)","kind":"color","definedIn":"labs/labs.css"},{"name":"--thermal","value":"linear-gradient(90deg, var(--human) 0%, oklch(0.55 0.1 180) 50%, var(--protocol) 100%)","kind":"color","definedIn":"labs/labs.css"},{"name":"--paper","value":"oklch(0.985 0.005 85)","kind":"color","definedIn":"labs/labs.css"},{"name":"--paper-warm","value":"oklch(0.98 0.008 75)","kind":"color","definedIn":"labs/labs.css"},{"name":"--ink","value":"oklch(0.16 0.01 60)","kind":"color","definedIn":"labs/labs.css"},{"name":"--ink-soft","value":"oklch(0.35 0.01 60)","kind":"color","definedIn":"labs/labs.css"},{"name":"--ink-faint","value":"oklch(0.55 0.01 60)","kind":"color","definedIn":"labs/labs.css"},{"name":"--ink-whisper","value":"oklch(0.72 0.01 60)","kind":"color","definedIn":"labs/labs.css"},{"name":"--rule","value":"oklch(0.88 0.01 60)","kind":"color","definedIn":"labs/labs.css"},{"name":"--rule-deep","value":"oklch(0.78 0.01 60)","kind":"color","definedIn":"labs/labs.css"},{"name":"--live","value":"oklch(0.55 0.13 145)","kind":"color","definedIn":"labs/labs.css"},{"name":"--waning","value":"oklch(0.64 0.14 65)","kind":"color","definedIn":"labs/labs.css"},{"name":"--voided","value":"oklch(0.5 0.16 25)","kind":"color","definedIn":"labs/labs.css"},{"name":"--teach","value":"oklch(0.48 0.1 280)","kind":"color","definedIn":"labs/labs.css"},{"name":"--ease-read","value":"cubic-bezier(0.2, 0, 0, 1)","kind":"other","definedIn":"labs/labs.css","annotation":"other"},{"name":"--ease-settle","value":"cubic-bezier(0.34, 1.2, 0.64, 1)","kind":"other","definedIn":"labs/labs.css","annotation":"other"},{"name":"--paper","value":"oklch(0.14 0.012 255)","kind":"color","definedIn":"labs/labs.css","scope":":root[data-view=\"night\"]"},{"name":"--paper-warm","value":"oklch(0.17 0.015 255)","kind":"color","definedIn":"labs/labs.css","scope":":root[data-view=\"night\"]"},{"name":"--ink","value":"oklch(0.94 0.008 85)","kind":"color","definedIn":"labs/labs.css","scope":":root[data-view=\"night\"]"},{"name":"--ink-soft","value":"oklch(0.78 0.008 85)","kind":"color","definedIn":"labs/labs.css","scope":":root[data-view=\"night\"]"},{"name":"--ink-faint","value":"oklch(0.58 0.008 85)","kind":"color","definedIn":"labs/labs.css","scope":":root[data-view=\"night\"]"},{"name":"--ink-whisper","value":"oklch(0.38 0.008 85)","kind":"color","definedIn":"labs/labs.css","scope":":root[data-view=\"night\"]"},{"name":"--rule","value":"oklch(0.26 0.012 255)","kind":"color","definedIn":"labs/labs.css","scope":":root[data-view=\"night\"]"},{"name":"--rule-deep","value":"oklch(0.34 0.012 255)","kind":"color","definedIn":"labs/labs.css","scope":":root[data-view=\"night\"]"},{"name":"--human","value":"oklch(0.72 0.12 45)","kind":"color","definedIn":"labs/labs.css","scope":":root[data-view=\"night\"]"},{"name":"--human-wash","value":"oklch(0.72 0.12 45 / 0.1)","kind":"color","definedIn":"labs/labs.css","scope":":root[data-view=\"night\"]"},{"name":"--human-glow","value":"oklch(0.82 0.15 45)","kind":"color","definedIn":"labs/labs.css","scope":":root[data-view=\"night\"]"},{"name":"--protocol","value":"oklch(0.74 0.16 253)","kind":"color","definedIn":"labs/labs.css","scope":":root[data-view=\"night\"]"},{"name":"--protocol-wash","value":"oklch(0.74 0.16 253 / 0.1)","kind":"color","definedIn":"labs/labs.css","scope":":root[data-view=\"night\"]"},{"name":"--protocol-glow","value":"oklch(0.85 0.18 253)","kind":"color","definedIn":"labs/labs.css","scope":":root[data-view=\"night\"]"},{"name":"--live","value":"oklch(0.72 0.15 145)","kind":"color","definedIn":"labs/labs.css","scope":":root[data-view=\"night\"]"}],"themes":[{"selector":"html[data-theme=\"dark\"])","label":"Dark )"},{"selector":":root[data-view=\"night\"]","label":"Root View Night"}],"fonts":[],"brandFonts":[{"family":"Geist","status":"ok","tokens":["--font-sans"],"path":"labs/labs.css"},{"family":"JetBrains Mono","status":"ok","tokens":["--font-mono"],"path":"labs/labs.css"},{"family":"Fraunces","status":"ok","tokens":["--font-serif"],"path":"labs/labs.css"}],"source":"spa"} \ No newline at end of file +{ + "namespace": "PDPPDesignSystem_b4e65a", + "components": [], + "startingPoints": [], + "cards": [ + { + "path": "preview/brand-logo.html", + "group": "Brand", + "viewport": "700x230", + "subtitle": "Mark, wordmark, and the carbon signature", + "name": "Brand" + }, + { + "path": "preview/iconography.html", + "group": "Brand", + "viewport": "700x200", + "subtitle": "The system draws no pictures — status is typed", + "name": "Glyphs, not icons" + }, + { + "path": "preview/colors-primary.html", + "group": "Colors", + "viewport": "700x170", + "subtitle": "Paper, ink, protocol ink, human warmth", + "name": "Colors · Core" + }, + { + "path": "preview/colors-semantic.html", + "group": "Colors", + "viewport": "700x170", + "subtitle": "Recorded, expiring, destructive — each spent on one meaning", + "name": "Colors · State" + }, + { + "path": "preview/colors-washes.html", + "group": "Colors", + "viewport": "700x230", + "subtitle": "Where hue is allowed — and where it is not", + "name": "Spent color" + }, + { + "path": "preview/components-badges.html", + "group": "Components", + "viewport": "700x190", + "subtitle": "Status, typed by the protocol — and neutral tags", + "name": "Endorsements" + }, + { + "path": "preview/components-buttons.html", + "group": "Components", + "viewport": "700x150", + "subtitle": "Ink, human act, ghost, destructive", + "name": "Buttons" + }, + { + "path": "preview/components-cards.html", + "group": "Components", + "viewport": "700x260", + "subtitle": "The paper artifact — and the duplicate the server keeps", + "name": "Sheets & carbon" + }, + { + "path": "preview/components-code.html", + "group": "Components", + "viewport": "700x230", + "subtitle": "Protocol payloads — the typed voice at full width", + "name": "Code" + }, + { + "path": "preview/components-consent-row.html", + "group": "Components", + "viewport": "700x230", + "subtitle": "Scope rows — granted typed, declined struck", + "name": "Consent scopes" + }, + { + "path": "preview/components-dataflow.html", + "group": "Components", + "viewport": "700x270", + "subtitle": "The aligned list — shared column axis, mandatory headers", + "name": "Table" + }, + { + "path": "preview/components-grant-card.html", + "group": "Components", + "viewport": "700x330", + "subtitle": "The full sheet — kv block, endorsement, actions", + "name": "Grant record" + }, + { + "path": "preview/components-inputs.html", + "group": "Components", + "viewport": "700x200", + "subtitle": "Fields — mono values, grotesk labels", + "name": "Inputs" + }, + { + "path": "preview/layout-grid.html", + "group": "Spacing", + "viewport": "700x220", + "subtitle": "Content widths, sidebar, stat band", + "name": "Layout" + }, + { + "path": "preview/radii.html", + "group": "Spacing", + "viewport": "700x190", + "subtitle": "Paper is square; controls get a 2px ease", + "name": "Corners" + }, + { + "path": "preview/shadows.html", + "group": "Spacing", + "viewport": "700x230", + "subtitle": "Nothing floats — retention casts carbon instead of shadow", + "name": "Elevation" + }, + { + "path": "preview/spacing-scale.html", + "group": "Spacing", + "viewport": "700x170", + "subtitle": "4px grid — compose padding and gap from these steps", + "name": "Spacing" + }, + { + "path": "preview/type-body.html", + "group": "Type", + "viewport": "700x220", + "subtitle": "Body, title, label, caption", + "name": "Type · Body" + }, + { + "path": "preview/type-display.html", + "group": "Type", + "viewport": "700x230", + "subtitle": "Schibsted Grotesk — the human voice, set heavy", + "name": "Type · Display" + }, + { + "path": "preview/type-mono.html", + "group": "Type", + "viewport": "700x230", + "subtitle": "JetBrains Mono — everything the protocol writes", + "name": "Type · Typed voice" + } + ], + "templates": [], + "globalCssPaths": [ + "colors_and_type.css", + "ui_kits/web/kit.css", + "explorer/explorer.css", + "labs/labs.css", + "styles.css" + ], + "tokens": [ + { + "name": "--font-sans", + "value": "\"Geist\", ui-sans-serif, system-ui, sans-serif", + "kind": "font", + "definedIn": "labs/labs.css" + }, + { + "name": "--font-mono", + "value": "\"JetBrains Mono\", ui-monospace, monospace", + "kind": "font", + "definedIn": "labs/labs.css" + }, + { + "name": "--background", + "value": "oklch(0.985 0.004 90)", + "kind": "color", + "definedIn": "colors_and_type.css" + }, + { + "name": "--foreground", + "value": "oklch(0.18 0.005 270)", + "kind": "color", + "definedIn": "colors_and_type.css" + }, + { + "name": "--card", + "value": "oklch(1 0 0)", + "kind": "color", + "definedIn": "colors_and_type.css" + }, + { + "name": "--card-foreground", + "value": "oklch(0.18 0.005 270)", + "kind": "color", + "definedIn": "colors_and_type.css" + }, + { + "name": "--popover", + "value": "oklch(1 0 0)", + "kind": "color", + "definedIn": "colors_and_type.css" + }, + { + "name": "--popover-foreground", + "value": "oklch(0.18 0.005 270)", + "kind": "color", + "definedIn": "colors_and_type.css" + }, + { + "name": "--primary", + "value": "oklch(0.46 0.11 255)", + "kind": "color", + "definedIn": "colors_and_type.css" + }, + { + "name": "--primary-foreground", + "value": "oklch(0.99 0.002 90)", + "kind": "color", + "definedIn": "colors_and_type.css" + }, + { + "name": "--primary-tint", + "value": "oklch(0.46 0.11 255 / 0.08)", + "kind": "color", + "definedIn": "colors_and_type.css" + }, + { + "name": "--human", + "value": "oklch(0.52 0.11 45)", + "kind": "color", + "definedIn": "labs/labs.css" + }, + { + "name": "--human-foreground", + "value": "oklch(0.99 0.005 90)", + "kind": "color", + "definedIn": "colors_and_type.css" + }, + { + "name": "--human-tint", + "value": "oklch(0.55 0.11 45 / 0.08)", + "kind": "color", + "definedIn": "colors_and_type.css" + }, + { + "name": "--carbon-outline", + "value": "oklch(0.46 0.11 255 / 0.40)", + "kind": "color", + "definedIn": "colors_and_type.css" + }, + { + "name": "--carbon-fill", + "value": "oklch(0.46 0.11 255 / 0.09)", + "kind": "color", + "definedIn": "colors_and_type.css" + }, + { + "name": "--carbon-offset", + "value": "9px", + "kind": "spacing", + "definedIn": "colors_and_type.css" + }, + { + "name": "--secondary", + "value": "oklch(0.955 0.004 270)", + "kind": "color", + "definedIn": "colors_and_type.css" + }, + { + "name": "--secondary-foreground", + "value": "oklch(0.22 0.005 270)", + "kind": "color", + "definedIn": "colors_and_type.css" + }, + { + "name": "--muted", + "value": "oklch(0.955 0.004 270)", + "kind": "color", + "definedIn": "colors_and_type.css" + }, + { + "name": "--muted-foreground", + "value": "oklch(0.47 0.008 270)", + "kind": "color", + "definedIn": "colors_and_type.css" + }, + { + "name": "--accent", + "value": "oklch(0.955 0.004 270)", + "kind": "color", + "definedIn": "colors_and_type.css" + }, + { + "name": "--accent-foreground", + "value": "oklch(0.22 0.005 270)", + "kind": "color", + "definedIn": "colors_and_type.css" + }, + { + "name": "--border", + "value": "oklch(0.905 0.005 270)", + "kind": "color", + "definedIn": "colors_and_type.css" + }, + { + "name": "--border-strong", + "value": "oklch(0.79 0.006 270)", + "kind": "color", + "definedIn": "colors_and_type.css" + }, + { + "name": "--input", + "value": "oklch(0.79 0.006 270)", + "kind": "color", + "definedIn": "colors_and_type.css" + }, + { + "name": "--ring", + "value": "oklch(0.46 0.11 255)", + "kind": "color", + "definedIn": "colors_and_type.css" + }, + { + "name": "--success", + "value": "oklch(0.50 0.11 158)", + "kind": "color", + "definedIn": "colors_and_type.css" + }, + { + "name": "--success-tint", + "value": "oklch(0.50 0.11 158 / 0.10)", + "kind": "color", + "definedIn": "colors_and_type.css" + }, + { + "name": "--warning", + "value": "oklch(0.58 0.13 70)", + "kind": "color", + "definedIn": "colors_and_type.css" + }, + { + "name": "--warning-tint", + "value": "oklch(0.58 0.13 70 / 0.10)", + "kind": "color", + "definedIn": "colors_and_type.css" + }, + { + "name": "--destructive", + "value": "oklch(0.52 0.16 27)", + "kind": "color", + "definedIn": "colors_and_type.css" + }, + { + "name": "--destructive-foreground", + "value": "oklch(0.99 0.002 90)", + "kind": "color", + "definedIn": "colors_and_type.css" + }, + { + "name": "--destructive-tint", + "value": "oklch(0.52 0.16 27 / 0.08)", + "kind": "color", + "definedIn": "colors_and_type.css" + }, + { + "name": "--primary-wash", + "value": "var(--primary-tint)", + "kind": "color", + "definedIn": "colors_and_type.css" + }, + { + "name": "--human-wash", + "value": "oklch(0.52 0.11 45 / 0.08)", + "kind": "color", + "definedIn": "labs/labs.css" + }, + { + "name": "--success-wash", + "value": "var(--success-tint)", + "kind": "color", + "definedIn": "colors_and_type.css" + }, + { + "name": "--success-wash-strong", + "value": "oklch(0.50 0.11 158 / 0.16)", + "kind": "color", + "definedIn": "colors_and_type.css" + }, + { + "name": "--warning-wash", + "value": "var(--warning-tint)", + "kind": "color", + "definedIn": "colors_and_type.css" + }, + { + "name": "--verified-wash", + "value": "var(--success-tint)", + "kind": "color", + "definedIn": "colors_and_type.css" + }, + { + "name": "--edu-fg", + "value": "oklch(0.50 0.06 210)", + "kind": "color", + "definedIn": "colors_and_type.css" + }, + { + "name": "--radius", + "value": "0px", + "kind": "radius", + "definedIn": "colors_and_type.css" + }, + { + "name": "--radius-control", + "value": "2px", + "kind": "radius", + "definedIn": "colors_and_type.css" + }, + { + "name": "--duration-instant", + "value": "0ms", + "kind": "other", + "definedIn": "colors_and_type.css", + "annotation": "other" + }, + { + "name": "--duration-fast", + "value": "0.01ms", + "kind": "other", + "definedIn": "colors_and_type.css", + "annotation": "other" + }, + { + "name": "--duration-base", + "value": "0.01ms", + "kind": "other", + "definedIn": "colors_and_type.css", + "annotation": "other" + }, + { + "name": "--duration-moderate", + "value": "0.01ms", + "kind": "other", + "definedIn": "colors_and_type.css", + "annotation": "other" + }, + { + "name": "--duration-slow", + "value": "0.01ms", + "kind": "other", + "definedIn": "colors_and_type.css", + "annotation": "other" + }, + { + "name": "--ease-enter", + "value": "cubic-bezier(0, 0, 0.2, 1)", + "kind": "other", + "definedIn": "colors_and_type.css", + "annotation": "other" + }, + { + "name": "--ease-exit", + "value": "cubic-bezier(0.4, 0, 1, 1)", + "kind": "other", + "definedIn": "colors_and_type.css", + "annotation": "other" + }, + { + "name": "--ease-standard", + "value": "cubic-bezier(0.4, 0, 0.2, 1)", + "kind": "other", + "definedIn": "colors_and_type.css", + "annotation": "other" + }, + { + "name": "--motion-enter", + "value": "var(--duration-moderate) var(--ease-enter)", + "kind": "other", + "definedIn": "colors_and_type.css", + "annotation": "other" + }, + { + "name": "--motion-exit", + "value": "var(--duration-fast) var(--ease-exit)", + "kind": "other", + "definedIn": "colors_and_type.css", + "annotation": "other" + }, + { + "name": "--motion-state", + "value": "var(--duration-base) var(--ease-standard)", + "kind": "other", + "definedIn": "colors_and_type.css", + "annotation": "other" + }, + { + "name": "--stagger-fast", + "value": "30ms", + "kind": "other", + "definedIn": "colors_and_type.css", + "annotation": "other" + }, + { + "name": "--stagger-base", + "value": "50ms", + "kind": "other", + "definedIn": "colors_and_type.css", + "annotation": "other" + }, + { + "name": "--content-width", + "value": "760px", + "kind": "spacing", + "definedIn": "colors_and_type.css" + }, + { + "name": "--content-wide-width", + "value": "1200px", + "kind": "spacing", + "definedIn": "colors_and_type.css" + }, + { + "name": "--chrome-height", + "value": "3.5rem", + "kind": "spacing", + "definedIn": "colors_and_type.css" + }, + { + "name": "--pdpp-sidebar-width", + "value": "280px", + "kind": "spacing", + "definedIn": "colors_and_type.css" + }, + { + "name": "--background", + "value": "oklch(0.17 0.006 262)", + "kind": "color", + "definedIn": "colors_and_type.css", + "scope": "html[data-theme=\"dark\"])" + }, + { + "name": "--foreground", + "value": "oklch(0.95 0.005 262)", + "kind": "color", + "definedIn": "colors_and_type.css", + "scope": "html[data-theme=\"dark\"])" + }, + { + "name": "--card", + "value": "oklch(0.21 0.007 262)", + "kind": "color", + "definedIn": "colors_and_type.css", + "scope": "html[data-theme=\"dark\"])" + }, + { + "name": "--card-foreground", + "value": "oklch(0.95 0.005 262)", + "kind": "color", + "definedIn": "colors_and_type.css", + "scope": "html[data-theme=\"dark\"])" + }, + { + "name": "--popover", + "value": "oklch(0.24 0.007 262)", + "kind": "color", + "definedIn": "colors_and_type.css", + "scope": "html[data-theme=\"dark\"])" + }, + { + "name": "--popover-foreground", + "value": "oklch(0.95 0.005 262)", + "kind": "color", + "definedIn": "colors_and_type.css", + "scope": "html[data-theme=\"dark\"])" + }, + { + "name": "--primary", + "value": "oklch(0.74 0.13 255)", + "kind": "color", + "definedIn": "colors_and_type.css", + "scope": "html[data-theme=\"dark\"])" + }, + { + "name": "--primary-foreground", + "value": "oklch(0.15 0.01 262)", + "kind": "color", + "definedIn": "colors_and_type.css", + "scope": "html[data-theme=\"dark\"])" + }, + { + "name": "--primary-tint", + "value": "oklch(0.74 0.13 255 / 0.13)", + "kind": "color", + "definedIn": "colors_and_type.css", + "scope": "html[data-theme=\"dark\"])" + }, + { + "name": "--human", + "value": "oklch(0.76 0.12 45)", + "kind": "color", + "definedIn": "colors_and_type.css", + "scope": "html[data-theme=\"dark\"])" + }, + { + "name": "--human-foreground", + "value": "oklch(0.16 0.01 45)", + "kind": "color", + "definedIn": "colors_and_type.css", + "scope": "html[data-theme=\"dark\"])" + }, + { + "name": "--human-tint", + "value": "oklch(0.76 0.12 45 / 0.11)", + "kind": "color", + "definedIn": "colors_and_type.css", + "scope": "html[data-theme=\"dark\"])" + }, + { + "name": "--carbon-outline", + "value": "oklch(0.74 0.13 255 / 0.62)", + "kind": "color", + "definedIn": "colors_and_type.css", + "scope": "html[data-theme=\"dark\"])" + }, + { + "name": "--carbon-fill", + "value": "oklch(0.74 0.13 255 / 0.22)", + "kind": "color", + "definedIn": "colors_and_type.css", + "scope": "html[data-theme=\"dark\"])" + }, + { + "name": "--secondary", + "value": "oklch(0.26 0.007 262)", + "kind": "color", + "definedIn": "colors_and_type.css", + "scope": "html[data-theme=\"dark\"])" + }, + { + "name": "--secondary-foreground", + "value": "oklch(0.95 0.005 262)", + "kind": "color", + "definedIn": "colors_and_type.css", + "scope": "html[data-theme=\"dark\"])" + }, + { + "name": "--muted", + "value": "oklch(0.24 0.007 262)", + "kind": "color", + "definedIn": "colors_and_type.css", + "scope": "html[data-theme=\"dark\"])" + }, + { + "name": "--muted-foreground", + "value": "oklch(0.72 0.01 262)", + "kind": "color", + "definedIn": "colors_and_type.css", + "scope": "html[data-theme=\"dark\"])" + }, + { + "name": "--accent", + "value": "oklch(0.26 0.007 262)", + "kind": "color", + "definedIn": "colors_and_type.css", + "scope": "html[data-theme=\"dark\"])" + }, + { + "name": "--accent-foreground", + "value": "oklch(0.95 0.005 262)", + "kind": "color", + "definedIn": "colors_and_type.css", + "scope": "html[data-theme=\"dark\"])" + }, + { + "name": "--border", + "value": "oklch(0.29 0.008 262)", + "kind": "color", + "definedIn": "colors_and_type.css", + "scope": "html[data-theme=\"dark\"])" + }, + { + "name": "--border-strong", + "value": "oklch(0.40 0.008 262)", + "kind": "color", + "definedIn": "colors_and_type.css", + "scope": "html[data-theme=\"dark\"])" + }, + { + "name": "--input", + "value": "oklch(0.34 0.008 262)", + "kind": "color", + "definedIn": "colors_and_type.css", + "scope": "html[data-theme=\"dark\"])" + }, + { + "name": "--ring", + "value": "oklch(0.74 0.13 255)", + "kind": "color", + "definedIn": "colors_and_type.css", + "scope": "html[data-theme=\"dark\"])" + }, + { + "name": "--success", + "value": "oklch(0.76 0.13 158)", + "kind": "color", + "definedIn": "colors_and_type.css", + "scope": "html[data-theme=\"dark\"])" + }, + { + "name": "--success-tint", + "value": "oklch(0.76 0.13 158 / 0.14)", + "kind": "color", + "definedIn": "colors_and_type.css", + "scope": "html[data-theme=\"dark\"])" + }, + { + "name": "--warning", + "value": "oklch(0.80 0.14 75)", + "kind": "color", + "definedIn": "colors_and_type.css", + "scope": "html[data-theme=\"dark\"])" + }, + { + "name": "--warning-tint", + "value": "oklch(0.80 0.14 75 / 0.13)", + "kind": "color", + "definedIn": "colors_and_type.css", + "scope": "html[data-theme=\"dark\"])" + }, + { + "name": "--destructive", + "value": "oklch(0.70 0.16 27)", + "kind": "color", + "definedIn": "colors_and_type.css", + "scope": "html[data-theme=\"dark\"])" + }, + { + "name": "--destructive-foreground", + "value": "oklch(0.15 0.01 262)", + "kind": "color", + "definedIn": "colors_and_type.css", + "scope": "html[data-theme=\"dark\"])" + }, + { + "name": "--destructive-tint", + "value": "oklch(0.70 0.16 27 / 0.12)", + "kind": "color", + "definedIn": "colors_and_type.css", + "scope": "html[data-theme=\"dark\"])" + }, + { + "name": "--primary-wash", + "value": "var(--primary-tint)", + "kind": "color", + "definedIn": "colors_and_type.css", + "scope": "html[data-theme=\"dark\"])" + }, + { + "name": "--human-wash", + "value": "var(--human-tint)", + "kind": "color", + "definedIn": "colors_and_type.css", + "scope": "html[data-theme=\"dark\"])" + }, + { + "name": "--success-wash", + "value": "var(--success-tint)", + "kind": "color", + "definedIn": "colors_and_type.css", + "scope": "html[data-theme=\"dark\"])" + }, + { + "name": "--success-wash-strong", + "value": "oklch(0.76 0.13 158 / 0.20)", + "kind": "color", + "definedIn": "colors_and_type.css", + "scope": "html[data-theme=\"dark\"])" + }, + { + "name": "--warning-wash", + "value": "var(--warning-tint)", + "kind": "color", + "definedIn": "colors_and_type.css", + "scope": "html[data-theme=\"dark\"])" + }, + { + "name": "--verified-wash", + "value": "var(--success-tint)", + "kind": "color", + "definedIn": "colors_and_type.css", + "scope": "html[data-theme=\"dark\"])" + }, + { + "name": "--edu-fg", + "value": "oklch(0.78 0.06 210)", + "kind": "color", + "definedIn": "colors_and_type.css", + "scope": "html[data-theme=\"dark\"])" + }, + { + "name": "--font-serif", + "value": "\"Fraunces\", Georgia, serif", + "kind": "font", + "definedIn": "labs/labs.css" + }, + { + "name": "--human-deep", + "value": "oklch(0.38 0.09 40)", + "kind": "color", + "definedIn": "labs/labs.css" + }, + { + "name": "--human-glow", + "value": "oklch(0.68 0.14 45)", + "kind": "color", + "definedIn": "labs/labs.css" + }, + { + "name": "--protocol", + "value": "oklch(0.58 0.18 253)", + "kind": "color", + "definedIn": "labs/labs.css" + }, + { + "name": "--protocol-deep", + "value": "oklch(0.42 0.14 253)", + "kind": "color", + "definedIn": "labs/labs.css" + }, + { + "name": "--protocol-wash", + "value": "oklch(0.58 0.18 253 / 0.08)", + "kind": "color", + "definedIn": "labs/labs.css" + }, + { + "name": "--protocol-glow", + "value": "oklch(0.72 0.16 253)", + "kind": "color", + "definedIn": "labs/labs.css" + }, + { + "name": "--thermal", + "value": "linear-gradient(90deg, var(--human) 0%, oklch(0.55 0.1 180) 50%, var(--protocol) 100%)", + "kind": "color", + "definedIn": "labs/labs.css" + }, + { + "name": "--paper", + "value": "oklch(0.985 0.005 85)", + "kind": "color", + "definedIn": "labs/labs.css" + }, + { + "name": "--paper-warm", + "value": "oklch(0.98 0.008 75)", + "kind": "color", + "definedIn": "labs/labs.css" + }, + { + "name": "--ink", + "value": "oklch(0.16 0.01 60)", + "kind": "color", + "definedIn": "labs/labs.css" + }, + { + "name": "--ink-soft", + "value": "oklch(0.35 0.01 60)", + "kind": "color", + "definedIn": "labs/labs.css" + }, + { + "name": "--ink-faint", + "value": "oklch(0.55 0.01 60)", + "kind": "color", + "definedIn": "labs/labs.css" + }, + { + "name": "--ink-whisper", + "value": "oklch(0.72 0.01 60)", + "kind": "color", + "definedIn": "labs/labs.css" + }, + { + "name": "--rule", + "value": "oklch(0.88 0.01 60)", + "kind": "color", + "definedIn": "labs/labs.css" + }, + { + "name": "--rule-deep", + "value": "oklch(0.78 0.01 60)", + "kind": "color", + "definedIn": "labs/labs.css" + }, + { + "name": "--live", + "value": "oklch(0.55 0.13 145)", + "kind": "color", + "definedIn": "labs/labs.css" + }, + { + "name": "--waning", + "value": "oklch(0.64 0.14 65)", + "kind": "color", + "definedIn": "labs/labs.css" + }, + { + "name": "--voided", + "value": "oklch(0.5 0.16 25)", + "kind": "color", + "definedIn": "labs/labs.css" + }, + { + "name": "--teach", + "value": "oklch(0.48 0.1 280)", + "kind": "color", + "definedIn": "labs/labs.css" + }, + { + "name": "--ease-read", + "value": "cubic-bezier(0.2, 0, 0, 1)", + "kind": "other", + "definedIn": "labs/labs.css", + "annotation": "other" + }, + { + "name": "--ease-settle", + "value": "cubic-bezier(0.34, 1.2, 0.64, 1)", + "kind": "other", + "definedIn": "labs/labs.css", + "annotation": "other" + }, + { + "name": "--paper", + "value": "oklch(0.14 0.012 255)", + "kind": "color", + "definedIn": "labs/labs.css", + "scope": ":root[data-view=\"night\"]" + }, + { + "name": "--paper-warm", + "value": "oklch(0.17 0.015 255)", + "kind": "color", + "definedIn": "labs/labs.css", + "scope": ":root[data-view=\"night\"]" + }, + { + "name": "--ink", + "value": "oklch(0.94 0.008 85)", + "kind": "color", + "definedIn": "labs/labs.css", + "scope": ":root[data-view=\"night\"]" + }, + { + "name": "--ink-soft", + "value": "oklch(0.78 0.008 85)", + "kind": "color", + "definedIn": "labs/labs.css", + "scope": ":root[data-view=\"night\"]" + }, + { + "name": "--ink-faint", + "value": "oklch(0.58 0.008 85)", + "kind": "color", + "definedIn": "labs/labs.css", + "scope": ":root[data-view=\"night\"]" + }, + { + "name": "--ink-whisper", + "value": "oklch(0.38 0.008 85)", + "kind": "color", + "definedIn": "labs/labs.css", + "scope": ":root[data-view=\"night\"]" + }, + { + "name": "--rule", + "value": "oklch(0.26 0.012 255)", + "kind": "color", + "definedIn": "labs/labs.css", + "scope": ":root[data-view=\"night\"]" + }, + { + "name": "--rule-deep", + "value": "oklch(0.34 0.012 255)", + "kind": "color", + "definedIn": "labs/labs.css", + "scope": ":root[data-view=\"night\"]" + }, + { + "name": "--human", + "value": "oklch(0.72 0.12 45)", + "kind": "color", + "definedIn": "labs/labs.css", + "scope": ":root[data-view=\"night\"]" + }, + { + "name": "--human-wash", + "value": "oklch(0.72 0.12 45 / 0.1)", + "kind": "color", + "definedIn": "labs/labs.css", + "scope": ":root[data-view=\"night\"]" + }, + { + "name": "--human-glow", + "value": "oklch(0.82 0.15 45)", + "kind": "color", + "definedIn": "labs/labs.css", + "scope": ":root[data-view=\"night\"]" + }, + { + "name": "--protocol", + "value": "oklch(0.74 0.16 253)", + "kind": "color", + "definedIn": "labs/labs.css", + "scope": ":root[data-view=\"night\"]" + }, + { + "name": "--protocol-wash", + "value": "oklch(0.74 0.16 253 / 0.1)", + "kind": "color", + "definedIn": "labs/labs.css", + "scope": ":root[data-view=\"night\"]" + }, + { + "name": "--protocol-glow", + "value": "oklch(0.85 0.18 253)", + "kind": "color", + "definedIn": "labs/labs.css", + "scope": ":root[data-view=\"night\"]" + }, + { + "name": "--live", + "value": "oklch(0.72 0.15 145)", + "kind": "color", + "definedIn": "labs/labs.css", + "scope": ":root[data-view=\"night\"]" + } + ], + "themes": [ + { "selector": "html[data-theme=\"dark\"])", "label": "Dark )" }, + { "selector": ":root[data-view=\"night\"]", "label": "Root View Night" } + ], + "fonts": [], + "brandFonts": [ + { + "family": "Geist", + "status": "ok", + "tokens": ["--font-sans"], + "path": "labs/labs.css" + }, + { + "family": "JetBrains Mono", + "status": "ok", + "tokens": ["--font-mono"], + "path": "labs/labs.css" + }, + { + "family": "Fraunces", + "status": "ok", + "tokens": ["--font-serif"], + "path": "labs/labs.css" + } + ], + "source": "spa" +} diff --git a/docs/design-system/ink-carbon/project/assets/logo-mark.svg b/docs/design-system/ink-carbon/project/assets/logo-mark.svg index 58b24ae7f..9d14f92d2 100644 --- a/docs/design-system/ink-carbon/project/assets/logo-mark.svg +++ b/docs/design-system/ink-carbon/project/assets/logo-mark.svg @@ -1,4 +1,15 @@ - P - \ No newline at end of file + + P + + diff --git a/docs/design-system/ink-carbon/project/assets/wordmark.svg b/docs/design-system/ink-carbon/project/assets/wordmark.svg index c7f57a3b6..1f8e6d794 100644 --- a/docs/design-system/ink-carbon/project/assets/wordmark.svg +++ b/docs/design-system/ink-carbon/project/assets/wordmark.svg @@ -1,6 +1,35 @@ - P - PDPP - v0.1.0 - \ No newline at end of file + + P + + + PDPP + + + v0.1.0 + + diff --git a/docs/design-system/ink-carbon/project/colors_and_type.css b/docs/design-system/ink-carbon/project/colors_and_type.css index 485af2d46..841f926db 100644 --- a/docs/design-system/ink-carbon/project/colors_and_type.css +++ b/docs/design-system/ink-carbon/project/colors_and_type.css @@ -16,7 +16,7 @@ · paper geometry: sheets are square; only controls get a 2px ease ═══════════════════════════════════════════════════════════════════════════ */ -@import url('https://fonts.googleapis.com/css2?family=Schibsted+Grotesk:ital,wght@0,400..900;1,400..700&family=JetBrains+Mono:ital,wght@0,400;0,500;0,600;1,400&display=swap'); +@import url("https://fonts.googleapis.com/css2?family=Schibsted+Grotesk:ital,wght@0,400..900;1,400..700&family=JetBrains+Mono:ital,wght@0,400;0,500;0,600;1,400&display=swap"); :root { /* ─── Fonts — the two voices ─────────────────────────────────── */ @@ -42,7 +42,7 @@ --human-tint: oklch(0.55 0.11 45 / 0.08); /* ─── Carbon — the duplicate the server keeps ────────────────── */ - --carbon-outline: oklch(0.46 0.11 255 / 0.40); + --carbon-outline: oklch(0.46 0.11 255 / 0.4); --carbon-fill: oklch(0.46 0.11 255 / 0.09); --carbon-offset: 9px; @@ -59,11 +59,11 @@ --ring: oklch(0.46 0.11 255); /* ─── State (spent, never sprayed) ──────────────────────────── */ - --success: oklch(0.50 0.11 158); /* recorded / active */ - --success-tint: oklch(0.50 0.11 158 / 0.10); - --warning: oklch(0.58 0.13 70); /* expiring */ - --warning-tint: oklch(0.58 0.13 70 / 0.10); - --destructive: oklch(0.52 0.16 27); /* destructive acts only */ + --success: oklch(0.5 0.11 158); /* recorded / active */ + --success-tint: oklch(0.5 0.11 158 / 0.1); + --warning: oklch(0.58 0.13 70); /* expiring */ + --warning-tint: oklch(0.58 0.13 70 / 0.1); + --destructive: oklch(0.52 0.16 27); /* destructive acts only */ --destructive-foreground: oklch(0.99 0.002 90); --destructive-tint: oklch(0.52 0.16 27 / 0.08); @@ -71,10 +71,10 @@ --primary-wash: var(--primary-tint); --human-wash: var(--human-tint); --success-wash: var(--success-tint); - --success-wash-strong: oklch(0.50 0.11 158 / 0.16); + --success-wash-strong: oklch(0.5 0.11 158 / 0.16); --warning-wash: var(--warning-tint); --verified-wash: var(--success-tint); - --edu-fg: oklch(0.50 0.06 210); + --edu-fg: oklch(0.5 0.06 210); /* ─── Geometry — paper is square, controls get a 2px ease ───── */ --radius: 0px; @@ -103,7 +103,9 @@ } @media (min-width: 1440px) { - :root { --pdpp-sidebar-width: 280px; } + :root { + --pdpp-sidebar-width: 280px; + } } @media (prefers-reduced-motion: reduce) { @@ -143,22 +145,22 @@ --accent: oklch(0.26 0.007 262); --accent-foreground: oklch(0.95 0.005 262); --border: oklch(0.29 0.008 262); - --border-strong: oklch(0.40 0.008 262); + --border-strong: oklch(0.4 0.008 262); --input: oklch(0.34 0.008 262); --ring: oklch(0.74 0.13 255); --success: oklch(0.76 0.13 158); --success-tint: oklch(0.76 0.13 158 / 0.14); - --warning: oklch(0.80 0.14 75); - --warning-tint: oklch(0.80 0.14 75 / 0.13); - --destructive: oklch(0.70 0.16 27); + --warning: oklch(0.8 0.14 75); + --warning-tint: oklch(0.8 0.14 75 / 0.13); + --destructive: oklch(0.7 0.16 27); --destructive-foreground: oklch(0.15 0.01 262); - --destructive-tint: oklch(0.70 0.16 27 / 0.12); + --destructive-tint: oklch(0.7 0.16 27 / 0.12); --primary-wash: var(--primary-tint); --human-wash: var(--human-tint); --success-wash: var(--success-tint); - --success-wash-strong: oklch(0.76 0.13 158 / 0.20); + --success-wash-strong: oklch(0.76 0.13 158 / 0.2); --warning-wash: var(--warning-tint); --verified-wash: var(--success-tint); --edu-fg: oklch(0.78 0.06 210); @@ -185,19 +187,44 @@ body { color: var(--foreground); } -*, *::before, *::after { box-sizing: border-box; } +*, +*::before, +*::after { + box-sizing: border-box; +} -button, [role="button"] { cursor: pointer; } +button, +[role="button"] { + cursor: pointer; +} -a { color: inherit; } +a { + color: inherit; +} -code, pre, kbd, samp { font-family: var(--font-mono); } +code, +pre, +kbd, +samp { + font-family: var(--font-mono); +} -::-webkit-scrollbar { width: 5px; height: 5px; } -::-webkit-scrollbar-track { background: transparent; } -::-webkit-scrollbar-thumb { background: oklch(0 0 0 / 0.14); } -::-webkit-scrollbar-thumb:hover { background: oklch(0 0 0 / 0.22); } -:is(html.dark, html[data-theme="dark"]) ::-webkit-scrollbar-thumb { background: oklch(1 0 0 / 0.14); } +::-webkit-scrollbar { + width: 5px; + height: 5px; +} +::-webkit-scrollbar-track { + background: transparent; +} +::-webkit-scrollbar-thumb { + background: oklch(0 0 0 / 0.14); +} +::-webkit-scrollbar-thumb:hover { + background: oklch(0 0 0 / 0.22); +} +:is(html.dark, html[data-theme="dark"]) ::-webkit-scrollbar-thumb { + background: oklch(1 0 0 / 0.14); +} /* ─── Surfaces — flat tints, no accent stripe, no gradient ──────── The temperature system survives, rendered honestly: a protocol-authored @@ -217,7 +244,11 @@ code, pre, kbd, samp { font-family: var(--font-mono); } [data-surface="stage"] { background-color: var(--muted); - background-image: radial-gradient(circle, oklch(0 0 0 / 0.06) 1px, transparent 1px); + background-image: radial-gradient( + circle, + oklch(0 0 0 / 0.06) 1px, + transparent 1px + ); background-size: 20px 20px; } @@ -227,7 +258,9 @@ code, pre, kbd, samp { font-family: var(--font-mono); } the owner's behalf: a staged consent request, a held grant, an export in flight. Never decoration; if nothing is retained, no carbon. */ -.pdpp-carbon { position: relative; } +.pdpp-carbon { + position: relative; +} .pdpp-carbon::before { content: ""; position: absolute; @@ -237,13 +270,15 @@ code, pre, kbd, samp { font-family: var(--font-mono); } border: 1px solid var(--carbon-outline); pointer-events: none; } -.pdpp-carbon > * { position: relative; } +.pdpp-carbon > * { + position: relative; +} /* ─── Type scale ──────────────────────────────────────────────── */ .pdpp-display-lg { font-family: var(--font-sans); - font-size: 3.75rem; /* 60px */ + font-size: 3.75rem; /* 60px */ font-weight: 700; line-height: 1.04; letter-spacing: -0.028em; @@ -251,7 +286,7 @@ code, pre, kbd, samp { font-family: var(--font-mono); } .pdpp-display { font-family: var(--font-sans); - font-size: 2.5rem; /* 40px */ + font-size: 2.5rem; /* 40px */ font-weight: 700; line-height: 1.08; letter-spacing: -0.022em; @@ -259,7 +294,7 @@ code, pre, kbd, samp { font-family: var(--font-mono); } .pdpp-heading { font-family: var(--font-sans); - font-size: 1.25rem; /* 20px */ + font-size: 1.25rem; /* 20px */ font-weight: 700; line-height: 1.3; letter-spacing: -0.012em; @@ -267,35 +302,35 @@ code, pre, kbd, samp { font-family: var(--font-mono); } .pdpp-title { font-family: var(--font-sans); - font-size: 0.875rem; /* 14px */ + font-size: 0.875rem; /* 14px */ font-weight: 600; line-height: 1.4; } .pdpp-body-lg { font-family: var(--font-sans); - font-size: 1.125rem; /* 18px */ + font-size: 1.125rem; /* 18px */ font-weight: 400; line-height: 1.6; } .pdpp-body { font-family: var(--font-sans); - font-size: 0.875rem; /* 14px */ + font-size: 0.875rem; /* 14px */ font-weight: 400; line-height: 1.6; } .pdpp-label { font-family: var(--font-sans); - font-size: 0.75rem; /* 12px */ + font-size: 0.75rem; /* 12px */ font-weight: 500; line-height: 1.4; } .pdpp-caption { font-family: var(--font-sans); - font-size: 0.75rem; /* 12px */ + font-size: 0.75rem; /* 12px */ font-weight: 400; line-height: 1.5; } @@ -333,9 +368,26 @@ code, pre, kbd, samp { font-family: var(--font-mono); } border-bottom: 1px solid var(--border); } -@keyframes spin { to { transform: rotate(360deg); } } -@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } } +@keyframes spin { + to { + transform: rotate(360deg); + } +} +@keyframes fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} @keyframes slide-up { - from { opacity: 0; transform: translateY(6px); } - to { opacity: 1; transform: translateY(0); } + from { + opacity: 0; + transform: translateY(6px); + } + to { + opacity: 1; + transform: translateY(0); + } } diff --git a/docs/design-system/ink-carbon/project/explorer/app.jsx b/docs/design-system/ink-carbon/project/explorer/app.jsx index 05686611b..74c568958 100644 --- a/docs/design-system/ink-carbon/project/explorer/app.jsx +++ b/docs/design-system/ink-carbon/project/explorer/app.jsx @@ -9,403 +9,417 @@ *
*/ -;(() => { +(() => { + const { useState, useEffect, useMemo, useCallback } = React; + const { runQuery, computeFacets } = window.PDPP_QUERY; + const { detect, pickInitial, VIEW_ORDER } = window.PDPP_DISPATCH; + const { fmtRelative, CAP_GLYPH, CAP_LABEL, NOW } = window.PDPPPrim; -const { useState, useEffect, useMemo, useCallback } = React; -const { runQuery, computeFacets } = window.PDPP_QUERY; -const { detect, pickInitial, VIEW_ORDER } = window.PDPP_DISPATCH; -const { fmtRelative, CAP_GLYPH, CAP_LABEL, NOW } = window.PDPPPrim; + const APP_DEFAULTS = /*EDITMODE-BEGIN*/ { + projectionDefault: false, + defaultMode: "lex", + } /*EDITMODE-END*/; -const APP_DEFAULTS = /*EDITMODE-BEGIN*/{ - "projectionDefault": false, - "defaultMode": "lex" -}/*EDITMODE-END*/; + function App() { + const { grant, connections, streams } = window.PDPP_DATA; + const { TweaksPanel, useTweaks, TweakSection, TweakToggle, TweakRadio } = window; + const [tweaks, setTweak] = useTweaks(APP_DEFAULTS); -function App() { - const { grant, connections, streams } = window.PDPP_DATA; - const { TweaksPanel, useTweaks, TweakSection, TweakToggle, TweakRadio } = window; - const [tweaks, setTweak] = useTweaks(APP_DEFAULTS); + const [query, setQuery] = useState({ chips: [], text: "" }); + const [mode, setMode] = useState(tweaks.defaultMode); + const [queryFocused, setQueryFocused] = useState(false); + const [selected, setSelected] = useState(null); // { stream, record } + const [projection, setProjection] = useState(tweaks.projectionDefault); + const [forcedView, setForcedView] = useState(null); + const [whyOpen, setWhyOpen] = useState(false); - const [query, setQuery] = useState({ chips: [], text: "" }); - const [mode, setMode] = useState(tweaks.defaultMode); - const [queryFocused, setQueryFocused] = useState(false); - const [selected, setSelected] = useState(null); // { stream, record } - const [projection, setProjection] = useState(tweaks.projectionDefault); - const [forcedView, setForcedView] = useState(null); - const [whyOpen, setWhyOpen] = useState(false); - - // ── Hotkeys ────────────────────────────────────────────────────── - useEffect(() => { - function onKey(e) { - const tag = (e.target.tagName ?? "").toLowerCase(); - const inField = tag === "input" || tag === "textarea"; - if (!inField && e.key === "/" && !e.metaKey && !e.ctrlKey) { - e.preventDefault(); - document.querySelector(".exp-query__text")?.focus(); - } - if (e.key === "Escape") { - if (selected) { setSelected(null); return; } + // ── Hotkeys ────────────────────────────────────────────────────── + useEffect(() => { + function onKey(e) { + const tag = (e.target.tagName ?? "").toLowerCase(); + const inField = tag === "input" || tag === "textarea"; + if (!inField && e.key === "/" && !e.metaKey && !e.ctrlKey) { + e.preventDefault(); + document.querySelector(".exp-query__text")?.focus(); + } + if (e.key === "Escape") { + if (selected) { + setSelected(null); + return; + } + } } - } - window.addEventListener("keydown", onKey); - return () => window.removeEventListener("keydown", onKey); - }, [selected]); + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }, [selected]); - // ── Hits + dispatch ───────────────────────────────────────────── - const hits = useMemo(() => runQuery(query, streams), [query, streams]); - const facets = useMemo(() => computeFacets(hits), [hits]); + // ── Hits + dispatch ───────────────────────────────────────────── + const hits = useMemo(() => runQuery(query, streams), [query, streams]); + const facets = useMemo(() => computeFacets(hits), [hits]); - // Determine whether the result set is single-stream (route to dispatched view) - // or multi-stream (route to unified feed). - const uniqueStreams = useMemo(() => { - const s = new Map(); - for (const h of hits) s.set(`${h.stream.connection_id}::${h.stream.name}`, h.stream); - return [...s.values()]; - }, [hits]); + // Determine whether the result set is single-stream (route to dispatched view) + // or multi-stream (route to unified feed). + const uniqueStreams = useMemo(() => { + const s = new Map(); + for (const h of hits) s.set(`${h.stream.connection_id}::${h.stream.name}`, h.stream); + return [...s.values()]; + }, [hits]); - const singleStream = uniqueStreams.length === 1 ? uniqueStreams[0] : null; + const singleStream = uniqueStreams.length === 1 ? uniqueStreams[0] : null; - // Capability dispatch for the single-stream case - const dispatchResult = useMemo(() => singleStream ? detect(singleStream) : null, [singleStream]); - const initialView = useMemo(() => singleStream ? pickInitial(singleStream) : "feed", [singleStream]); + // Capability dispatch for the single-stream case + const dispatchResult = useMemo(() => (singleStream ? detect(singleStream) : null), [singleStream]); + const initialView = useMemo(() => (singleStream ? pickInitial(singleStream) : "feed"), [singleStream]); - // Reset forcedView when the result-set shape changes - useEffect(() => { setForcedView(null); }, [singleStream?.connection_id, singleStream?.name]); + // Reset forcedView when the result-set shape changes + useEffect(() => { + setForcedView(null); + }, [singleStream?.connection_id, singleStream?.name]); - const activeView = forcedView ?? (singleStream ? initialView : "feed"); + const activeView = forcedView ?? (singleStream ? initialView : "feed"); - // Capabilities visible in the view switcher. - // For multi-stream, only "feed" is available; for single-stream, dispatch + table fallback. - const availableViews = singleStream ? dispatchResult.capabilities : ["feed"]; - - // ── Handlers ───────────────────────────────────────────────────── - function openRecord(stream, record) { setSelected({ stream, record }); } - function clearQuery() { setQuery({ chips: [], text: "" }); } - function addChip(chip) { - setQuery((q) => { - // de-dup - const chips = q.chips.filter((c) => !(c.field === chip.field && JSON.stringify(c.value) === JSON.stringify(chip.value))); - return { ...q, chips: [...chips, chip] }; - }); - } + // Capabilities visible in the view switcher. + // For multi-stream, only "feed" is available; for single-stream, dispatch + table fallback. + const availableViews = singleStream ? dispatchResult.capabilities : ["feed"]; - function bodyForView() { - if (activeView === "feed") { - const isEmpty = query.chips.length === 0 && !query.text.trim(); - return ( - - ); + // ── Handlers ───────────────────────────────────────────────────── + function openRecord(stream, record) { + setSelected({ stream, record }); } - // Single-stream: filter the underlying stream records to those in our hits - // (the query result), then mount the capability view over that subset. - const hitIds = new Set(hits.map((h) => h.record.id)); - const filteredStream = { - ...singleStream, - records: singleStream.records.filter((r) => hitIds.has(r.id)), - }; - const viewProps = { - stream: filteredStream, - selectedId: selected?.record?.id, - onSelect: (r) => openRecord(filteredStream, r), - projection, - }; - switch (activeView) { - case "table": return ; - case "timeline": return ; - case "conversation": return ; - case "ledger": return ; - case "gallery": return ; - case "map": return ; - case "calendar": return ; - case "chart": return ; - case "reader": return ; - default: return ; + function clearQuery() { + setQuery({ chips: [], text: "" }); + } + function addChip(chip) { + setQuery((q) => { + // de-dup + const chips = q.chips.filter( + (c) => !(c.field === chip.field && JSON.stringify(c.value) === JSON.stringify(chip.value)) + ); + return { ...q, chips: [...chips, chip] }; + }); } - } - return ( - <> -
- {/* ── Top bar ── */} -
-
- pdpp - pdpp -
-
- -
-
- setWhyOpen((x) => !x)} - value={activeView} - whyOpen={whyOpen} - /> - setProjection((x) => !x)} - projection={projection} - /> -
-
+ function bodyForView() { + if (activeView === "feed") { + const isEmpty = query.chips.length === 0 && !query.text.trim(); + return ( + + ); + } + // Single-stream: filter the underlying stream records to those in our hits + // (the query result), then mount the capability view over that subset. + const hitIds = new Set(hits.map((h) => h.record.id)); + const filteredStream = { + ...singleStream, + records: singleStream.records.filter((r) => hitIds.has(r.id)), + }; + const viewProps = { + stream: filteredStream, + selectedId: selected?.record?.id, + onSelect: (r) => openRecord(filteredStream, r), + projection, + }; + switch (activeView) { + case "table": + return ; + case "timeline": + return ; + case "conversation": + return ; + case "ledger": + return ; + case "gallery": + return ; + case "map": + return ; + case "calendar": + return ; + case "chart": + return ; + case "reader": + return ; + default: + return ; + } + } + + return ( + <> +
+ {/* ── Top bar ── */} +
+
+ pdpp + pdpp +
+
+ +
+
+ setWhyOpen((x) => !x)} + value={activeView} + whyOpen={whyOpen} + /> + setProjection((x) => !x)} projection={projection} /> +
+
- {/* ── Facets ── */} - + {/* ── Facets ── */} + - {/* ── Main canvas ── */} -
-
- {bodyForView()} -
-
+ {/* ── Main canvas ── */} +
+
{bodyForView()}
+
- {/* ── Peek ── */} - {selected ? ( - setSelected(null)} - projection={projection} - record={selected.record} - stream={selected.stream} - /> - ) : null} + {/* ── Peek ── */} + {selected ? ( + setSelected(null)} + projection={projection} + record={selected.record} + stream={selected.stream} + /> + ) : null} - {/* ── Why-this-view popover ── */} - {whyOpen && dispatchResult ? ( -
e.stopPropagation()}> -
Why these views?
-
- Lit up from the stream's typed schema fields. Connector identity is irrelevant. -
- {VIEW_ORDER.map((cap) => ( -
- - {dispatchResult.capabilities.includes(cap) ? "✓ " : " "} - {CAP_LABEL[cap]} - - - {dispatchResult.capabilities.includes(cap) - ? (dispatchResult.signals[cap] ?? []).join(" · ") || "—" - : "no signal"} - + {/* ── Why-this-view popover ── */} + {whyOpen && dispatchResult ? ( +
e.stopPropagation()}> +
Why these views?
+
+ Lit up from the stream's typed schema fields. Connector identity is irrelevant.
- ))} -
- ) : null} -
+ {VIEW_ORDER.map((cap) => ( +
+ + {dispatchResult.capabilities.includes(cap) ? "✓ " : " "} + {CAP_LABEL[cap]} + + + {dispatchResult.capabilities.includes(cap) + ? (dispatchResult.signals[cap] ?? []).join(" · ") || "—" + : "no signal"} + +
+ ))} +
+ ) : null} +
- - - { setTweak("defaultMode", v); setMode(v); }} - options={[ - { value: "lex", label: "Lexical" }, - { value: "sem", label: "Semantic" }, - { value: "hyb", label: "Hybrid" }, - ]} - value={tweaks.defaultMode} - /> - - - { setTweak("projectionDefault", v); setProjection(v); }} - value={tweaks.projectionDefault} - /> - - - - ); -} + + + { + setTweak("defaultMode", v); + setMode(v); + }} + options={[ + { value: "lex", label: "Lexical" }, + { value: "sem", label: "Semantic" }, + { value: "hyb", label: "Hybrid" }, + ]} + value={tweaks.defaultMode} + /> + + + { + setTweak("projectionDefault", v); + setProjection(v); + }} + value={tweaks.projectionDefault} + /> + + + + ); + } -/* ─── ViewSwitcher ─────────────────────────────────────────────────── */ + /* ─── ViewSwitcher ─────────────────────────────────────────────────── */ -function ViewSwitcher({ available, value, onChange, isFeed, dispatchResult, onWhy, whyOpen }) { - // When the result set spans multiple streams, the only option is "feed" - // — there's nothing useful to switch to, so hide the chrome entirely. - // The switcher only appears once the user has narrowed to one stream - // and the stream's capability views can light up. - if (isFeed) return null; - return ( -
-
- {window.PDPP_DISPATCH.VIEW_ORDER.map((cap) => { - if (!available.includes(cap)) return null; - return ( - - ); - })} + function ViewSwitcher({ available, value, onChange, isFeed, dispatchResult, onWhy, whyOpen }) { + // When the result set spans multiple streams, the only option is "feed" + // — there's nothing useful to switch to, so hide the chrome entirely. + // The switcher only appears once the user has narrowed to one stream + // and the stream's capability views can light up. + if (isFeed) return null; + return ( +
+
+ {window.PDPP_DISPATCH.VIEW_ORDER.map((cap) => { + if (!available.includes(cap)) return null; + return ( + + ); + })} +
+
+ ); + } + + /* ─── GrantChip (top bar) ─────────────────────────────────────────── */ + + function GrantChip({ grant, projection, onToggleProjection }) { + const days = Math.ceil((new Date(grant.expires_at).getTime() - NOW) / 86_400_000); + return ( -
- ); -} - -/* ─── GrantChip (top bar) ─────────────────────────────────────────── */ - -function GrantChip({ grant, projection, onToggleProjection }) { - const days = Math.ceil((new Date(grant.expires_at).getTime() - NOW) / 86_400_000); - return ( - - ); -} + ); + } -/* ─── FacetLine ───────────────────────────────────────────────────── */ + /* ─── FacetLine ───────────────────────────────────────────────────── */ -function FacetLine({ hits, facets, query, onAddChip, activeView }) { - const totalCount = hits.length; - const streamCount = facets.streams.length; - const ppl = facets.people.length; - const months = facets.months.length; + function FacetLine({ hits, facets, query, onAddChip, activeView }) { + const totalCount = hits.length; + const streamCount = facets.streams.length; + const ppl = facets.people.length; + const months = facets.months.length; - return ( -
- - {totalCount === 0 ? "no results" - : totalCount === 1 ? "1 record" - : `${totalCount.toLocaleString()} records`} - - {totalCount > 0 ? ( - <> - · - - spans {months === 1 ? "1 month" : `${months} months`} - - {streamCount > 0 ? ( - <> - · - - {facets.streams.slice(0, 4).map(([s, c], i) => ( - - ))} - {streamCount > 4 ? +{streamCount - 4} more : null} - - - ) : null} - {ppl > 0 ? ( - <> - · - - {facets.people.slice(0, 4).map(([p, c]) => { - const first = window.PDPP_QUERY.firstNameToken(p); - if (!first) return null; - return ( + return ( +
+ + {totalCount === 0 ? "no results" : totalCount === 1 ? "1 record" : `${totalCount.toLocaleString()} records`} + + {totalCount > 0 ? ( + <> + · + spans {months === 1 ? "1 month" : `${months} months`} + {streamCount > 0 ? ( + <> + · + + {facets.streams.slice(0, 4).map(([s, c], i) => ( - ); - })} - - - ) : null} - {facets.categories.length > 0 ? ( - <> - · - - {facets.categories.slice(0, 3).map(([cat, c]) => ( - - ))} - - - ) : null} - - ) : null} - - {query.chips.length > 0 || query.text ? ( - - - - ) : null} -
- ); -} - -const root = ReactDOM.createRoot(document.getElementById("app")); -root.render(); + ))} + {streamCount > 4 ? +{streamCount - 4} more : null} +
+ + ) : null} + {ppl > 0 ? ( + <> + · + + {facets.people.slice(0, 4).map(([p, c]) => { + const first = window.PDPP_QUERY.firstNameToken(p); + if (!first) return null; + return ( + + ); + })} + + + ) : null} + {facets.categories.length > 0 ? ( + <> + · + + {facets.categories.slice(0, 3).map(([cat, c]) => ( + + ))} + + + ) : null} + + ) : null} + + {query.chips.length > 0 || query.text ? ( + + + + ) : null} +
+ ); + } + const root = ReactDOM.createRoot(document.getElementById("app")); + root.render(); })(); diff --git a/docs/design-system/ink-carbon/project/explorer/assets/logo-mark.svg b/docs/design-system/ink-carbon/project/explorer/assets/logo-mark.svg index 58b24ae7f..9d14f92d2 100644 --- a/docs/design-system/ink-carbon/project/explorer/assets/logo-mark.svg +++ b/docs/design-system/ink-carbon/project/explorer/assets/logo-mark.svg @@ -1,4 +1,15 @@ - P - \ No newline at end of file + + P + + diff --git a/docs/design-system/ink-carbon/project/explorer/colors_and_type.css b/docs/design-system/ink-carbon/project/explorer/colors_and_type.css index bd58ab14b..6c2d22992 100644 --- a/docs/design-system/ink-carbon/project/explorer/colors_and_type.css +++ b/docs/design-system/ink-carbon/project/explorer/colors_and_type.css @@ -11,13 +11,14 @@ /* Webfonts — served from the Fontsource CDN (same source the repo uses). If you need to self-host, replace with local @font-face pointing at /fonts/geist-variable.woff2 and /fonts/jetbrains-mono-variable.woff2. */ -@import url('https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&display=swap'); -@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap'); +@import url("https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&display=swap"); +@import url("https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap"); :root { /* ─── Fonts ──────────────────────────────────────────────────── */ --font-sans: "Geist", "Geist Variable", ui-sans-serif, system-ui, sans-serif; - --font-mono: "JetBrains Mono", "JetBrains Mono Variable", ui-monospace, monospace; + --font-mono: + "JetBrains Mono", "JetBrains Mono Variable", ui-monospace, monospace; /* ─── Core surface + text (oklch) ────────────────────────────── */ --background: oklch(0.99 0.002 95); @@ -28,20 +29,20 @@ --popover-foreground: oklch(0.13 0 0); /* ─── Protocol blue (primary) ───────────────────────────────── */ - --primary: oklch(0.580 0.172 253.7); /* ≈ #187adc */ + --primary: oklch(0.58 0.172 253.7); /* ≈ #187adc */ --primary-foreground: oklch(0.99 0 0); - --primary-wash: oklch(0.580 0.172 253.7 / 0.06); + --primary-wash: oklch(0.58 0.172 253.7 / 0.06); /* ─── Neutrals ──────────────────────────────────────────────── */ --secondary: oklch(0.96 0 0); - --secondary-foreground: oklch(0.20 0 0); + --secondary-foreground: oklch(0.2 0 0); --muted: oklch(0.96 0 0); - --muted-foreground: oklch(0.50 0 0); + --muted-foreground: oklch(0.5 0 0); --accent: oklch(0.96 0 0); - --accent-foreground: oklch(0.20 0 0); + --accent-foreground: oklch(0.2 0 0); --border: oklch(0.94 0 0); --input: oklch(0.91 0 0); - --ring: oklch(0.580 0.172 253.7); + --ring: oklch(0.58 0.172 253.7); /* ─── Copper / Human (the "warm" axis of the duality) ───────── */ --human: oklch(0.52 0.09 45); @@ -53,7 +54,7 @@ --success-wash-strong: oklch(0.52 0.15 150 / 0.15); --warning: oklch(0.62 0.15 70); --warning-wash: oklch(0.62 0.15 70 / 0.1); - --destructive: oklch(0.55 0.20 27); + --destructive: oklch(0.55 0.2 27); --destructive-foreground: oklch(0.99 0 0); --verified-wash: oklch(0.52 0.15 150 / 0.1); @@ -88,7 +89,9 @@ } @media (min-width: 1440px) { - :root { --pdpp-sidebar-width: 280px; } + :root { + --pdpp-sidebar-width: 280px; + } } @media (prefers-reduced-motion: reduce) { @@ -114,26 +117,51 @@ html { body { margin: 0; background: - radial-gradient(circle at top left, oklch(0.52 0.09 45 / 0.06), transparent 28rem), + radial-gradient( + circle at top left, + oklch(0.52 0.09 45 / 0.06), + transparent 28rem + ), linear-gradient(180deg, oklch(1 0 0) 0%, var(--background) 14rem); color: var(--foreground); } -*, *::before, *::after { box-sizing: border-box; } +*, +*::before, +*::after { + box-sizing: border-box; +} -button, [role="button"] { cursor: pointer; } +button, +[role="button"] { + cursor: pointer; +} -a { color: inherit; } +a { + color: inherit; +} -code, pre, kbd, samp { font-family: var(--font-mono); } +code, +pre, +kbd, +samp { + font-family: var(--font-mono); +} -::-webkit-scrollbar { width: 5px; height: 5px; } -::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar { + width: 5px; + height: 5px; +} +::-webkit-scrollbar-track { + background: transparent; +} ::-webkit-scrollbar-thumb { background: oklch(0 0 0 / 0.12); border-radius: 9999px; } -::-webkit-scrollbar-thumb:hover { background: oklch(0 0 0 / 0.20); } +::-webkit-scrollbar-thumb:hover { + background: oklch(0 0 0 / 0.2); +} /* ─── Surfaces (the temperature system) ──────────────────────── */ @@ -142,9 +170,15 @@ code, pre, kbd, samp { font-family: var(--font-mono); } border-right: 1px solid var(--border); border-bottom: 1px solid var(--border); border-left: 2px solid var(--human); - background-image: linear-gradient(to bottom, var(--human-wash), transparent 35%); + background-image: linear-gradient( + to bottom, + var(--human-wash), + transparent 35% + ); background-color: var(--card); - box-shadow: 0 1px 2px rgb(0 0 0 / 0.04), 0 1px 3px rgb(0 0 0 / 0.02); + box-shadow: + 0 1px 2px rgb(0 0 0 / 0.04), + 0 1px 3px rgb(0 0 0 / 0.02); } [data-surface="protocol"] { @@ -152,14 +186,24 @@ code, pre, kbd, samp { font-family: var(--font-mono); } border-right: 1px solid var(--border); border-bottom: 1px solid var(--border); border-left: 2px solid var(--primary); - background-image: linear-gradient(to right, oklch(0.580 0.172 253.7 / 0.04), transparent 70%); + background-image: linear-gradient( + to right, + oklch(0.58 0.172 253.7 / 0.04), + transparent 70% + ); background-color: var(--card); - box-shadow: 0 1px 2px rgb(0 0 0 / 0.04), 0 1px 3px rgb(0 0 0 / 0.02); + box-shadow: + 0 1px 2px rgb(0 0 0 / 0.04), + 0 1px 3px rgb(0 0 0 / 0.02); } [data-surface="stage"] { background-color: var(--muted); - background-image: radial-gradient(circle, oklch(0 0 0 / 0.06) 1px, transparent 1px); + background-image: radial-gradient( + circle, + oklch(0 0 0 / 0.06) 1px, + transparent 1px + ); background-size: 20px 20px; } @@ -167,7 +211,7 @@ code, pre, kbd, samp { font-family: var(--font-mono); } .pdpp-display-lg { font-family: var(--font-sans); - font-size: 3.75rem; /* 60px */ + font-size: 3.75rem; /* 60px */ font-weight: 600; line-height: 1.05; letter-spacing: -0.03em; @@ -175,7 +219,7 @@ code, pre, kbd, samp { font-family: var(--font-mono); } .pdpp-display { font-family: var(--font-sans); - font-size: 2.5rem; /* 40px */ + font-size: 2.5rem; /* 40px */ font-weight: 600; line-height: 1.08; letter-spacing: -0.025em; @@ -183,7 +227,7 @@ code, pre, kbd, samp { font-family: var(--font-mono); } .pdpp-heading { font-family: var(--font-sans); - font-size: 1.25rem; /* 20px */ + font-size: 1.25rem; /* 20px */ font-weight: 600; line-height: 1.3; letter-spacing: -0.01em; @@ -191,35 +235,35 @@ code, pre, kbd, samp { font-family: var(--font-mono); } .pdpp-title { font-family: var(--font-sans); - font-size: 0.875rem; /* 14px */ + font-size: 0.875rem; /* 14px */ font-weight: 600; line-height: 1.4; } .pdpp-body-lg { font-family: var(--font-sans); - font-size: 1.125rem; /* 18px */ + font-size: 1.125rem; /* 18px */ font-weight: 400; line-height: 1.625; } .pdpp-body { font-family: var(--font-sans); - font-size: 0.875rem; /* 14px */ + font-size: 0.875rem; /* 14px */ font-weight: 400; line-height: 1.6; } .pdpp-label { font-family: var(--font-sans); - font-size: 0.75rem; /* 12px */ + font-size: 0.75rem; /* 12px */ font-weight: 500; line-height: 1.4; } .pdpp-caption { font-family: var(--font-sans); - font-size: 0.75rem; /* 12px */ + font-size: 0.75rem; /* 12px */ font-weight: 400; line-height: 1.5; } @@ -239,9 +283,26 @@ code, pre, kbd, samp { font-family: var(--font-mono); } border-bottom: 1px solid var(--border); } -@keyframes spin { to { transform: rotate(360deg); } } -@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } } +@keyframes spin { + to { + transform: rotate(360deg); + } +} +@keyframes fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} @keyframes slide-up { - from { opacity: 0; transform: translateY(6px); } - to { opacity: 1; transform: translateY(0); } + from { + opacity: 0; + transform: translateY(6px); + } + to { + opacity: 1; + transform: translateY(0); + } } diff --git a/docs/design-system/ink-carbon/project/explorer/data.js b/docs/design-system/ink-carbon/project/explorer/data.js index fa3f55ca4..17cd59e32 100644 --- a/docs/design-system/ink-carbon/project/explorer/data.js +++ b/docs/design-system/ink-carbon/project/explorer/data.js @@ -19,7 +19,8 @@ const HOUR = 3600_000; const MIN = 60_000; const ago = (ms) => new Date(NOW - ms).toISOString(); const at = (d) => new Date(d).toISOString(); -let _rid = 0; const rid = (prefix) => `${prefix}_${(++_rid).toString(36).padStart(5, "0")}`; +let _rid = 0; +const rid = (prefix) => `${prefix}_${(++_rid).toString(36).padStart(5, "0")}`; // ─── grant ────────────────────────────────────────────────────────────── const grant = { @@ -33,9 +34,15 @@ const grant = { scope: "single_use_extended", access_mode: "continuous", granted_streams: [ - "gmail/messages", "slack/messages", "chase/transactions", - "amazon/orders", "github/events", "strava/activities", - "photos/media", "oura/sleep", "calendar/events", + "gmail/messages", + "slack/messages", + "chase/transactions", + "amazon/orders", + "github/events", + "strava/activities", + "photos/media", + "oura/sleep", + "calendar/events", ], granted_field_count: 47, total_field_count: 112, @@ -43,16 +50,46 @@ const grant = { // ─── connections ──────────────────────────────────────────────────────── const connections = [ - { id: "c_gmail_p", connector_id: "gmail", display_name: "the owner@example.com", group: "Google", account_kind: "personal" }, - { id: "c_gmail_w", connector_id: "gmail", display_name: "the owner@example.org", group: "Google", account_kind: "work" }, - { id: "c_photos", connector_id: "google_takeout", display_name: "the owner@example.com", group: "Google", account_kind: "personal" }, - { id: "c_slack", connector_id: "slack", display_name: "acme.slack.com", group: "Slack", account_kind: "work" }, - { id: "c_chase", connector_id: "chase", display_name: "Chase •6432", group: "Chase", account_kind: "personal" }, - { id: "c_amazon", connector_id: "amazon", display_name: "the owner@example.com", group: "Amazon", account_kind: "personal" }, - { id: "c_github", connector_id: "github", display_name: "owner", group: "GitHub", account_kind: "personal" }, - { id: "c_strava", connector_id: "strava", display_name: "the owner Rivera", group: "Strava", account_kind: "personal" }, - { id: "c_oura", connector_id: "oura", display_name: "Ring Gen3 · silver", group: "Oura", account_kind: "personal" }, - { id: "c_ical", connector_id: "ical", display_name: "personal.ics", group: "Calendar", account_kind: "personal" }, + { + id: "c_gmail_p", + connector_id: "gmail", + display_name: "the owner@example.com", + group: "Google", + account_kind: "personal", + }, + { + id: "c_gmail_w", + connector_id: "gmail", + display_name: "the owner@example.org", + group: "Google", + account_kind: "work", + }, + { + id: "c_photos", + connector_id: "google_takeout", + display_name: "the owner@example.com", + group: "Google", + account_kind: "personal", + }, + { id: "c_slack", connector_id: "slack", display_name: "acme.slack.com", group: "Slack", account_kind: "work" }, + { id: "c_chase", connector_id: "chase", display_name: "Chase •6432", group: "Chase", account_kind: "personal" }, + { + id: "c_amazon", + connector_id: "amazon", + display_name: "the owner@example.com", + group: "Amazon", + account_kind: "personal", + }, + { id: "c_github", connector_id: "github", display_name: "owner", group: "GitHub", account_kind: "personal" }, + { + id: "c_strava", + connector_id: "strava", + display_name: "the owner Rivera", + group: "Strava", + account_kind: "personal", + }, + { id: "c_oura", connector_id: "oura", display_name: "Ring Gen3 · silver", group: "Oura", account_kind: "personal" }, + { id: "c_ical", connector_id: "ical", display_name: "personal.ics", group: "Calendar", account_kind: "personal" }, ]; // ─── streams: schema + records ────────────────────────────────────────── @@ -61,267 +98,1229 @@ const connections = [ // GMAIL ───────────────────────────────────────────────────────────────── const gmailFields = [ - { name: "id", type: "id", granted: true }, - { name: "thread_id", type: "id", granted: true }, - { name: "from", type: "person", granted: true }, - { name: "to", type: "person[]", granted: true }, - { name: "subject", type: "text", granted: true }, - { name: "snippet", type: "text", granted: true }, - { name: "body", type: "text", granted: false, redacted_reason: "client requested only headers + snippet" }, - { name: "date", type: "timestamp", granted: true }, - { name: "labels", type: "enum[]", granted: true }, + { name: "id", type: "id", granted: true }, + { name: "thread_id", type: "id", granted: true }, + { name: "from", type: "person", granted: true }, + { name: "to", type: "person[]", granted: true }, + { name: "subject", type: "text", granted: true }, + { name: "snippet", type: "text", granted: true }, + { name: "body", type: "text", granted: false, redacted_reason: "client requested only headers + snippet" }, + { name: "date", type: "timestamp", granted: true }, + { name: "labels", type: "enum[]", granted: true }, { name: "has_attachment", type: "boolean", granted: true }, ]; const gmailViews = ["all", "unread", "this_week", "starred"]; const gmailPersonalRecords = [ - { id: rid("gm"), thread_id: "t01", from: "Maya Chen ", to: ["the owner@example.com"], subject: "re: portfolio review thursday", snippet: "Thursday 2pm still works on my end. I'll bring the redlines from the consent flow we talked about — curious what you think of the copper rule on…", date: ago(2 * HOUR), labels: ["Inbox", "Starred"], has_attachment: false, body: "Thursday 2pm still works on my end. I'll bring the redlines from the consent flow we talked about — curious what you think of the copper rule on the grant inspector. Also: I think I figured out the weird thing with the right-pane scrim, I'll show you in person.\n\nMaya" }, - { id: rid("gm"), thread_id: "t02", from: "Southwest Airlines ", to: ["the owner@example.com"], subject: "Your trip to Austin is confirmed — May 31", snippet: "Confirmation L4M2QP · the owner Rivera · SFO → AUS · Sat May 31 · 6:35a · Wanna Get Away+", date: ago(8 * HOUR), labels: ["Inbox", "Travel"], has_attachment: true }, - { id: rid("gm"), thread_id: "t03", from: "Anthropic ", to: ["the owner@example.com"], subject: "Your monthly receipt", snippet: "Receipt for May 2026 — Claude Pro $20.00 charged to Visa ending in 6432. Thanks for being a subscriber.", date: ago(1 * DAY + 4 * HOUR), labels: ["Inbox", "Receipts"], has_attachment: true }, - { id: rid("gm"), thread_id: "t04", from: "mom ", to: ["the owner@example.com"], subject: "the chickens", snippet: "Hattie laid a green egg today!! It's the size of a quarter. Sending pics. Dad says hi and is mad about the gutters again.", date: ago(2 * DAY), labels: ["Inbox"], has_attachment: true }, - { id: rid("gm"), thread_id: "t05", from: "Tara Patel ", to: ["the owner@example.com"], subject: "Longview client integration — grant ready?", snippet: "Hi the owner — we got our scoped grant approved this morning. Wanted to flag: we're noticing some streams come back with `connection_id` populated and some without…", date: ago(3 * DAY + 6 * HOUR), labels: ["Inbox"], has_attachment: false }, - { id: rid("gm"), thread_id: "t06", from: "Strava ", to: ["the owner@example.com"], subject: "Your weekly summary: 31.4 mi", snippet: "Nice work this week. 3 runs · 31.4 mi · 4h 48m · 1,247 ft elevation. New all-time best for May.", date: ago(4 * DAY), labels: ["Inbox", "Updates"], has_attachment: false }, - { id: rid("gm"), thread_id: "t07", from: "Chase Online ", to: ["the owner@example.com"], subject: "Alert: large purchase on •6432", snippet: "A $487.14 purchase at Apple Store was made on May 19. If this was you, no action needed. Otherwise reply STOP or call us.", date: ago(5 * DAY), labels: ["Inbox", "Banking"], has_attachment: false }, - { id: rid("gm"), thread_id: "t08", from: "GitHub ", to: ["the owner@example.com"], subject: "[vana-com/pdpp] PR #847 was merged: capability dispatch in explorer", snippet: "Merged by @maya-chen · 12 files changed · +482 −137", date: ago(6 * DAY), labels: ["Inbox", "Code"], has_attachment: false }, - { id: rid("gm"), thread_id: "t09", from: "Spotify ", to: ["the owner@example.com"], subject: "Your minutes this month: 1,847", snippet: "You listened more than 87% of users in San Francisco this month. Top artist: Caroline Polachek. Top genre: art pop.", date: ago(8 * DAY), labels: ["Inbox"], has_attachment: false }, - { id: rid("gm"), thread_id: "t10", from: "Maya Chen ", to: ["the owner@example.com"], subject: "stair brewery sat?", snippet: "they have the saison on tap again. 4ish? bring the dog", date: ago(12 * DAY), labels: ["Inbox", "Starred"], has_attachment: false }, - { id: rid("gm"), thread_id: "t11", from: "Costco Wholesale ", to: ["the owner@example.com"], subject: "Your Costco order is ready for pickup", snippet: "Order #C-58291 · Pickup window: May 14, 4–6pm · 8 items · Total: $213.47", date: ago(14 * DAY), labels: ["Inbox", "Receipts"], has_attachment: false }, - { id: rid("gm"), thread_id: "t12", from: "ACLU ", to: ["the owner@example.com"], subject: "the owner — your May donation receipt", snippet: "Thank you for your monthly $25 contribution. Tax receipt attached. Every dollar funds the fight.", date: ago(18 * DAY), labels: ["Inbox", "Receipts"], has_attachment: true }, + { + id: rid("gm"), + thread_id: "t01", + from: "Maya Chen ", + to: ["the owner@example.com"], + subject: "re: portfolio review thursday", + snippet: + "Thursday 2pm still works on my end. I'll bring the redlines from the consent flow we talked about — curious what you think of the copper rule on…", + date: ago(2 * HOUR), + labels: ["Inbox", "Starred"], + has_attachment: false, + body: "Thursday 2pm still works on my end. I'll bring the redlines from the consent flow we talked about — curious what you think of the copper rule on the grant inspector. Also: I think I figured out the weird thing with the right-pane scrim, I'll show you in person.\n\nMaya", + }, + { + id: rid("gm"), + thread_id: "t02", + from: "Southwest Airlines ", + to: ["the owner@example.com"], + subject: "Your trip to Austin is confirmed — May 31", + snippet: "Confirmation L4M2QP · the owner Rivera · SFO → AUS · Sat May 31 · 6:35a · Wanna Get Away+", + date: ago(8 * HOUR), + labels: ["Inbox", "Travel"], + has_attachment: true, + }, + { + id: rid("gm"), + thread_id: "t03", + from: "Anthropic ", + to: ["the owner@example.com"], + subject: "Your monthly receipt", + snippet: "Receipt for May 2026 — Claude Pro $20.00 charged to Visa ending in 6432. Thanks for being a subscriber.", + date: ago(1 * DAY + 4 * HOUR), + labels: ["Inbox", "Receipts"], + has_attachment: true, + }, + { + id: rid("gm"), + thread_id: "t04", + from: "mom ", + to: ["the owner@example.com"], + subject: "the chickens", + snippet: + "Hattie laid a green egg today!! It's the size of a quarter. Sending pics. Dad says hi and is mad about the gutters again.", + date: ago(2 * DAY), + labels: ["Inbox"], + has_attachment: true, + }, + { + id: rid("gm"), + thread_id: "t05", + from: "Tara Patel ", + to: ["the owner@example.com"], + subject: "Longview client integration — grant ready?", + snippet: + "Hi the owner — we got our scoped grant approved this morning. Wanted to flag: we're noticing some streams come back with `connection_id` populated and some without…", + date: ago(3 * DAY + 6 * HOUR), + labels: ["Inbox"], + has_attachment: false, + }, + { + id: rid("gm"), + thread_id: "t06", + from: "Strava ", + to: ["the owner@example.com"], + subject: "Your weekly summary: 31.4 mi", + snippet: "Nice work this week. 3 runs · 31.4 mi · 4h 48m · 1,247 ft elevation. New all-time best for May.", + date: ago(4 * DAY), + labels: ["Inbox", "Updates"], + has_attachment: false, + }, + { + id: rid("gm"), + thread_id: "t07", + from: "Chase Online ", + to: ["the owner@example.com"], + subject: "Alert: large purchase on •6432", + snippet: + "A $487.14 purchase at Apple Store was made on May 19. If this was you, no action needed. Otherwise reply STOP or call us.", + date: ago(5 * DAY), + labels: ["Inbox", "Banking"], + has_attachment: false, + }, + { + id: rid("gm"), + thread_id: "t08", + from: "GitHub ", + to: ["the owner@example.com"], + subject: "[vana-com/pdpp] PR #847 was merged: capability dispatch in explorer", + snippet: "Merged by @maya-chen · 12 files changed · +482 −137", + date: ago(6 * DAY), + labels: ["Inbox", "Code"], + has_attachment: false, + }, + { + id: rid("gm"), + thread_id: "t09", + from: "Spotify ", + to: ["the owner@example.com"], + subject: "Your minutes this month: 1,847", + snippet: + "You listened more than 87% of users in San Francisco this month. Top artist: Caroline Polachek. Top genre: art pop.", + date: ago(8 * DAY), + labels: ["Inbox"], + has_attachment: false, + }, + { + id: rid("gm"), + thread_id: "t10", + from: "Maya Chen ", + to: ["the owner@example.com"], + subject: "stair brewery sat?", + snippet: "they have the saison on tap again. 4ish? bring the dog", + date: ago(12 * DAY), + labels: ["Inbox", "Starred"], + has_attachment: false, + }, + { + id: rid("gm"), + thread_id: "t11", + from: "Costco Wholesale ", + to: ["the owner@example.com"], + subject: "Your Costco order is ready for pickup", + snippet: "Order #C-58291 · Pickup window: May 14, 4–6pm · 8 items · Total: $213.47", + date: ago(14 * DAY), + labels: ["Inbox", "Receipts"], + has_attachment: false, + }, + { + id: rid("gm"), + thread_id: "t12", + from: "ACLU ", + to: ["the owner@example.com"], + subject: "the owner — your May donation receipt", + snippet: "Thank you for your monthly $25 contribution. Tax receipt attached. Every dollar funds the fight.", + date: ago(18 * DAY), + labels: ["Inbox", "Receipts"], + has_attachment: true, + }, ]; const gmailWorkRecords = [ - { id: rid("gm"), thread_id: "t20", from: "Jordan Vasquez ", to: ["the owner@example.org"], subject: "[PDPP] design review — explorer prototype", snippet: "Sending notes from yesterday. tldr: the field-projection toggle is the killer feature, lean into it. also: command-K should default to lexical not semantic.", date: ago(4 * HOUR), labels: ["Inbox", "Important"], has_attachment: false }, - { id: rid("gm"), thread_id: "t21", from: "Jordan Vasquez ", to: ["the owner@example.org", "maya@example.org"], subject: "Re: launch readiness sync", snippet: "Pushing tomorrow's standup to 10:30 — Maya has the IETF call at 9. Agenda is in the doc, anyone can add.", date: ago(1 * DAY), labels: ["Inbox"], has_attachment: false }, - { id: rid("gm"), thread_id: "t22", from: "Calendar ", to: ["the owner@example.org"], subject: "Invitation: PDPP biweekly (May 27)", snippet: "Wed May 27 · 11:00–11:30 · Jordan, Maya, the owner, Drew · Zoom · recurring biweekly", date: ago(2 * DAY), labels: ["Inbox", "Calendar"], has_attachment: false }, - { id: rid("gm"), thread_id: "t23", from: "Drew Park ", to: ["the owner@example.org"], subject: "fwd: from the IETF list", snippet: "Worth a read — the working group is converging on something close to PDPP's grant model. They're calling it scoped-read profiles.", date: ago(5 * DAY), labels: ["Inbox"], has_attachment: false }, - { id: rid("gm"), thread_id: "t24", from: "1Password ", to: ["the owner@example.org"], subject: "New device signed in", snippet: "A new device (MacBook Pro · Safari 18) signed into your work vault from San Francisco, CA. If this was you, no action needed.", date: ago(8 * DAY), labels: ["Inbox", "Security"], has_attachment: false }, - { id: rid("gm"), thread_id: "t25", from: "Notion ", to: ["the owner@example.org"], subject: "Jordan shared a page with you: \"explorer copy passes\"", snippet: "Jordan Vasquez shared the page \"explorer copy passes\" in the Acme workspace. 14 comments waiting.", date: ago(10 * DAY), labels: ["Inbox"], has_attachment: false }, + { + id: rid("gm"), + thread_id: "t20", + from: "Jordan Vasquez ", + to: ["the owner@example.org"], + subject: "[PDPP] design review — explorer prototype", + snippet: + "Sending notes from yesterday. tldr: the field-projection toggle is the killer feature, lean into it. also: command-K should default to lexical not semantic.", + date: ago(4 * HOUR), + labels: ["Inbox", "Important"], + has_attachment: false, + }, + { + id: rid("gm"), + thread_id: "t21", + from: "Jordan Vasquez ", + to: ["the owner@example.org", "maya@example.org"], + subject: "Re: launch readiness sync", + snippet: "Pushing tomorrow's standup to 10:30 — Maya has the IETF call at 9. Agenda is in the doc, anyone can add.", + date: ago(1 * DAY), + labels: ["Inbox"], + has_attachment: false, + }, + { + id: rid("gm"), + thread_id: "t22", + from: "Calendar ", + to: ["the owner@example.org"], + subject: "Invitation: PDPP biweekly (May 27)", + snippet: "Wed May 27 · 11:00–11:30 · Jordan, Maya, the owner, Drew · Zoom · recurring biweekly", + date: ago(2 * DAY), + labels: ["Inbox", "Calendar"], + has_attachment: false, + }, + { + id: rid("gm"), + thread_id: "t23", + from: "Drew Park ", + to: ["the owner@example.org"], + subject: "fwd: from the IETF list", + snippet: + "Worth a read — the working group is converging on something close to PDPP's grant model. They're calling it scoped-read profiles.", + date: ago(5 * DAY), + labels: ["Inbox"], + has_attachment: false, + }, + { + id: rid("gm"), + thread_id: "t24", + from: "1Password ", + to: ["the owner@example.org"], + subject: "New device signed in", + snippet: + "A new device (MacBook Pro · Safari 18) signed into your work vault from San Francisco, CA. If this was you, no action needed.", + date: ago(8 * DAY), + labels: ["Inbox", "Security"], + has_attachment: false, + }, + { + id: rid("gm"), + thread_id: "t25", + from: "Notion ", + to: ["the owner@example.org"], + subject: 'Jordan shared a page with you: "explorer copy passes"', + snippet: 'Jordan Vasquez shared the page "explorer copy passes" in the Acme workspace. 14 comments waiting.', + date: ago(10 * DAY), + labels: ["Inbox"], + has_attachment: false, + }, ]; // SLACK ───────────────────────────────────────────────────────────────── const slackFields = [ - { name: "id", type: "id", granted: true }, - { name: "channel_id", type: "id", granted: true }, - { name: "channel", type: "text", granted: true }, - { name: "thread_ts", type: "id", granted: true }, - { name: "author", type: "person", granted: true }, - { name: "text", type: "text", granted: true }, - { name: "ts", type: "timestamp", granted: true }, - { name: "reactions", type: "json", granted: true }, - { name: "is_dm", type: "boolean", granted: true }, + { name: "id", type: "id", granted: true }, + { name: "channel_id", type: "id", granted: true }, + { name: "channel", type: "text", granted: true }, + { name: "thread_ts", type: "id", granted: true }, + { name: "author", type: "person", granted: true }, + { name: "text", type: "text", granted: true }, + { name: "ts", type: "timestamp", granted: true }, + { name: "reactions", type: "json", granted: true }, + { name: "is_dm", type: "boolean", granted: true }, ]; const slackViews = ["all", "this_week", "channels", "dms", "threads_im_in"]; const slackRecords = [ - { id: rid("sl"), channel: "#eng-platform", channel_id: "C01", thread_ts: null, author: "Jordan Vasquez", text: "ok the dispatch is in. capability detection working for gmail/slack/chase. amazon/strava next.", ts: ago(45 * MIN), reactions: [{emoji: "🚀", count: 3}, {emoji: "👀", count: 1}], is_dm: false }, - { id: rid("sl"), channel: "#eng-platform", channel_id: "C01", thread_ts: null, author: "the owner Rivera", text: "nice. how is it handling streams with mixed signals (e.g. photos has timestamp + geo + blob)?", ts: ago(40 * MIN), reactions: [], is_dm: false }, - { id: rid("sl"), channel: "#eng-platform", channel_id: "C01", thread_ts: null, author: "Jordan Vasquez", text: "lights up all three views. table is always there as floor. honestly feels right", ts: ago(38 * MIN), reactions: [{emoji: "💯", count: 2}], is_dm: false }, - { id: rid("sl"), channel: "#design", channel_id: "C02", thread_ts: null, author: "Maya Chen", text: "draft of the grant strip is in figma — i went with the always-visible variant. the dismissable one felt like an ad", ts: ago(3 * HOUR), reactions: [{emoji: "✅", count: 4}], is_dm: false }, - { id: rid("sl"), channel: "#design", channel_id: "C02", thread_ts: null, author: "Drew Park", text: "agree. one note: the expires-in countdown should be subtle, not loud. red only in the last 24h", ts: ago(2 * HOUR + 50 * MIN), reactions: [{emoji: "👍", count: 2}], is_dm: false }, - { id: rid("sl"), channel: "#general", channel_id: "C00", thread_ts: null, author: "Jordan Vasquez", text: "reminder: launch readiness sync tomorrow 10:30, not 9. agenda doc is open for additions", ts: ago(5 * HOUR), reactions: [{emoji: "🗓️", count: 7}], is_dm: false }, - { id: rid("sl"), channel: "DM with Maya Chen", channel_id: "D01", thread_ts: null, author: "Maya Chen", text: "do you want me to bring the redlines printed thursday or just on screen?", ts: ago(1 * DAY + 2 * HOUR), reactions: [], is_dm: true }, - { id: rid("sl"), channel: "DM with Maya Chen", channel_id: "D01", thread_ts: null, author: "the owner Rivera", text: "screen is fine. coffee plan stands?", ts: ago(1 * DAY + 1 * HOUR + 50 * MIN), reactions: [{emoji: "☕", count: 1}], is_dm: true }, - { id: rid("sl"), channel: "DM with Maya Chen", channel_id: "D01", thread_ts: null, author: "Maya Chen", text: "yep. ritual @ 1:30. see you there", ts: ago(1 * DAY + 1 * HOUR + 45 * MIN), reactions: [], is_dm: true }, - { id: rid("sl"), channel: "#eng-platform", channel_id: "C01", thread_ts: null, author: "Drew Park", text: "btw the openapi spec for /v1/search now has the mode enum — lexical/semantic/hybrid. backwards compatible, mode is optional, defaults to lexical", ts: ago(1 * DAY + 6 * HOUR), reactions: [{emoji: "📘", count: 2}], is_dm: false }, - { id: rid("sl"), channel: "#eng-platform", channel_id: "C01", thread_ts: null, author: "the owner Rivera", text: "the explorer is going to expose that as a pill in command-K. \"lex / sem / hybrid\". feels right", ts: ago(1 * DAY + 5 * HOUR + 30 * MIN), reactions: [{emoji: "💡", count: 1}], is_dm: false }, - { id: rid("sl"), channel: "#general", channel_id: "C00", thread_ts: null, author: "Drew Park", text: "tara from longview shipped the demo client. it's running against our sandbox AS. genuinely cool to see external code requesting a scoped grant", ts: ago(2 * DAY), reactions: [{emoji: "🎉", count: 9}, {emoji: "🔥", count: 4}], is_dm: false }, - { id: rid("sl"), channel: "#design", channel_id: "C02", thread_ts: null, author: "Maya Chen", text: "should the home screen lead with memories or with the heatmap? i keep flipping", ts: ago(2 * DAY + 4 * HOUR), reactions: [], is_dm: false }, - { id: rid("sl"), channel: "#design", channel_id: "C02", thread_ts: null, author: "the owner Rivera", text: "memories. heatmap is dense — better as a second-fold module. memories is what you want to *open* the app for", ts: ago(2 * DAY + 3 * HOUR + 55 * MIN), reactions: [{emoji: "✨", count: 3}], is_dm: false }, - { id: rid("sl"), channel: "#random", channel_id: "C03", thread_ts: null, author: "Drew Park", text: "hattie laid a green egg apparently. the owner has photographic evidence", ts: ago(2 * DAY + 8 * HOUR), reactions: [{emoji: "🥚", count: 6}, {emoji: "🐔", count: 4}], is_dm: false }, - { id: rid("sl"), channel: "DM with Jordan Vasquez", channel_id: "D02", thread_ts: null, author: "Jordan Vasquez", text: "are you good to demo the explorer at the IETF thing wed? 15 min slot, second half can be Q&A", ts: ago(3 * DAY), reactions: [], is_dm: true }, - { id: rid("sl"), channel: "DM with Jordan Vasquez", channel_id: "D02", thread_ts: null, author: "the owner Rivera", text: "yes. i'll lead with the field-projection toggle. that's the moment.", ts: ago(3 * DAY - 10 * MIN), reactions: [{emoji: "💪", count: 1}], is_dm: true }, - { id: rid("sl"), channel: "#general", channel_id: "C00", thread_ts: null, author: "Jordan Vasquez", text: "spec working group put out 0.9-rc1. our reference impl validates clean against it. one minor breaking change in /v1/search results shape — drew's PR covers it", ts: ago(5 * DAY), reactions: [{emoji: "📦", count: 5}], is_dm: false }, - { id: rid("sl"), channel: "#eng-platform", channel_id: "C01", thread_ts: null, author: "Maya Chen", text: "is anyone else seeing the chase connector eat its 2FA cookie between runs? it's been every-time-otp for two weeks", ts: ago(6 * DAY), reactions: [{emoji: "😩", count: 2}], is_dm: false }, - { id: rid("sl"), channel: "#eng-platform", channel_id: "C01", thread_ts: null, author: "Drew Park", text: "yeah — _tmprememberme is session-only. there's an open PR. it's a chase-side change in how they set the cookie", ts: ago(6 * DAY - 5 * MIN), reactions: [{emoji: "🔍", count: 1}], is_dm: false }, + { + id: rid("sl"), + channel: "#eng-platform", + channel_id: "C01", + thread_ts: null, + author: "Jordan Vasquez", + text: "ok the dispatch is in. capability detection working for gmail/slack/chase. amazon/strava next.", + ts: ago(45 * MIN), + reactions: [ + { emoji: "🚀", count: 3 }, + { emoji: "👀", count: 1 }, + ], + is_dm: false, + }, + { + id: rid("sl"), + channel: "#eng-platform", + channel_id: "C01", + thread_ts: null, + author: "the owner Rivera", + text: "nice. how is it handling streams with mixed signals (e.g. photos has timestamp + geo + blob)?", + ts: ago(40 * MIN), + reactions: [], + is_dm: false, + }, + { + id: rid("sl"), + channel: "#eng-platform", + channel_id: "C01", + thread_ts: null, + author: "Jordan Vasquez", + text: "lights up all three views. table is always there as floor. honestly feels right", + ts: ago(38 * MIN), + reactions: [{ emoji: "💯", count: 2 }], + is_dm: false, + }, + { + id: rid("sl"), + channel: "#design", + channel_id: "C02", + thread_ts: null, + author: "Maya Chen", + text: "draft of the grant strip is in figma — i went with the always-visible variant. the dismissable one felt like an ad", + ts: ago(3 * HOUR), + reactions: [{ emoji: "✅", count: 4 }], + is_dm: false, + }, + { + id: rid("sl"), + channel: "#design", + channel_id: "C02", + thread_ts: null, + author: "Drew Park", + text: "agree. one note: the expires-in countdown should be subtle, not loud. red only in the last 24h", + ts: ago(2 * HOUR + 50 * MIN), + reactions: [{ emoji: "👍", count: 2 }], + is_dm: false, + }, + { + id: rid("sl"), + channel: "#general", + channel_id: "C00", + thread_ts: null, + author: "Jordan Vasquez", + text: "reminder: launch readiness sync tomorrow 10:30, not 9. agenda doc is open for additions", + ts: ago(5 * HOUR), + reactions: [{ emoji: "🗓️", count: 7 }], + is_dm: false, + }, + { + id: rid("sl"), + channel: "DM with Maya Chen", + channel_id: "D01", + thread_ts: null, + author: "Maya Chen", + text: "do you want me to bring the redlines printed thursday or just on screen?", + ts: ago(1 * DAY + 2 * HOUR), + reactions: [], + is_dm: true, + }, + { + id: rid("sl"), + channel: "DM with Maya Chen", + channel_id: "D01", + thread_ts: null, + author: "the owner Rivera", + text: "screen is fine. coffee plan stands?", + ts: ago(1 * DAY + 1 * HOUR + 50 * MIN), + reactions: [{ emoji: "☕", count: 1 }], + is_dm: true, + }, + { + id: rid("sl"), + channel: "DM with Maya Chen", + channel_id: "D01", + thread_ts: null, + author: "Maya Chen", + text: "yep. ritual @ 1:30. see you there", + ts: ago(1 * DAY + 1 * HOUR + 45 * MIN), + reactions: [], + is_dm: true, + }, + { + id: rid("sl"), + channel: "#eng-platform", + channel_id: "C01", + thread_ts: null, + author: "Drew Park", + text: "btw the openapi spec for /v1/search now has the mode enum — lexical/semantic/hybrid. backwards compatible, mode is optional, defaults to lexical", + ts: ago(1 * DAY + 6 * HOUR), + reactions: [{ emoji: "📘", count: 2 }], + is_dm: false, + }, + { + id: rid("sl"), + channel: "#eng-platform", + channel_id: "C01", + thread_ts: null, + author: "the owner Rivera", + text: 'the explorer is going to expose that as a pill in command-K. "lex / sem / hybrid". feels right', + ts: ago(1 * DAY + 5 * HOUR + 30 * MIN), + reactions: [{ emoji: "💡", count: 1 }], + is_dm: false, + }, + { + id: rid("sl"), + channel: "#general", + channel_id: "C00", + thread_ts: null, + author: "Drew Park", + text: "tara from longview shipped the demo client. it's running against our sandbox AS. genuinely cool to see external code requesting a scoped grant", + ts: ago(2 * DAY), + reactions: [ + { emoji: "🎉", count: 9 }, + { emoji: "🔥", count: 4 }, + ], + is_dm: false, + }, + { + id: rid("sl"), + channel: "#design", + channel_id: "C02", + thread_ts: null, + author: "Maya Chen", + text: "should the home screen lead with memories or with the heatmap? i keep flipping", + ts: ago(2 * DAY + 4 * HOUR), + reactions: [], + is_dm: false, + }, + { + id: rid("sl"), + channel: "#design", + channel_id: "C02", + thread_ts: null, + author: "the owner Rivera", + text: "memories. heatmap is dense — better as a second-fold module. memories is what you want to *open* the app for", + ts: ago(2 * DAY + 3 * HOUR + 55 * MIN), + reactions: [{ emoji: "✨", count: 3 }], + is_dm: false, + }, + { + id: rid("sl"), + channel: "#random", + channel_id: "C03", + thread_ts: null, + author: "Drew Park", + text: "hattie laid a green egg apparently. the owner has photographic evidence", + ts: ago(2 * DAY + 8 * HOUR), + reactions: [ + { emoji: "🥚", count: 6 }, + { emoji: "🐔", count: 4 }, + ], + is_dm: false, + }, + { + id: rid("sl"), + channel: "DM with Jordan Vasquez", + channel_id: "D02", + thread_ts: null, + author: "Jordan Vasquez", + text: "are you good to demo the explorer at the IETF thing wed? 15 min slot, second half can be Q&A", + ts: ago(3 * DAY), + reactions: [], + is_dm: true, + }, + { + id: rid("sl"), + channel: "DM with Jordan Vasquez", + channel_id: "D02", + thread_ts: null, + author: "the owner Rivera", + text: "yes. i'll lead with the field-projection toggle. that's the moment.", + ts: ago(3 * DAY - 10 * MIN), + reactions: [{ emoji: "💪", count: 1 }], + is_dm: true, + }, + { + id: rid("sl"), + channel: "#general", + channel_id: "C00", + thread_ts: null, + author: "Jordan Vasquez", + text: "spec working group put out 0.9-rc1. our reference impl validates clean against it. one minor breaking change in /v1/search results shape — drew's PR covers it", + ts: ago(5 * DAY), + reactions: [{ emoji: "📦", count: 5 }], + is_dm: false, + }, + { + id: rid("sl"), + channel: "#eng-platform", + channel_id: "C01", + thread_ts: null, + author: "Maya Chen", + text: "is anyone else seeing the chase connector eat its 2FA cookie between runs? it's been every-time-otp for two weeks", + ts: ago(6 * DAY), + reactions: [{ emoji: "😩", count: 2 }], + is_dm: false, + }, + { + id: rid("sl"), + channel: "#eng-platform", + channel_id: "C01", + thread_ts: null, + author: "Drew Park", + text: "yeah — _tmprememberme is session-only. there's an open PR. it's a chase-side change in how they set the cookie", + ts: ago(6 * DAY - 5 * MIN), + reactions: [{ emoji: "🔍", count: 1 }], + is_dm: false, + }, ]; // CHASE ───────────────────────────────────────────────────────────────── const chaseFields = [ - { name: "id", type: "id", granted: true }, - { name: "account_id", type: "id", granted: true }, - { name: "posted_at", type: "timestamp", granted: true }, - { name: "merchant", type: "text", granted: true }, - { name: "category", type: "enum", granted: true }, - { name: "amount", type: "currency", granted: true, currency: "USD" }, + { name: "id", type: "id", granted: true }, + { name: "account_id", type: "id", granted: true }, + { name: "posted_at", type: "timestamp", granted: true }, + { name: "merchant", type: "text", granted: true }, + { name: "category", type: "enum", granted: true }, + { name: "amount", type: "currency", granted: true, currency: "USD" }, { name: "balance_after", type: "currency", granted: false, redacted_reason: "running balance not in grant" }, - { name: "memo", type: "text", granted: true }, + { name: "memo", type: "text", granted: true }, ]; const chaseViews = ["all", "this_month", "by_category", "large_only"]; const chaseRecords = [ - { id: rid("ch"), account_id: "•6432", posted_at: ago(6 * HOUR), merchant: "Blue Bottle Coffee — Mint Plaza", category: "Food & Drink", amount: -5.75, memo: "" }, - { id: rid("ch"), account_id: "•6432", posted_at: ago(1 * DAY + 2*HOUR), merchant: "Trader Joe's", category: "Groceries", amount: -67.42, memo: "" }, - { id: rid("ch"), account_id: "•6432", posted_at: ago(1 * DAY + 8*HOUR), merchant: "Caltrain · Mobile", category: "Transit", amount: -7.00, memo: "Mountain View → 22nd St" }, - { id: rid("ch"), account_id: "•6432", posted_at: ago(2 * DAY), merchant: "Ritual Coffee Roasters", category: "Food & Drink", amount: -4.50, memo: "" }, - { id: rid("ch"), account_id: "•6432", posted_at: ago(3 * DAY), merchant: "Pacific Gas & Electric", category: "Utilities", amount: -118.34, memo: "Bill payment" }, - { id: rid("ch"), account_id: "•6432", posted_at: ago(4 * DAY), merchant: "Whole Foods", category: "Groceries", amount: -84.19, memo: "" }, - { id: rid("ch"), account_id: "•6432", posted_at: ago(4 * DAY + 3*HOUR), merchant: "Lyft · Ride", category: "Transit", amount: -14.20, memo: "Mission → Outer Sunset" }, - { id: rid("ch"), account_id: "•6432", posted_at: ago(5 * DAY), merchant: "Apple Store — Stockton St", category: "Electronics", amount: -487.14, memo: "AirPods Max" }, - { id: rid("ch"), account_id: "•6432", posted_at: ago(6 * DAY), merchant: "Stripe — Payroll Deposit", category: "Income", amount: 4_872.00, memo: "Vana May 1-15" }, - { id: rid("ch"), account_id: "•6432", posted_at: ago(7 * DAY), merchant: "Rainbow Grocery", category: "Groceries", amount: -31.07, memo: "" }, - { id: rid("ch"), account_id: "•6432", posted_at: ago(9 * DAY), merchant: "Comcast Internet", category: "Utilities", amount: -89.99, memo: "" }, - { id: rid("ch"), account_id: "•6432", posted_at: ago(10 * DAY), merchant: "Tartine Bakery", category: "Food & Drink", amount: -12.50, memo: "" }, - { id: rid("ch"), account_id: "•6432", posted_at: ago(11 * DAY), merchant: "Costco Wholesale", category: "Groceries", amount: -213.47, memo: "" }, - { id: rid("ch"), account_id: "•6432", posted_at: ago(13 * DAY), merchant: "Amazon.com", category: "Shopping", amount: -42.18, memo: "Books" }, - { id: rid("ch"), account_id: "•6432", posted_at: ago(15 * DAY), merchant: "Chevron · Castro", category: "Transit", amount: -52.40, memo: "" }, - { id: rid("ch"), account_id: "•6432", posted_at: ago(17 * DAY), merchant: "ACLU of Northern California", category: "Donations", amount: -25.00, memo: "Monthly" }, - { id: rid("ch"), account_id: "•6432", posted_at: ago(20 * DAY), merchant: "Stripe — Payroll Deposit", category: "Income", amount: 4_872.00, memo: "Vana April 16-30" }, - { id: rid("ch"), account_id: "•6432", posted_at: ago(22 * DAY), merchant: "Bi-Rite Market", category: "Groceries", amount: -38.91, memo: "" }, - { id: rid("ch"), account_id: "•6432", posted_at: ago(24 * DAY), merchant: "Hayes Valley Bakeworks", category: "Food & Drink", amount: -8.75, memo: "" }, - { id: rid("ch"), account_id: "•6432", posted_at: ago(26 * DAY), merchant: "Anthropic — Claude Pro", category: "Subscriptions", amount: -20.00, memo: "" }, + { + id: rid("ch"), + account_id: "•6432", + posted_at: ago(6 * HOUR), + merchant: "Blue Bottle Coffee — Mint Plaza", + category: "Food & Drink", + amount: -5.75, + memo: "", + }, + { + id: rid("ch"), + account_id: "•6432", + posted_at: ago(1 * DAY + 2 * HOUR), + merchant: "Trader Joe's", + category: "Groceries", + amount: -67.42, + memo: "", + }, + { + id: rid("ch"), + account_id: "•6432", + posted_at: ago(1 * DAY + 8 * HOUR), + merchant: "Caltrain · Mobile", + category: "Transit", + amount: -7.0, + memo: "Mountain View → 22nd St", + }, + { + id: rid("ch"), + account_id: "•6432", + posted_at: ago(2 * DAY), + merchant: "Ritual Coffee Roasters", + category: "Food & Drink", + amount: -4.5, + memo: "", + }, + { + id: rid("ch"), + account_id: "•6432", + posted_at: ago(3 * DAY), + merchant: "Pacific Gas & Electric", + category: "Utilities", + amount: -118.34, + memo: "Bill payment", + }, + { + id: rid("ch"), + account_id: "•6432", + posted_at: ago(4 * DAY), + merchant: "Whole Foods", + category: "Groceries", + amount: -84.19, + memo: "", + }, + { + id: rid("ch"), + account_id: "•6432", + posted_at: ago(4 * DAY + 3 * HOUR), + merchant: "Lyft · Ride", + category: "Transit", + amount: -14.2, + memo: "Mission → Outer Sunset", + }, + { + id: rid("ch"), + account_id: "•6432", + posted_at: ago(5 * DAY), + merchant: "Apple Store — Stockton St", + category: "Electronics", + amount: -487.14, + memo: "AirPods Max", + }, + { + id: rid("ch"), + account_id: "•6432", + posted_at: ago(6 * DAY), + merchant: "Stripe — Payroll Deposit", + category: "Income", + amount: 4_872.0, + memo: "Vana May 1-15", + }, + { + id: rid("ch"), + account_id: "•6432", + posted_at: ago(7 * DAY), + merchant: "Rainbow Grocery", + category: "Groceries", + amount: -31.07, + memo: "", + }, + { + id: rid("ch"), + account_id: "•6432", + posted_at: ago(9 * DAY), + merchant: "Comcast Internet", + category: "Utilities", + amount: -89.99, + memo: "", + }, + { + id: rid("ch"), + account_id: "•6432", + posted_at: ago(10 * DAY), + merchant: "Tartine Bakery", + category: "Food & Drink", + amount: -12.5, + memo: "", + }, + { + id: rid("ch"), + account_id: "•6432", + posted_at: ago(11 * DAY), + merchant: "Costco Wholesale", + category: "Groceries", + amount: -213.47, + memo: "", + }, + { + id: rid("ch"), + account_id: "•6432", + posted_at: ago(13 * DAY), + merchant: "Amazon.com", + category: "Shopping", + amount: -42.18, + memo: "Books", + }, + { + id: rid("ch"), + account_id: "•6432", + posted_at: ago(15 * DAY), + merchant: "Chevron · Castro", + category: "Transit", + amount: -52.4, + memo: "", + }, + { + id: rid("ch"), + account_id: "•6432", + posted_at: ago(17 * DAY), + merchant: "ACLU of Northern California", + category: "Donations", + amount: -25.0, + memo: "Monthly", + }, + { + id: rid("ch"), + account_id: "•6432", + posted_at: ago(20 * DAY), + merchant: "Stripe — Payroll Deposit", + category: "Income", + amount: 4_872.0, + memo: "Vana April 16-30", + }, + { + id: rid("ch"), + account_id: "•6432", + posted_at: ago(22 * DAY), + merchant: "Bi-Rite Market", + category: "Groceries", + amount: -38.91, + memo: "", + }, + { + id: rid("ch"), + account_id: "•6432", + posted_at: ago(24 * DAY), + merchant: "Hayes Valley Bakeworks", + category: "Food & Drink", + amount: -8.75, + memo: "", + }, + { + id: rid("ch"), + account_id: "•6432", + posted_at: ago(26 * DAY), + merchant: "Anthropic — Claude Pro", + category: "Subscriptions", + amount: -20.0, + memo: "", + }, ]; // AMAZON ──────────────────────────────────────────────────────────────── const amazonFields = [ - { name: "id", type: "id", granted: true }, - { name: "ordered_at", type: "timestamp", granted: true }, - { name: "title", type: "text", granted: true }, - { name: "merchant", type: "text", granted: true }, - { name: "amount", type: "currency", granted: true, currency: "USD" }, - { name: "thumbnail", type: "blob", granted: true, media_type: "image/jpeg" }, - { name: "status", type: "enum", granted: true }, - { name: "tracking_id", type: "id", granted: false, redacted_reason: "out of scope" }, + { name: "id", type: "id", granted: true }, + { name: "ordered_at", type: "timestamp", granted: true }, + { name: "title", type: "text", granted: true }, + { name: "merchant", type: "text", granted: true }, + { name: "amount", type: "currency", granted: true, currency: "USD" }, + { name: "thumbnail", type: "blob", granted: true, media_type: "image/jpeg" }, + { name: "status", type: "enum", granted: true }, + { name: "tracking_id", type: "id", granted: false, redacted_reason: "out of scope" }, ]; const amazonViews = ["all", "this_year", "by_seller"]; const amazonRecords = [ - { id: rid("az"), ordered_at: ago(2 * DAY), title: "Hario V60 Plastic Coffee Dripper, Size 02", merchant: "Hario Direct", amount: -14.99, thumbnail: "https://picsum.photos/seed/hario/240", status: "Delivered" }, - { id: rid("az"), ordered_at: ago(6 * DAY), title: "DK Bicycles Crank Brothers Eggbeater 3 Pedals", merchant: "Crank Brothers", amount: -129.95, thumbnail: "https://picsum.photos/seed/pedal/240", status: "Delivered" }, - { id: rid("az"), ordered_at: ago(11 * DAY), title: "Anker 737 Power Bank 24,000mAh 140W USB-C", merchant: "AnkerDirect", amount: -89.99, thumbnail: "https://picsum.photos/seed/anker/240", status: "Delivered" }, - { id: rid("az"), ordered_at: ago(13 * DAY), title: "The Address Book by Sophie Calle", merchant: "Siglio Press", amount: -42.18, thumbnail: "https://picsum.photos/seed/book/240", status: "Delivered" }, - { id: rid("az"), ordered_at: ago(18 * DAY), title: "Field Notes Original Kraft 3-Pack (Graph)", merchant: "Field Notes Brand", amount: -12.95, thumbnail: "https://picsum.photos/seed/fieldnotes/240", status: "Delivered" }, - { id: rid("az"), ordered_at: ago(22 * DAY), title: "Stanley IceFlow Flip Straw Tumbler 30oz", merchant: "Stanley", amount: -35.00, thumbnail: "https://picsum.photos/seed/stanley/240", status: "Delivered" }, + { + id: rid("az"), + ordered_at: ago(2 * DAY), + title: "Hario V60 Plastic Coffee Dripper, Size 02", + merchant: "Hario Direct", + amount: -14.99, + thumbnail: "https://picsum.photos/seed/hario/240", + status: "Delivered", + }, + { + id: rid("az"), + ordered_at: ago(6 * DAY), + title: "DK Bicycles Crank Brothers Eggbeater 3 Pedals", + merchant: "Crank Brothers", + amount: -129.95, + thumbnail: "https://picsum.photos/seed/pedal/240", + status: "Delivered", + }, + { + id: rid("az"), + ordered_at: ago(11 * DAY), + title: "Anker 737 Power Bank 24,000mAh 140W USB-C", + merchant: "AnkerDirect", + amount: -89.99, + thumbnail: "https://picsum.photos/seed/anker/240", + status: "Delivered", + }, + { + id: rid("az"), + ordered_at: ago(13 * DAY), + title: "The Address Book by Sophie Calle", + merchant: "Siglio Press", + amount: -42.18, + thumbnail: "https://picsum.photos/seed/book/240", + status: "Delivered", + }, + { + id: rid("az"), + ordered_at: ago(18 * DAY), + title: "Field Notes Original Kraft 3-Pack (Graph)", + merchant: "Field Notes Brand", + amount: -12.95, + thumbnail: "https://picsum.photos/seed/fieldnotes/240", + status: "Delivered", + }, + { + id: rid("az"), + ordered_at: ago(22 * DAY), + title: "Stanley IceFlow Flip Straw Tumbler 30oz", + merchant: "Stanley", + amount: -35.0, + thumbnail: "https://picsum.photos/seed/stanley/240", + status: "Delivered", + }, ]; // GITHUB ──────────────────────────────────────────────────────────────── const githubFields = [ - { name: "id", type: "id", granted: true }, - { name: "type", type: "enum", granted: true }, - { name: "repo", type: "text", granted: true }, - { name: "title", type: "text", granted: true }, - { name: "body", type: "text", granted: true }, - { name: "actor", type: "person", granted: true }, + { name: "id", type: "id", granted: true }, + { name: "type", type: "enum", granted: true }, + { name: "repo", type: "text", granted: true }, + { name: "title", type: "text", granted: true }, + { name: "body", type: "text", granted: true }, + { name: "actor", type: "person", granted: true }, { name: "created_at", type: "timestamp", granted: true }, - { name: "url", type: "url", granted: true }, - { name: "additions", type: "number", granted: true }, - { name: "deletions", type: "number", granted: true }, + { name: "url", type: "url", granted: true }, + { name: "additions", type: "number", granted: true }, + { name: "deletions", type: "number", granted: true }, ]; const githubViews = ["all", "commits", "prs", "issues"]; const githubRecords = [ - { id: rid("gh"), type: "PullRequest", repo: "vana-com/pdpp", title: "explorer: capability dispatch + tier-1 table view", body: "Implements the schema-signal dispatch we discussed. Streams now declare typed fields and the explorer infers timeline/map/gallery/ledger/conversation/calendar/reader/chart eligibility from the fields, not the connector id.", actor: "owner", created_at: ago(3 * HOUR), url: "https://github.com/PDP-Connect/pdpp/pull/851", additions: 712, deletions: 184 }, - { id: rid("gh"), type: "Push", repo: "vana-com/pdpp", title: "fix: peek panel respects field projection toggle", body: "Was showing all fields regardless of grant.", actor: "owner", created_at: ago(8 * HOUR), url: "https://github.com/PDP-Connect/pdpp/commit/a47c2d1", additions: 28, deletions: 12 }, - { id: rid("gh"), type: "PullRequest", repo: "vana-com/pdpp", title: "search: expose mode (lexical/semantic/hybrid)", body: "Propagates the new search mode enum through MCP and the dashboard search view.", actor: "drewpark", created_at: ago(1 * DAY + 2 * HOUR), url: "https://github.com/PDP-Connect/pdpp/pull/848", additions: 412, deletions: 89 }, - { id: rid("gh"), type: "PullRequest", repo: "vana-com/pdpp", title: "[merged] capability dispatch in explorer", body: "First version of the dispatch + table fallback. Connector-specific layouts will land in follow-up PRs.", actor: "mayachen", created_at: ago(6 * DAY), url: "https://github.com/PDP-Connect/pdpp/pull/847", additions: 482, deletions: 137 }, - { id: rid("gh"), type: "Issue", repo: "vana-com/pdpp", title: "chase connector loses trusted-device cookie between runs", body: "Every run requires fresh SMS OTP. Suspect _tmprememberme is session-only.", actor: "owner", created_at: ago(6 * DAY + 4 * HOUR), url: "https://github.com/PDP-Connect/pdpp/issues/843", additions: 0, deletions: 0 }, - { id: rid("gh"), type: "Push", repo: "owner/resource-budget", title: "0.4.2: nicer histogram colors", body: "Use copper for the bars and grey for the baseline.", actor: "owner", created_at: ago(9 * DAY), url: "https://github.com/owner/resource-budget/commit/9d7e3a2", additions: 14, deletions: 8 }, - { id: rid("gh"), type: "PullRequest", repo: "vana-com/pdpp", title: "polyfill-connectors: scaffold loom + linkedin", body: "Manifest + connector shell only; selectors TBD with live co-pilot session.", actor: "owner", created_at: ago(12 * DAY), url: "https://github.com/PDP-Connect/pdpp/pull/831", additions: 348, deletions: 0 }, - { id: rid("gh"), type: "Issue", repo: "vana-com/pdpp", title: "MCP `search` mode pill not surfaced in agent skill", body: "We added the enum but the agent skill instructions still pin mode=lexical.", actor: "annavasquez", created_at: ago(15 * DAY), url: "https://github.com/PDP-Connect/pdpp/issues/827", additions: 0, deletions: 0 }, - { id: rid("gh"), type: "Push", repo: "owner/dotfiles", title: "switch shell to ghostty + nushell", body: "no regrets", actor: "owner", created_at: ago(19 * DAY), url: "https://github.com/owner/dotfiles/commit/0c8a91f", additions: 84, deletions: 142 }, - { id: rid("gh"), type: "PullRequest", repo: "vana-com/pdpp", title: "docs: human/protocol duality writeup", body: "Adds README section on the 2px copper-vs-blue temperature system.", actor: "mayachen", created_at: ago(23 * DAY), url: "https://github.com/PDP-Connect/pdpp/pull/814", additions: 187, deletions: 14 }, + { + id: rid("gh"), + type: "PullRequest", + repo: "vana-com/pdpp", + title: "explorer: capability dispatch + tier-1 table view", + body: "Implements the schema-signal dispatch we discussed. Streams now declare typed fields and the explorer infers timeline/map/gallery/ledger/conversation/calendar/reader/chart eligibility from the fields, not the connector id.", + actor: "owner", + created_at: ago(3 * HOUR), + url: "https://github.com/PDP-Connect/pdpp/pull/851", + additions: 712, + deletions: 184, + }, + { + id: rid("gh"), + type: "Push", + repo: "vana-com/pdpp", + title: "fix: peek panel respects field projection toggle", + body: "Was showing all fields regardless of grant.", + actor: "owner", + created_at: ago(8 * HOUR), + url: "https://github.com/PDP-Connect/pdpp/commit/a47c2d1", + additions: 28, + deletions: 12, + }, + { + id: rid("gh"), + type: "PullRequest", + repo: "vana-com/pdpp", + title: "search: expose mode (lexical/semantic/hybrid)", + body: "Propagates the new search mode enum through MCP and the dashboard search view.", + actor: "drewpark", + created_at: ago(1 * DAY + 2 * HOUR), + url: "https://github.com/PDP-Connect/pdpp/pull/848", + additions: 412, + deletions: 89, + }, + { + id: rid("gh"), + type: "PullRequest", + repo: "vana-com/pdpp", + title: "[merged] capability dispatch in explorer", + body: "First version of the dispatch + table fallback. Connector-specific layouts will land in follow-up PRs.", + actor: "mayachen", + created_at: ago(6 * DAY), + url: "https://github.com/PDP-Connect/pdpp/pull/847", + additions: 482, + deletions: 137, + }, + { + id: rid("gh"), + type: "Issue", + repo: "vana-com/pdpp", + title: "chase connector loses trusted-device cookie between runs", + body: "Every run requires fresh SMS OTP. Suspect _tmprememberme is session-only.", + actor: "owner", + created_at: ago(6 * DAY + 4 * HOUR), + url: "https://github.com/PDP-Connect/pdpp/issues/843", + additions: 0, + deletions: 0, + }, + { + id: rid("gh"), + type: "Push", + repo: "owner/resource-budget", + title: "0.4.2: nicer histogram colors", + body: "Use copper for the bars and grey for the baseline.", + actor: "owner", + created_at: ago(9 * DAY), + url: "https://github.com/owner/resource-budget/commit/9d7e3a2", + additions: 14, + deletions: 8, + }, + { + id: rid("gh"), + type: "PullRequest", + repo: "vana-com/pdpp", + title: "polyfill-connectors: scaffold loom + linkedin", + body: "Manifest + connector shell only; selectors TBD with live co-pilot session.", + actor: "owner", + created_at: ago(12 * DAY), + url: "https://github.com/PDP-Connect/pdpp/pull/831", + additions: 348, + deletions: 0, + }, + { + id: rid("gh"), + type: "Issue", + repo: "vana-com/pdpp", + title: "MCP `search` mode pill not surfaced in agent skill", + body: "We added the enum but the agent skill instructions still pin mode=lexical.", + actor: "annavasquez", + created_at: ago(15 * DAY), + url: "https://github.com/PDP-Connect/pdpp/issues/827", + additions: 0, + deletions: 0, + }, + { + id: rid("gh"), + type: "Push", + repo: "owner/dotfiles", + title: "switch shell to ghostty + nushell", + body: "no regrets", + actor: "owner", + created_at: ago(19 * DAY), + url: "https://github.com/owner/dotfiles/commit/0c8a91f", + additions: 84, + deletions: 142, + }, + { + id: rid("gh"), + type: "PullRequest", + repo: "vana-com/pdpp", + title: "docs: human/protocol duality writeup", + body: "Adds README section on the 2px copper-vs-blue temperature system.", + actor: "mayachen", + created_at: ago(23 * DAY), + url: "https://github.com/PDP-Connect/pdpp/pull/814", + additions: 187, + deletions: 14, + }, ]; // STRAVA ──────────────────────────────────────────────────────────────── const stravaFields = [ - { name: "id", type: "id", granted: true }, - { name: "type", type: "enum", granted: true }, - { name: "title", type: "text", granted: true }, - { name: "started_at", type: "timestamp", granted: true }, - { name: "distance_m", type: "number", granted: true, unit: "meters" }, - { name: "duration_s", type: "number", granted: true, unit: "seconds" }, - { name: "elev_m", type: "number", granted: true, unit: "meters" }, - { name: "start_lat", type: "number", granted: true }, - { name: "start_lng", type: "number", granted: true }, - { name: "polyline", type: "geo", granted: false, redacted_reason: "route geometry not in grant" }, + { name: "id", type: "id", granted: true }, + { name: "type", type: "enum", granted: true }, + { name: "title", type: "text", granted: true }, + { name: "started_at", type: "timestamp", granted: true }, + { name: "distance_m", type: "number", granted: true, unit: "meters" }, + { name: "duration_s", type: "number", granted: true, unit: "seconds" }, + { name: "elev_m", type: "number", granted: true, unit: "meters" }, + { name: "start_lat", type: "number", granted: true }, + { name: "start_lng", type: "number", granted: true }, + { name: "polyline", type: "geo", granted: false, redacted_reason: "route geometry not in grant" }, ]; const stravaViews = ["all", "runs", "rides", "this_month"]; const stravaRecords = [ - { id: rid("st"), type: "Run", title: "Lunch trail loop · Glen Canyon", started_at: ago(7 * HOUR), distance_m: 8_240, duration_s: 2_730, elev_m: 187, start_lat: 37.7390, start_lng: -122.4408 }, - { id: rid("st"), type: "Ride", title: "Marin Headlands · Hawk Hill", started_at: ago(2 * DAY + 4 * HOUR), distance_m: 38_700, duration_s: 6_840, elev_m: 612, start_lat: 37.8324, start_lng: -122.4795 }, - { id: rid("st"), type: "Run", title: "Easy shakeout — Mission Dolores", started_at: ago(3 * DAY + 7 * HOUR), distance_m: 5_080, duration_s: 1_680, elev_m: 41, start_lat: 37.7585, start_lng: -122.4263 }, - { id: rid("st"), type: "Run", title: "Sunday long — Lands End", started_at: ago(5 * DAY + 8 * HOUR), distance_m: 18_120, duration_s: 6_180, elev_m: 284, start_lat: 37.7820, start_lng: -122.5060 }, - { id: rid("st"), type: "Ride", title: "Commute · Mission → Soma", started_at: ago(6 * DAY + 1 * HOUR), distance_m: 4_310, duration_s: 1_080, elev_m: 28, start_lat: 37.7599, start_lng: -122.4147 }, - { id: rid("st"), type: "Run", title: "Track Tuesday — 6x800", started_at: ago(8 * DAY + 7 * HOUR), distance_m: 9_600, duration_s: 2_810, elev_m: 12, start_lat: 37.7311, start_lng: -122.4470 }, - { id: rid("st"), type: "Ride", title: "Sausalito ferry loop", started_at: ago(11 * DAY + 9 * HOUR), distance_m: 52_400, duration_s: 9_120, elev_m: 487, start_lat: 37.8086, start_lng: -122.4108 }, - { id: rid("st"), type: "Run", title: "Recovery — Bernal Heights", started_at: ago(13 * DAY + 8 * HOUR), distance_m: 4_700, duration_s: 1_620, elev_m: 91, start_lat: 37.7430, start_lng: -122.4140 }, - { id: rid("st"), type: "Run", title: "Hill repeats — Sanchez stairs", started_at: ago(15 * DAY + 7 * HOUR), distance_m: 6_800, duration_s: 2_280, elev_m: 312, start_lat: 37.7510, start_lng: -122.4291 }, - { id: rid("st"), type: "Ride", title: "GG Park to Ocean Beach", started_at: ago(18 * DAY + 10 * HOUR), distance_m: 14_200, duration_s: 2_640, elev_m: 78, start_lat: 37.7694, start_lng: -122.4862 }, - { id: rid("st"), type: "Run", title: "Crissy Field flat", started_at: ago(22 * DAY + 7 * HOUR), distance_m: 7_300, duration_s: 2_460, elev_m: 22, start_lat: 37.8030, start_lng: -122.4660 }, - { id: rid("st"), type: "Run", title: "Long — Presidio loop", started_at: ago(26 * DAY + 8 * HOUR), distance_m: 16_400, duration_s: 5_700, elev_m: 198, start_lat: 37.7989, start_lng: -122.4662 }, + { + id: rid("st"), + type: "Run", + title: "Lunch trail loop · Glen Canyon", + started_at: ago(7 * HOUR), + distance_m: 8_240, + duration_s: 2_730, + elev_m: 187, + start_lat: 37.739, + start_lng: -122.4408, + }, + { + id: rid("st"), + type: "Ride", + title: "Marin Headlands · Hawk Hill", + started_at: ago(2 * DAY + 4 * HOUR), + distance_m: 38_700, + duration_s: 6_840, + elev_m: 612, + start_lat: 37.8324, + start_lng: -122.4795, + }, + { + id: rid("st"), + type: "Run", + title: "Easy shakeout — Mission Dolores", + started_at: ago(3 * DAY + 7 * HOUR), + distance_m: 5_080, + duration_s: 1_680, + elev_m: 41, + start_lat: 37.7585, + start_lng: -122.4263, + }, + { + id: rid("st"), + type: "Run", + title: "Sunday long — Lands End", + started_at: ago(5 * DAY + 8 * HOUR), + distance_m: 18_120, + duration_s: 6_180, + elev_m: 284, + start_lat: 37.782, + start_lng: -122.506, + }, + { + id: rid("st"), + type: "Ride", + title: "Commute · Mission → Soma", + started_at: ago(6 * DAY + 1 * HOUR), + distance_m: 4_310, + duration_s: 1_080, + elev_m: 28, + start_lat: 37.7599, + start_lng: -122.4147, + }, + { + id: rid("st"), + type: "Run", + title: "Track Tuesday — 6x800", + started_at: ago(8 * DAY + 7 * HOUR), + distance_m: 9_600, + duration_s: 2_810, + elev_m: 12, + start_lat: 37.7311, + start_lng: -122.447, + }, + { + id: rid("st"), + type: "Ride", + title: "Sausalito ferry loop", + started_at: ago(11 * DAY + 9 * HOUR), + distance_m: 52_400, + duration_s: 9_120, + elev_m: 487, + start_lat: 37.8086, + start_lng: -122.4108, + }, + { + id: rid("st"), + type: "Run", + title: "Recovery — Bernal Heights", + started_at: ago(13 * DAY + 8 * HOUR), + distance_m: 4_700, + duration_s: 1_620, + elev_m: 91, + start_lat: 37.743, + start_lng: -122.414, + }, + { + id: rid("st"), + type: "Run", + title: "Hill repeats — Sanchez stairs", + started_at: ago(15 * DAY + 7 * HOUR), + distance_m: 6_800, + duration_s: 2_280, + elev_m: 312, + start_lat: 37.751, + start_lng: -122.4291, + }, + { + id: rid("st"), + type: "Ride", + title: "GG Park to Ocean Beach", + started_at: ago(18 * DAY + 10 * HOUR), + distance_m: 14_200, + duration_s: 2_640, + elev_m: 78, + start_lat: 37.7694, + start_lng: -122.4862, + }, + { + id: rid("st"), + type: "Run", + title: "Crissy Field flat", + started_at: ago(22 * DAY + 7 * HOUR), + distance_m: 7_300, + duration_s: 2_460, + elev_m: 22, + start_lat: 37.803, + start_lng: -122.466, + }, + { + id: rid("st"), + type: "Run", + title: "Long — Presidio loop", + started_at: ago(26 * DAY + 8 * HOUR), + distance_m: 16_400, + duration_s: 5_700, + elev_m: 198, + start_lat: 37.7989, + start_lng: -122.4662, + }, ]; // PHOTOS ──────────────────────────────────────────────────────────────── const photosFields = [ - { name: "id", type: "id", granted: true }, - { name: "taken_at", type: "timestamp", granted: true }, - { name: "caption", type: "text", granted: true }, - { name: "thumbnail", type: "blob", granted: true, media_type: "image/jpeg" }, - { name: "lat", type: "number", granted: true }, - { name: "lng", type: "number", granted: true }, - { name: "camera", type: "text", granted: false, redacted_reason: "EXIF stripped — out of scope" }, - { name: "people", type: "person[]", granted: false, redacted_reason: "face-detection metadata not in grant" }, + { name: "id", type: "id", granted: true }, + { name: "taken_at", type: "timestamp", granted: true }, + { name: "caption", type: "text", granted: true }, + { name: "thumbnail", type: "blob", granted: true, media_type: "image/jpeg" }, + { name: "lat", type: "number", granted: true }, + { name: "lng", type: "number", granted: true }, + { name: "camera", type: "text", granted: false, redacted_reason: "EXIF stripped — out of scope" }, + { name: "people", type: "person[]", granted: false, redacted_reason: "face-detection metadata not in grant" }, ]; const photosViews = ["all", "this_month", "starred", "by_place"]; const photosRecords = [ - { id: rid("ph"), taken_at: ago(6 * HOUR), caption: "morning light, kitchen", thumbnail: "https://picsum.photos/seed/morn-light/600", lat: 37.7599, lng: -122.4147 }, - { id: rid("ph"), taken_at: ago(1 * DAY + 4*HOUR), caption: "trail above Glen Park", thumbnail: "https://picsum.photos/seed/glen-trail/600", lat: 37.7390, lng: -122.4408 }, - { id: rid("ph"), taken_at: ago(2 * DAY + 8*HOUR), caption: "the green egg", thumbnail: "https://picsum.photos/seed/green-egg/600", lat: 39.5296, lng: -119.8138 }, - { id: rid("ph"), taken_at: ago(3 * DAY), caption: "Jordan's whiteboard sketch", thumbnail: "https://picsum.photos/seed/whiteboard/600", lat: 37.7794, lng: -122.4078 }, - { id: rid("ph"), taken_at: ago(4 * DAY + 7*HOUR), caption: "fog rolling in, twin peaks", thumbnail: "https://picsum.photos/seed/fog-tp/600", lat: 37.7544, lng: -122.4477 }, - { id: rid("ph"), taken_at: ago(6 * DAY), caption: "espresso, ritual", thumbnail: "https://picsum.photos/seed/espresso/600", lat: 37.7765, lng: -122.4243 }, - { id: rid("ph"), taken_at: ago(8 * DAY), caption: "bike against the wall", thumbnail: "https://picsum.photos/seed/bike-wall/600", lat: 37.7599, lng: -122.4147 }, - { id: rid("ph"), taken_at: ago(11 * DAY + 9*HOUR), caption: "sausalito, mid ferry", thumbnail: "https://picsum.photos/seed/ferry/600", lat: 37.8590, lng: -122.4853 }, - { id: rid("ph"), taken_at: ago(14 * DAY), caption: "library window", thumbnail: "https://picsum.photos/seed/library/600", lat: 37.7793, lng: -122.4162 }, - { id: rid("ph"), taken_at: ago(17 * DAY), caption: "wet hydrant, Castro", thumbnail: "https://picsum.photos/seed/hydrant/600", lat: 37.7609, lng: -122.4351 }, - { id: rid("ph"), taken_at: ago(20 * DAY), caption: "yellow door, Mission", thumbnail: "https://picsum.photos/seed/yellow-door/600", lat: 37.7595, lng: -122.4148 }, - { id: rid("ph"), taken_at: ago(25 * DAY), caption: "old MUNI signal", thumbnail: "https://picsum.photos/seed/muni-sig/600", lat: 37.7707, lng: -122.4316 }, + { + id: rid("ph"), + taken_at: ago(6 * HOUR), + caption: "morning light, kitchen", + thumbnail: "https://picsum.photos/seed/morn-light/600", + lat: 37.7599, + lng: -122.4147, + }, + { + id: rid("ph"), + taken_at: ago(1 * DAY + 4 * HOUR), + caption: "trail above Glen Park", + thumbnail: "https://picsum.photos/seed/glen-trail/600", + lat: 37.739, + lng: -122.4408, + }, + { + id: rid("ph"), + taken_at: ago(2 * DAY + 8 * HOUR), + caption: "the green egg", + thumbnail: "https://picsum.photos/seed/green-egg/600", + lat: 39.5296, + lng: -119.8138, + }, + { + id: rid("ph"), + taken_at: ago(3 * DAY), + caption: "Jordan's whiteboard sketch", + thumbnail: "https://picsum.photos/seed/whiteboard/600", + lat: 37.7794, + lng: -122.4078, + }, + { + id: rid("ph"), + taken_at: ago(4 * DAY + 7 * HOUR), + caption: "fog rolling in, twin peaks", + thumbnail: "https://picsum.photos/seed/fog-tp/600", + lat: 37.7544, + lng: -122.4477, + }, + { + id: rid("ph"), + taken_at: ago(6 * DAY), + caption: "espresso, ritual", + thumbnail: "https://picsum.photos/seed/espresso/600", + lat: 37.7765, + lng: -122.4243, + }, + { + id: rid("ph"), + taken_at: ago(8 * DAY), + caption: "bike against the wall", + thumbnail: "https://picsum.photos/seed/bike-wall/600", + lat: 37.7599, + lng: -122.4147, + }, + { + id: rid("ph"), + taken_at: ago(11 * DAY + 9 * HOUR), + caption: "sausalito, mid ferry", + thumbnail: "https://picsum.photos/seed/ferry/600", + lat: 37.859, + lng: -122.4853, + }, + { + id: rid("ph"), + taken_at: ago(14 * DAY), + caption: "library window", + thumbnail: "https://picsum.photos/seed/library/600", + lat: 37.7793, + lng: -122.4162, + }, + { + id: rid("ph"), + taken_at: ago(17 * DAY), + caption: "wet hydrant, Castro", + thumbnail: "https://picsum.photos/seed/hydrant/600", + lat: 37.7609, + lng: -122.4351, + }, + { + id: rid("ph"), + taken_at: ago(20 * DAY), + caption: "yellow door, Mission", + thumbnail: "https://picsum.photos/seed/yellow-door/600", + lat: 37.7595, + lng: -122.4148, + }, + { + id: rid("ph"), + taken_at: ago(25 * DAY), + caption: "old MUNI signal", + thumbnail: "https://picsum.photos/seed/muni-sig/600", + lat: 37.7707, + lng: -122.4316, + }, ]; // OURA ────────────────────────────────────────────────────────────────── const ouraFields = [ - { name: "id", type: "id", granted: true }, - { name: "night_of", type: "timestamp", granted: true }, - { name: "score", type: "number", granted: true, unit: "0-100" }, - { name: "deep_min", type: "number", granted: true, unit: "minutes" }, - { name: "rem_min", type: "number", granted: true, unit: "minutes" }, - { name: "light_min", type: "number", granted: true, unit: "minutes" }, - { name: "hrv_ms", type: "number", granted: true, unit: "ms" }, - { name: "resting_hr", type: "number", granted: true, unit: "bpm" }, + { name: "id", type: "id", granted: true }, + { name: "night_of", type: "timestamp", granted: true }, + { name: "score", type: "number", granted: true, unit: "0-100" }, + { name: "deep_min", type: "number", granted: true, unit: "minutes" }, + { name: "rem_min", type: "number", granted: true, unit: "minutes" }, + { name: "light_min", type: "number", granted: true, unit: "minutes" }, + { name: "hrv_ms", type: "number", granted: true, unit: "ms" }, + { name: "resting_hr", type: "number", granted: true, unit: "bpm" }, ]; const ouraViews = ["all", "this_month", "low_score"]; -const ouraRecords = Array.from({length: 14}, (_, i) => ({ +const ouraRecords = Array.from({ length: 14 }, (_, i) => ({ id: rid("ou"), night_of: ago((i + 1) * DAY - 6 * HOUR), score: [82, 76, 88, 91, 71, 79, 85, 84, 67, 73, 88, 92, 80, 74][i], deep_min: [62, 51, 78, 81, 42, 58, 71, 69, 38, 47, 74, 82, 64, 49][i], - rem_min: [108, 94, 121, 134, 81, 99, 117, 115, 73, 88, 119, 128, 105, 92][i], + rem_min: [108, 94, 121, 134, 81, 99, 117, 115, 73, 88, 119, 128, 105, 92][i], light_min: [212, 198, 234, 247, 174, 201, 226, 224, 165, 188, 230, 241, 218, 196][i], - hrv_ms: [48, 42, 56, 61, 38, 44, 52, 51, 34, 41, 55, 59, 47, 43][i], + hrv_ms: [48, 42, 56, 61, 38, 44, 52, 51, 34, 41, 55, 59, 47, 43][i], resting_hr: [54, 56, 51, 50, 58, 55, 52, 53, 60, 57, 51, 50, 54, 56][i], })); // CALENDAR (iCal) ─────────────────────────────────────────────────────── const calendarFields = [ - { name: "id", type: "id", granted: true }, - { name: "title", type: "text", granted: true }, - { name: "start", type: "timestamp", granted: true }, - { name: "end", type: "timestamp", granted: true }, - { name: "location", type: "text", granted: true }, + { name: "id", type: "id", granted: true }, + { name: "title", type: "text", granted: true }, + { name: "start", type: "timestamp", granted: true }, + { name: "end", type: "timestamp", granted: true }, + { name: "location", type: "text", granted: true }, { name: "attendees", type: "person[]", granted: true }, - { name: "description", type: "text", granted: false, redacted_reason: "description excluded by grant" }, + { name: "description", type: "text", granted: false, redacted_reason: "description excluded by grant" }, ]; const calendarViews = ["upcoming", "all", "this_week"]; const calendarRecords = [ - { id: rid("cal"), title: "Standup", start: at(NOW + 14 * HOUR), end: at(NOW + 14.5 * HOUR), location: "Zoom", attendees: ["Jordan Vasquez","Maya Chen","Drew Park","the owner Rivera"] }, - { id: rid("cal"), title: "Coffee w/ Maya", start: at(NOW + 1 * DAY + 6 * HOUR), end: at(NOW + 1 * DAY + 7 * HOUR), location: "Ritual Coffee Mission", attendees: ["Maya Chen","the owner Rivera"] }, - { id: rid("cal"), title: "Portfolio review w/ Maya", start: at(NOW + 3 * DAY + 5 * HOUR), end: at(NOW + 3 * DAY + 6 * HOUR), location: "Figma SF", attendees: ["Maya Chen","the owner Rivera"] }, - { id: rid("cal"), title: "PDPP biweekly", start: at(NOW + 3 * DAY + 11 * HOUR), end: at(NOW + 3 * DAY + 11.5 * HOUR), location: "Zoom", attendees: ["Jordan Vasquez","Maya Chen","Drew Park","the owner Rivera"] }, - { id: rid("cal"), title: "Flight SFO → AUS (Southwest L4M2QP)", start: at(NOW + 7 * DAY + 6.5 * HOUR), end: at(NOW + 7 * DAY + 10 * HOUR), location: "SFO Terminal 1", attendees: [] }, - { id: rid("cal"), title: "Dentist · Dr. Mei", start: at(NOW + 9 * DAY + 9 * HOUR), end: at(NOW + 9 * DAY + 10 * HOUR), location: "1700 Castro St", attendees: [] }, + { + id: rid("cal"), + title: "Standup", + start: at(NOW + 14 * HOUR), + end: at(NOW + 14.5 * HOUR), + location: "Zoom", + attendees: ["Jordan Vasquez", "Maya Chen", "Drew Park", "the owner Rivera"], + }, + { + id: rid("cal"), + title: "Coffee w/ Maya", + start: at(NOW + 1 * DAY + 6 * HOUR), + end: at(NOW + 1 * DAY + 7 * HOUR), + location: "Ritual Coffee Mission", + attendees: ["Maya Chen", "the owner Rivera"], + }, + { + id: rid("cal"), + title: "Portfolio review w/ Maya", + start: at(NOW + 3 * DAY + 5 * HOUR), + end: at(NOW + 3 * DAY + 6 * HOUR), + location: "Figma SF", + attendees: ["Maya Chen", "the owner Rivera"], + }, + { + id: rid("cal"), + title: "PDPP biweekly", + start: at(NOW + 3 * DAY + 11 * HOUR), + end: at(NOW + 3 * DAY + 11.5 * HOUR), + location: "Zoom", + attendees: ["Jordan Vasquez", "Maya Chen", "Drew Park", "the owner Rivera"], + }, + { + id: rid("cal"), + title: "Flight SFO → AUS (Southwest L4M2QP)", + start: at(NOW + 7 * DAY + 6.5 * HOUR), + end: at(NOW + 7 * DAY + 10 * HOUR), + location: "SFO Terminal 1", + attendees: [], + }, + { + id: rid("cal"), + title: "Dentist · Dr. Mei", + start: at(NOW + 9 * DAY + 9 * HOUR), + end: at(NOW + 9 * DAY + 10 * HOUR), + location: "1700 Castro St", + attendees: [], + }, ]; // ─── deep-time synthetic records ──────────────────────────────────── @@ -331,39 +1330,248 @@ const calendarRecords = [ const Y = (year, month, day, hour = 12) => new Date(Date.UTC(year, month - 1, day, hour, 0)).toISOString(); const deepGmail = [ - { id: rid("gm"), thread_id: "th1", from: "Maya Chen ", to: ["the owner@example.com"], subject: "happy birthday old man", snippet: "hope it's a good one. drinks tomorrow at zeitgeist?", date: Y(2025, 5, 24, 9), labels: ["Inbox", "Starred"], has_attachment: false }, - { id: rid("gm"), thread_id: "th2", from: "United Airlines ", to: ["the owner@example.com"], subject: "Your flight to Tokyo on May 24 is confirmed", snippet: "SFO → NRT · UA837 · 11:40am · seat 14A", date: Y(2024, 5, 24, 7), labels: ["Inbox", "Travel"], has_attachment: true }, - { id: rid("gm"), thread_id: "th3", from: "Jordan Vasquez ", to: ["the owner@example.com"], subject: "welcome to vana", snippet: "the owner — so glad to have you on board. Here's everything you need for your first week.", date: Y(2023, 8, 14, 10), labels: ["Inbox", "Important"], has_attachment: true }, - { id: rid("gm"), thread_id: "th4", from: "Apple ", to: ["the owner@example.com"], subject: "Your iPhone 13 is ready for pickup", snippet: "Order #W4729103 · Apple Store Stockton St", date: Y(2022, 9, 24, 14), labels: ["Inbox", "Receipts"], has_attachment: false }, - { id: rid("gm"), thread_id: "th5", from: "mom ", to: ["the owner@example.com"], subject: "the new puppy!!", snippet: "we named her hattie. sending all the photos.", date: Y(2021, 4, 11, 17), labels: ["Inbox"], has_attachment: true }, - { id: rid("gm"), thread_id: "th6", from: "Square ", to: ["the owner@example.com"], subject: "Receipt from Ritual Coffee", snippet: "$4.50 · cortado · Tip $1.00", date: Y(2020, 3, 7, 9), labels: ["Inbox", "Receipts"], has_attachment: false }, - { id: rid("gm"), thread_id: "th7", from: "OkCupid ", to: ["the owner@example.com"], subject: "You have a new match", snippet: "Someone liked you back. Open the app to chat.", date: Y(2019, 7, 22, 20), labels: ["Inbox"], has_attachment: false }, - { id: rid("gm"), thread_id: "th8", from: "Stanford Alumni ", to: ["the owner@example.com"], subject: "Class of 2014 — 5-year reunion", snippet: "Save the date: October 19, 2019. Memorial Auditorium.", date: Y(2018, 11, 2, 11), labels: ["Inbox"], has_attachment: false }, + { + id: rid("gm"), + thread_id: "th1", + from: "Maya Chen ", + to: ["the owner@example.com"], + subject: "happy birthday old man", + snippet: "hope it's a good one. drinks tomorrow at zeitgeist?", + date: Y(2025, 5, 24, 9), + labels: ["Inbox", "Starred"], + has_attachment: false, + }, + { + id: rid("gm"), + thread_id: "th2", + from: "United Airlines ", + to: ["the owner@example.com"], + subject: "Your flight to Tokyo on May 24 is confirmed", + snippet: "SFO → NRT · UA837 · 11:40am · seat 14A", + date: Y(2024, 5, 24, 7), + labels: ["Inbox", "Travel"], + has_attachment: true, + }, + { + id: rid("gm"), + thread_id: "th3", + from: "Jordan Vasquez ", + to: ["the owner@example.com"], + subject: "welcome to vana", + snippet: "the owner — so glad to have you on board. Here's everything you need for your first week.", + date: Y(2023, 8, 14, 10), + labels: ["Inbox", "Important"], + has_attachment: true, + }, + { + id: rid("gm"), + thread_id: "th4", + from: "Apple ", + to: ["the owner@example.com"], + subject: "Your iPhone 13 is ready for pickup", + snippet: "Order #W4729103 · Apple Store Stockton St", + date: Y(2022, 9, 24, 14), + labels: ["Inbox", "Receipts"], + has_attachment: false, + }, + { + id: rid("gm"), + thread_id: "th5", + from: "mom ", + to: ["the owner@example.com"], + subject: "the new puppy!!", + snippet: "we named her hattie. sending all the photos.", + date: Y(2021, 4, 11, 17), + labels: ["Inbox"], + has_attachment: true, + }, + { + id: rid("gm"), + thread_id: "th6", + from: "Square ", + to: ["the owner@example.com"], + subject: "Receipt from Ritual Coffee", + snippet: "$4.50 · cortado · Tip $1.00", + date: Y(2020, 3, 7, 9), + labels: ["Inbox", "Receipts"], + has_attachment: false, + }, + { + id: rid("gm"), + thread_id: "th7", + from: "OkCupid ", + to: ["the owner@example.com"], + subject: "You have a new match", + snippet: "Someone liked you back. Open the app to chat.", + date: Y(2019, 7, 22, 20), + labels: ["Inbox"], + has_attachment: false, + }, + { + id: rid("gm"), + thread_id: "th8", + from: "Stanford Alumni ", + to: ["the owner@example.com"], + subject: "Class of 2014 — 5-year reunion", + snippet: "Save the date: October 19, 2019. Memorial Auditorium.", + date: Y(2018, 11, 2, 11), + labels: ["Inbox"], + has_attachment: false, + }, ]; const deepPhotos = [ - { id: rid("ph"), taken_at: Y(2025, 5, 24, 9), caption: "birthday morning", thumbnail: "https://picsum.photos/seed/bd25/600", lat: 37.7599, lng: -122.4147 }, - { id: rid("ph"), taken_at: Y(2024, 5, 24, 14), caption: "shinjuku at noon", thumbnail: "https://picsum.photos/seed/tokyo24/600", lat: 35.6895, lng: 139.6917 }, - { id: rid("ph"), taken_at: Y(2023, 12, 31, 22), caption: "NYE rooftop", thumbnail: "https://picsum.photos/seed/nye23/600", lat: 37.7749, lng: -122.4194 }, - { id: rid("ph"), taken_at: Y(2022, 9, 24, 15), caption: "new phone, first photo", thumbnail: "https://picsum.photos/seed/iph22/600", lat: 37.7768, lng: -122.4063 }, - { id: rid("ph"), taken_at: Y(2021, 4, 11, 18), caption: "hattie, first day home", thumbnail: "https://picsum.photos/seed/hattie/600", lat: 39.5296, lng: -119.8138 }, - { id: rid("ph"), taken_at: Y(2020, 6, 18, 19), caption: "balcony tomatoes, year 1", thumbnail: "https://picsum.photos/seed/toms/600", lat: 37.7599, lng: -122.4147 }, - { id: rid("ph"), taken_at: Y(2019, 8, 4, 16), caption: "trout, Yellowstone", thumbnail: "https://picsum.photos/seed/yst19/600", lat: 44.4280, lng: -110.5885 }, - { id: rid("ph"), taken_at: Y(2018, 6, 16, 12), caption: "graduation day", thumbnail: "https://picsum.photos/seed/grad18/600", lat: 37.4275, lng: -122.1697 }, + { + id: rid("ph"), + taken_at: Y(2025, 5, 24, 9), + caption: "birthday morning", + thumbnail: "https://picsum.photos/seed/bd25/600", + lat: 37.7599, + lng: -122.4147, + }, + { + id: rid("ph"), + taken_at: Y(2024, 5, 24, 14), + caption: "shinjuku at noon", + thumbnail: "https://picsum.photos/seed/tokyo24/600", + lat: 35.6895, + lng: 139.6917, + }, + { + id: rid("ph"), + taken_at: Y(2023, 12, 31, 22), + caption: "NYE rooftop", + thumbnail: "https://picsum.photos/seed/nye23/600", + lat: 37.7749, + lng: -122.4194, + }, + { + id: rid("ph"), + taken_at: Y(2022, 9, 24, 15), + caption: "new phone, first photo", + thumbnail: "https://picsum.photos/seed/iph22/600", + lat: 37.7768, + lng: -122.4063, + }, + { + id: rid("ph"), + taken_at: Y(2021, 4, 11, 18), + caption: "hattie, first day home", + thumbnail: "https://picsum.photos/seed/hattie/600", + lat: 39.5296, + lng: -119.8138, + }, + { + id: rid("ph"), + taken_at: Y(2020, 6, 18, 19), + caption: "balcony tomatoes, year 1", + thumbnail: "https://picsum.photos/seed/toms/600", + lat: 37.7599, + lng: -122.4147, + }, + { + id: rid("ph"), + taken_at: Y(2019, 8, 4, 16), + caption: "trout, Yellowstone", + thumbnail: "https://picsum.photos/seed/yst19/600", + lat: 44.428, + lng: -110.5885, + }, + { + id: rid("ph"), + taken_at: Y(2018, 6, 16, 12), + caption: "graduation day", + thumbnail: "https://picsum.photos/seed/grad18/600", + lat: 37.4275, + lng: -122.1697, + }, ]; const deepChase = [ - { id: rid("ch"), account_id: "•6432", posted_at: Y(2025, 5, 24, 21), merchant: "Zeitgeist", category: "Food & Drink", amount: -38.00, memo: "" }, - { id: rid("ch"), account_id: "•6432", posted_at: Y(2024, 5, 24, 11), merchant: "United Airlines", category: "Travel", amount: -1_482.00, memo: "SFO → NRT" }, - { id: rid("ch"), account_id: "•6432", posted_at: Y(2022, 9, 24, 14), merchant: "Apple Store — Stockton St", category: "Electronics", amount: -1_099.00, memo: "iPhone 13" }, - { id: rid("ch"), account_id: "•6432", posted_at: Y(2020, 6, 14, 9), merchant: "Sloat Garden Center", category: "Home", amount: -84.40, memo: "soil, tomato cages" }, - { id: rid("ch"), account_id: "•6432", posted_at: Y(2018, 8, 1, 10), merchant: "Stripe — Payroll Deposit", category: "Income", amount: 3_200.00, memo: "first paycheck" }, + { + id: rid("ch"), + account_id: "•6432", + posted_at: Y(2025, 5, 24, 21), + merchant: "Zeitgeist", + category: "Food & Drink", + amount: -38.0, + memo: "", + }, + { + id: rid("ch"), + account_id: "•6432", + posted_at: Y(2024, 5, 24, 11), + merchant: "United Airlines", + category: "Travel", + amount: -1_482.0, + memo: "SFO → NRT", + }, + { + id: rid("ch"), + account_id: "•6432", + posted_at: Y(2022, 9, 24, 14), + merchant: "Apple Store — Stockton St", + category: "Electronics", + amount: -1_099.0, + memo: "iPhone 13", + }, + { + id: rid("ch"), + account_id: "•6432", + posted_at: Y(2020, 6, 14, 9), + merchant: "Sloat Garden Center", + category: "Home", + amount: -84.4, + memo: "soil, tomato cages", + }, + { + id: rid("ch"), + account_id: "•6432", + posted_at: Y(2018, 8, 1, 10), + merchant: "Stripe — Payroll Deposit", + category: "Income", + amount: 3_200.0, + memo: "first paycheck", + }, ]; const deepGithub = [ - { id: rid("gh"), type: "PullRequest", repo: "vana-com/pdpp", title: "init repo, README", body: "Hello world.", actor: "owner", created_at: Y(2023, 9, 1, 14), url: "https://github.com/PDP-Connect/pdpp/pull/1", additions: 412, deletions: 0 }, - { id: rid("gh"), type: "Push", repo: "owner/dotfiles", title: "first commit", body: "", actor: "owner", created_at: Y(2018, 7, 14, 21), url: "", additions: 87, deletions: 0 }, - { id: rid("gh"), type: "PullRequest", repo: "owner/resource-budget", title: "0.1.0 — initial release", body: "", actor: "owner", created_at: Y(2020, 11, 19, 16), url: "", additions: 1_482, deletions: 0 }, + { + id: rid("gh"), + type: "PullRequest", + repo: "vana-com/pdpp", + title: "init repo, README", + body: "Hello world.", + actor: "owner", + created_at: Y(2023, 9, 1, 14), + url: "https://github.com/PDP-Connect/pdpp/pull/1", + additions: 412, + deletions: 0, + }, + { + id: rid("gh"), + type: "Push", + repo: "owner/dotfiles", + title: "first commit", + body: "", + actor: "owner", + created_at: Y(2018, 7, 14, 21), + url: "", + additions: 87, + deletions: 0, + }, + { + id: rid("gh"), + type: "PullRequest", + repo: "owner/resource-budget", + title: "0.1.0 — initial release", + body: "", + actor: "owner", + created_at: Y(2020, 11, 19, 16), + url: "", + additions: 1_482, + deletions: 0, + }, ]; gmailPersonalRecords.push(...deepGmail); @@ -374,72 +1582,122 @@ githubRecords.push(...deepGithub); // ─── assembled streams ───────────────────────────────────────────────── const streams = [ { - name: "messages", connector_id: "gmail", connection_id: "c_gmail_p", - title: "Gmail · personal", icon: "✉", connection_display: "the owner@example.com", - record_count: 27_359, latest_at: gmailPersonalRecords[0].date, + name: "messages", + connector_id: "gmail", + connection_id: "c_gmail_p", + title: "Gmail · personal", + icon: "✉", + connection_display: "the owner@example.com", + record_count: 27_359, + latest_at: gmailPersonalRecords[0].date, schema: { fields: gmailFields, views: gmailViews }, records: gmailPersonalRecords, }, { - name: "messages", connector_id: "gmail", connection_id: "c_gmail_w", - title: "Gmail · work", icon: "✉", connection_display: "the owner@example.org", - record_count: 8_412, latest_at: gmailWorkRecords[0].date, + name: "messages", + connector_id: "gmail", + connection_id: "c_gmail_w", + title: "Gmail · work", + icon: "✉", + connection_display: "the owner@example.org", + record_count: 8_412, + latest_at: gmailWorkRecords[0].date, schema: { fields: gmailFields, views: gmailViews }, records: gmailWorkRecords, }, { - name: "messages", connector_id: "slack", connection_id: "c_slack", - title: "Slack · vana", icon: "▤", connection_display: "acme.slack.com", - record_count: 14_028, latest_at: slackRecords[0].ts, + name: "messages", + connector_id: "slack", + connection_id: "c_slack", + title: "Slack · vana", + icon: "▤", + connection_display: "acme.slack.com", + record_count: 14_028, + latest_at: slackRecords[0].ts, schema: { fields: slackFields, views: slackViews }, records: slackRecords, }, { - name: "transactions", connector_id: "chase", connection_id: "c_chase", - title: "Chase · •6432", icon: "$", connection_display: "•6432", - record_count: 1_482, latest_at: chaseRecords[0].posted_at, + name: "transactions", + connector_id: "chase", + connection_id: "c_chase", + title: "Chase · •6432", + icon: "$", + connection_display: "•6432", + record_count: 1_482, + latest_at: chaseRecords[0].posted_at, schema: { fields: chaseFields, views: chaseViews }, records: chaseRecords, }, { - name: "orders", connector_id: "amazon", connection_id: "c_amazon", - title: "Amazon · orders", icon: "▪", connection_display: "the owner@example.com", - record_count: 2_863, latest_at: amazonRecords[0].ordered_at, + name: "orders", + connector_id: "amazon", + connection_id: "c_amazon", + title: "Amazon · orders", + icon: "▪", + connection_display: "the owner@example.com", + record_count: 2_863, + latest_at: amazonRecords[0].ordered_at, schema: { fields: amazonFields, views: amazonViews }, records: amazonRecords, }, { - name: "events", connector_id: "github", connection_id: "c_github", - title: "GitHub · owner", icon: "<>", connection_display: "owner", - record_count: 553, latest_at: githubRecords[0].created_at, + name: "events", + connector_id: "github", + connection_id: "c_github", + title: "GitHub · owner", + icon: "<>", + connection_display: "owner", + record_count: 553, + latest_at: githubRecords[0].created_at, schema: { fields: githubFields, views: githubViews }, records: githubRecords, }, { - name: "activities", connector_id: "strava", connection_id: "c_strava", - title: "Strava · activities", icon: "↗", connection_display: "the owner Rivera", - record_count: 412, latest_at: stravaRecords[0].started_at, + name: "activities", + connector_id: "strava", + connection_id: "c_strava", + title: "Strava · activities", + icon: "↗", + connection_display: "the owner Rivera", + record_count: 412, + latest_at: stravaRecords[0].started_at, schema: { fields: stravaFields, views: stravaViews }, records: stravaRecords, }, { - name: "media", connector_id: "google_takeout", connection_id: "c_photos", - title: "Photos · Takeout", icon: "□", connection_display: "the owner@example.com", - record_count: 18_240, latest_at: photosRecords[0].taken_at, + name: "media", + connector_id: "google_takeout", + connection_id: "c_photos", + title: "Photos · Takeout", + icon: "□", + connection_display: "the owner@example.com", + record_count: 18_240, + latest_at: photosRecords[0].taken_at, schema: { fields: photosFields, views: photosViews }, records: photosRecords, }, { - name: "sleep", connector_id: "oura", connection_id: "c_oura", - title: "Oura · sleep", icon: "○", connection_display: "Ring Gen3", - record_count: 421, latest_at: ouraRecords[0].night_of, + name: "sleep", + connector_id: "oura", + connection_id: "c_oura", + title: "Oura · sleep", + icon: "○", + connection_display: "Ring Gen3", + record_count: 421, + latest_at: ouraRecords[0].night_of, schema: { fields: ouraFields, views: ouraViews }, records: ouraRecords, }, { - name: "events", connector_id: "ical", connection_id: "c_ical", - title: "Calendar · personal", icon: "▭", connection_display: "personal.ics", - record_count: 312, latest_at: calendarRecords[0].start, + name: "events", + connector_id: "ical", + connection_id: "c_ical", + title: "Calendar · personal", + icon: "▭", + connection_display: "personal.ics", + record_count: 312, + latest_at: calendarRecords[0].start, schema: { fields: calendarFields, views: calendarViews }, records: calendarRecords, }, diff --git a/docs/design-system/ink-carbon/project/explorer/discover.jsx b/docs/design-system/ink-carbon/project/explorer/discover.jsx index 7ac4b704f..a1f67f5bb 100644 --- a/docs/design-system/ink-carbon/project/explorer/discover.jsx +++ b/docs/design-system/ink-carbon/project/explorer/discover.jsx @@ -14,444 +14,472 @@ * - findMemories — records from exactly N years/months ago (Apple Photos) */ -;(() => { +(() => { + const { useMemo } = React; + const { fmtRelative, fmtClock, fmtDate, fmtDay, fmtCurrency, fmtDistance, initials, NOW } = window.PDPPPrim; -const { useMemo } = React; -const { fmtRelative, fmtClock, fmtDate, fmtDay, fmtCurrency, fmtDistance, initials, NOW } = window.PDPPPrim; + // ─── helpers ───────────────────────────────────────────────────────── -// ─── helpers ───────────────────────────────────────────────────────── - -function recordTimeISO(stream, r) { - const tf = stream.schema.fields.find((f) => f.type === "timestamp")?.name - ?? stream.schema.fields.find((f) => /date|at$|night_of/i.test(f.name))?.name; - return tf ? r[tf] : null; -} + function recordTimeISO(stream, r) { + const tf = + stream.schema.fields.find((f) => f.type === "timestamp")?.name ?? + stream.schema.fields.find((f) => /date|at$|night_of/i.test(f.name))?.name; + return tf ? r[tf] : null; + } -function avatarColor(s) { - let h = 0; - for (let i = 0; i < (s ?? "").length; i++) h = (h * 31 + s.charCodeAt(i)) | 0; - return `oklch(0.55 0.12 ${Math.abs(h) % 360})`; -} + function avatarColor(s) { + let h = 0; + for (let i = 0; i < (s ?? "").length; i++) h = (h * 31 + s.charCodeAt(i)) | 0; + return `oklch(0.55 0.12 ${Math.abs(h) % 360})`; + } -function cleanPerson(s) { - if (!s) return ""; - return String(s).replace(/<[^>]+>/g, "").trim(); -} + function cleanPerson(s) { + if (!s) return ""; + return String(s) + .replace(/<[^>]+>/g, "") + .trim(); + } -// ═══════════════════════════════════════════════════════════════════════ -// Day Story — "Today across your data" -// -// References: Spotify Wrapped (story-card), Stripe Dashboard (key metrics) -// Generalized: counts records per stream that touched today; pulls headline -// numeric aggregates (sum of currency, sum of distance, average of score). -// ═══════════════════════════════════════════════════════════════════════ + // ═══════════════════════════════════════════════════════════════════════ + // Day Story — "Today across your data" + // + // References: Spotify Wrapped (story-card), Stripe Dashboard (key metrics) + // Generalized: counts records per stream that touched today; pulls headline + // numeric aggregates (sum of currency, sum of distance, average of score). + // ═══════════════════════════════════════════════════════════════════════ -function DayStory({ streams }) { - const todayStart = useMemo(() => { - const d = new Date(NOW); d.setHours(0,0,0,0); return d.getTime(); - }, []); - const todayEnd = todayStart + 86_400_000; + function DayStory({ streams }) { + const todayStart = useMemo(() => { + const d = new Date(NOW); + d.setHours(0, 0, 0, 0); + return d.getTime(); + }, []); + const todayEnd = todayStart + 86_400_000; - // Total records today - const stats = useMemo(() => { - const out = []; - for (const s of streams) { - const tf = s.schema.fields.find((f) => f.type === "timestamp")?.name; - if (!tf) continue; - const today = s.records.filter((r) => { - const t = new Date(r[tf]).getTime(); - return t >= todayStart && t < todayEnd; - }); - if (!today.length) continue; + // Total records today + const stats = useMemo(() => { + const out = []; + for (const s of streams) { + const tf = s.schema.fields.find((f) => f.type === "timestamp")?.name; + if (!tf) continue; + const today = s.records.filter((r) => { + const t = new Date(r[tf]).getTime(); + return t >= todayStart && t < todayEnd; + }); + if (!today.length) continue; - const fs = s.schema.fields; - const cur = fs.find((f) => f.type === "currency"); - const dist = fs.find((f) => /^distance/i.test(f.name)); - const dur = fs.find((f) => /^duration/i.test(f.name)); - const blob = fs.find((f) => f.type === "blob" && (f.media_type ?? "").startsWith("image/")); - const score = fs.find((f) => f.name === "score"); + const fs = s.schema.fields; + const cur = fs.find((f) => f.type === "currency"); + const dist = fs.find((f) => /^distance/i.test(f.name)); + const dur = fs.find((f) => /^duration/i.test(f.name)); + const blob = fs.find((f) => f.type === "blob" && (f.media_type ?? "").startsWith("image/")); + const score = fs.find((f) => f.name === "score"); - if (cur) { - const net = today.reduce((x, r) => x + (r[cur.name] ?? 0), 0); - out.push({ stream: s, label: net < 0 ? "spent" : "received", value: fmtCurrency(net), n: today.length }); - } else if (dist) { - const m = today.reduce((x, r) => x + (r[dist.name] ?? 0), 0); - out.push({ stream: s, label: "moved", value: fmtDistance(m), n: today.length }); - } else if (blob) { - out.push({ stream: s, label: "photo" + (today.length === 1 ? "" : "s"), value: String(today.length), n: today.length }); - } else if (score) { - const avg = today.reduce((x, r) => x + (r[score.name] ?? 0), 0) / today.length; - out.push({ stream: s, label: "score", value: avg.toFixed(0), n: today.length }); - } else { - out.push({ stream: s, label: s.name, value: String(today.length), n: today.length }); + if (cur) { + const net = today.reduce((x, r) => x + (r[cur.name] ?? 0), 0); + out.push({ stream: s, label: net < 0 ? "spent" : "received", value: fmtCurrency(net), n: today.length }); + } else if (dist) { + const m = today.reduce((x, r) => x + (r[dist.name] ?? 0), 0); + out.push({ stream: s, label: "moved", value: fmtDistance(m), n: today.length }); + } else if (blob) { + out.push({ + stream: s, + label: "photo" + (today.length === 1 ? "" : "s"), + value: String(today.length), + n: today.length, + }); + } else if (score) { + const avg = today.reduce((x, r) => x + (r[score.name] ?? 0), 0) / today.length; + out.push({ stream: s, label: "score", value: avg.toFixed(0), n: today.length }); + } else { + out.push({ stream: s, label: s.name, value: String(today.length), n: today.length }); + } } - } - return out.sort((a, b) => b.n - a.n).slice(0, 4); - }, [streams, todayStart, todayEnd]); + return out.sort((a, b) => b.n - a.n).slice(0, 4); + }, [streams, todayStart, todayEnd]); - if (!stats.length) return null; - const totalToday = stats.reduce((x, s) => x + s.n, 0); + if (!stats.length) return null; + const totalToday = stats.reduce((x, s) => x + s.n, 0); - return ( -
-
-
- {new Date(NOW).toLocaleDateString("en-US", { weekday: "long", month: "long", day: "numeric" })} -
-
- {totalToday} record{totalToday === 1 ? "" : "s"} so far + return ( +
+
+
+ {new Date(NOW).toLocaleDateString("en-US", { weekday: "long", month: "long", day: "numeric" })} +
+
+ {totalToday} record{totalToday === 1 ? "" : "s"} so far +
-
-
- {stats.map((s) => ( -
-
{s.value}
-
- {s.stream.icon} {s.label} +
+ {stats.map((s) => ( +
+
{s.value}
+
+ {s.stream.icon} {s.label} +
-
- ))} -
-
- ); -} + ))} +
+ + ); + } -// ═══════════════════════════════════════════════════════════════════════ -// Activity strip — 30-day density heatmap across ALL granted streams. -// -// References: Strava weekly summary, GitHub contribution graph. -// Generalized: any stream with a timestamp contributes; intensity is total -// record count per day. -// ═══════════════════════════════════════════════════════════════════════ + // ═══════════════════════════════════════════════════════════════════════ + // Activity strip — 30-day density heatmap across ALL granted streams. + // + // References: Strava weekly summary, GitHub contribution graph. + // Generalized: any stream with a timestamp contributes; intensity is total + // record count per day. + // ═══════════════════════════════════════════════════════════════════════ -function ActivityStrip({ streams, onPickDay }) { - const days = 30; - const counts = useMemo(() => { - const map = new Map(); - for (const s of streams) { - const tf = s.schema.fields.find((f) => f.type === "timestamp")?.name; - if (!tf) continue; - for (const r of s.records) { - const t = r[tf]; - if (!t) continue; - const d = t.slice(0, 10); - map.set(d, (map.get(d) ?? 0) + 1); + function ActivityStrip({ streams, onPickDay }) { + const days = 30; + const counts = useMemo(() => { + const map = new Map(); + for (const s of streams) { + const tf = s.schema.fields.find((f) => f.type === "timestamp")?.name; + if (!tf) continue; + for (const r of s.records) { + const t = r[tf]; + if (!t) continue; + const d = t.slice(0, 10); + map.set(d, (map.get(d) ?? 0) + 1); + } } - } - return map; - }, [streams]); - const max = Math.max(1, ...counts.values()); + return map; + }, [streams]); + const max = Math.max(1, ...counts.values()); - const cells = []; - for (let i = days - 1; i >= 0; i--) { - const d = new Date(NOW - i * 86_400_000); - const key = d.toISOString().slice(0, 10); - const v = counts.get(key) ?? 0; - const intensity = v === 0 ? 0 : 0.18 + 0.82 * (v / max); - const isToday = i === 0; - cells.push({ key, d, v, intensity, isToday }); - } + const cells = []; + for (let i = days - 1; i >= 0; i--) { + const d = new Date(NOW - i * 86_400_000); + const key = d.toISOString().slice(0, 10); + const v = counts.get(key) ?? 0; + const intensity = v === 0 ? 0 : 0.18 + 0.82 * (v / max); + const isToday = i === 0; + cells.push({ key, d, v, intensity, isToday }); + } - return ( -
-
-
last 30 days
-
- less - {[0.18, 0.4, 0.6, 0.8, 1].map((i) => ( - + return ( +
+
+
last 30 days
+
+ less + {[0.18, 0.4, 0.6, 0.8, 1].map((i) => ( + + ))} + more +
+
+
+ {cells.map(({ key, d, v, intensity, isToday }) => ( + ))} - more
-
-
- {cells.map(({ key, d, v, intensity, isToday }) => ( - - ))} -
-
- ); -} + + ); + } -// ═══════════════════════════════════════════════════════════════════════ -// Entity rails — auto-extracted facets that work for any future connector. -// -// References: Immich Faces / Places / Things. -// Generalized: scans every granted stream's schema for fields of type -// `person`/`person[]` (PeopleRail), or named `merchant`/`payee`/`seller` -// (MerchantRail), or `channel`/`channel_id` (ChannelRail). Each rail -// only renders if its underlying field exists somewhere in the grant. -// ═══════════════════════════════════════════════════════════════════════ + // ═══════════════════════════════════════════════════════════════════════ + // Entity rails — auto-extracted facets that work for any future connector. + // + // References: Immich Faces / Places / Things. + // Generalized: scans every granted stream's schema for fields of type + // `person`/`person[]` (PeopleRail), or named `merchant`/`payee`/`seller` + // (MerchantRail), or `channel`/`channel_id` (ChannelRail). Each rail + // only renders if its underlying field exists somewhere in the grant. + // ═══════════════════════════════════════════════════════════════════════ -function PeopleRail({ streams, onAddChip }) { - const { firstNameToken } = window.PDPP_QUERY; - const people = useMemo(() => { - const map = new Map(); - for (const s of streams) { - const personFields = s.schema.fields.filter((f) => f.type === "person" || f.type === "person[]"); - if (!personFields.length) continue; - for (const r of s.records) { - for (const f of personFields) { - const v = r[f.name]; - const list = Array.isArray(v) ? v : v ? [v] : []; - for (const p of list) { - const name = cleanPerson(p); - if (!name) continue; - const first = firstNameToken(p); - if (!first) continue; - if (!map.has(first)) map.set(first, { display: name, count: 0, first }); - map.get(first).count += 1; + function PeopleRail({ streams, onAddChip }) { + const { firstNameToken } = window.PDPP_QUERY; + const people = useMemo(() => { + const map = new Map(); + for (const s of streams) { + const personFields = s.schema.fields.filter((f) => f.type === "person" || f.type === "person[]"); + if (!personFields.length) continue; + for (const r of s.records) { + for (const f of personFields) { + const v = r[f.name]; + const list = Array.isArray(v) ? v : v ? [v] : []; + for (const p of list) { + const name = cleanPerson(p); + if (!name) continue; + const first = firstNameToken(p); + if (!first) continue; + if (!map.has(first)) map.set(first, { display: name, count: 0, first }); + map.get(first).count += 1; + } } } } - } - return [...map.values()].sort((a, b) => b.count - a.count).slice(0, 12); - }, [streams]); - if (!people.length) return null; - return ( - - {people.map((p) => ( - - ))} - - ); -} + return [...map.values()].sort((a, b) => b.count - a.count).slice(0, 12); + }, [streams]); + if (!people.length) return null; + return ( + + {people.map((p) => ( + + ))} + + ); + } -function MerchantRail({ streams, onAddChip }) { - const merchants = useMemo(() => { - const map = new Map(); - for (const s of streams) { - const merchField = s.schema.fields.find((f) => /merchant|payee|counterparty|seller|store/i.test(f.name))?.name; - if (!merchField) continue; - const amtField = s.schema.fields.find((f) => f.type === "currency")?.name; - for (const r of s.records) { - const m = r[merchField]; - if (!m || typeof m !== "string") continue; - if (!map.has(m)) map.set(m, { name: m, count: 0, total: 0 }); - const entry = map.get(m); - entry.count += 1; - if (amtField) entry.total += r[amtField] ?? 0; + function MerchantRail({ streams, onAddChip }) { + const merchants = useMemo(() => { + const map = new Map(); + for (const s of streams) { + const merchField = s.schema.fields.find((f) => /merchant|payee|counterparty|seller|store/i.test(f.name))?.name; + if (!merchField) continue; + const amtField = s.schema.fields.find((f) => f.type === "currency")?.name; + for (const r of s.records) { + const m = r[merchField]; + if (!m || typeof m !== "string") continue; + if (!map.has(m)) map.set(m, { name: m, count: 0, total: 0 }); + const entry = map.get(m); + entry.count += 1; + if (amtField) entry.total += r[amtField] ?? 0; + } } - } - return [...map.values()].sort((a, b) => b.count - a.count).slice(0, 10); - }, [streams]); - if (!merchants.length) return null; - return ( - - {merchants.map((m) => ( - - ))} - - ); -} + return [...map.values()].sort((a, b) => b.count - a.count).slice(0, 10); + }, [streams]); + if (!merchants.length) return null; + return ( + + {merchants.map((m) => ( + + ))} + + ); + } -function ChannelRail({ streams, onAddChip }) { - const channels = useMemo(() => { - const map = new Map(); - for (const s of streams) { - const ch = s.schema.fields.find((f) => /^channel$/i.test(f.name))?.name; - if (!ch) continue; - for (const r of s.records) { - const v = r[ch]; - if (!v) continue; - if (!map.has(v)) map.set(v, { name: v, count: 0 }); - map.get(v).count += 1; + function ChannelRail({ streams, onAddChip }) { + const channels = useMemo(() => { + const map = new Map(); + for (const s of streams) { + const ch = s.schema.fields.find((f) => /^channel$/i.test(f.name))?.name; + if (!ch) continue; + for (const r of s.records) { + const v = r[ch]; + if (!v) continue; + if (!map.has(v)) map.set(v, { name: v, count: 0 }); + map.get(v).count += 1; + } } - } - return [...map.values()].sort((a, b) => b.count - a.count).slice(0, 8); - }, [streams]); - if (!channels.length) return null; - return ( - - {channels.map((c) => ( - - ))} - - ); -} + return [...map.values()].sort((a, b) => b.count - a.count).slice(0, 8); + }, [streams]); + if (!channels.length) return null; + return ( + + {channels.map((c) => ( + + ))} + + ); + } -function Rail({ label, sub, children }) { - return ( -
-
-

{label}

- {sub} -
-
-
{children}
-
-
- ); -} + function Rail({ label, sub, children }) { + return ( +
+
+

{label}

+ {sub} +
+
+
{children}
+
+
+ ); + } -// ═══════════════════════════════════════════════════════════════════════ -// Memories — surface records from N years or months ago. -// -// Reference: Apple Photos "On this day". -// Generalized: any record with a timestamp; we look for exactly N×365 days -// ago and N×30 days ago. Returns up to 3 hits. -// ═══════════════════════════════════════════════════════════════════════ + // ═══════════════════════════════════════════════════════════════════════ + // Memories — surface records from N years or months ago. + // + // Reference: Apple Photos "On this day". + // Generalized: any record with a timestamp; we look for exactly N×365 days + // ago and N×30 days ago. Returns up to 3 hits. + // ═══════════════════════════════════════════════════════════════════════ -function findMemories(streams) { - const out = []; - const today = new Date(NOW); - const m = today.getMonth(); - const d = today.getDate(); - // Anniversaries we care about: same month/day, going back as far as data allows - const targetYears = []; - for (let yearsAgo = 1; yearsAgo <= 15; yearsAgo++) { - targetYears.push(today.getFullYear() - yearsAgo); - } - const seen = new Set(); - for (const ya of targetYears) { - const day = new Date(Date.UTC(ya, m, d)).toISOString().slice(0, 10); - for (const s of streams) { - const tf = s.schema.fields.find((f) => f.type === "timestamp")?.name; - if (!tf) continue; - for (const r of s.records) { - if (r[tf]?.slice(0, 10) === day) { - const key = `${s.connection_id}::${r.id}`; - if (seen.has(key)) continue; - seen.add(key); - const yearsAgo = today.getFullYear() - ya; - out.push({ - stream: s, - record: r, - label: yearsAgo === 1 ? "1 year ago" : `${yearsAgo} years ago`, - }); - if (out.length >= 4) return out; + function findMemories(streams) { + const out = []; + const today = new Date(NOW); + const m = today.getMonth(); + const d = today.getDate(); + // Anniversaries we care about: same month/day, going back as far as data allows + const targetYears = []; + for (let yearsAgo = 1; yearsAgo <= 15; yearsAgo++) { + targetYears.push(today.getFullYear() - yearsAgo); + } + const seen = new Set(); + for (const ya of targetYears) { + const day = new Date(Date.UTC(ya, m, d)).toISOString().slice(0, 10); + for (const s of streams) { + const tf = s.schema.fields.find((f) => f.type === "timestamp")?.name; + if (!tf) continue; + for (const r of s.records) { + if (r[tf]?.slice(0, 10) === day) { + const key = `${s.connection_id}::${r.id}`; + if (seen.has(key)) continue; + seen.add(key); + const yearsAgo = today.getFullYear() - ya; + out.push({ + stream: s, + record: r, + label: yearsAgo === 1 ? "1 year ago" : `${yearsAgo} years ago`, + }); + if (out.length >= 4) return out; + } } } } - } - // Fallback: same day-of-month last month - if (out.length < 2) { - const lastMonth = new Date(today.getFullYear(), m - 1, d).toISOString().slice(0, 10); - for (const s of streams) { - const tf = s.schema.fields.find((f) => f.type === "timestamp")?.name; - if (!tf) continue; - for (const r of s.records) { - if (r[tf]?.slice(0, 10) === lastMonth) { - out.push({ stream: s, record: r, label: "1 month ago" }); - if (out.length >= 4) return out; + // Fallback: same day-of-month last month + if (out.length < 2) { + const lastMonth = new Date(today.getFullYear(), m - 1, d).toISOString().slice(0, 10); + for (const s of streams) { + const tf = s.schema.fields.find((f) => f.type === "timestamp")?.name; + if (!tf) continue; + for (const r of s.records) { + if (r[tf]?.slice(0, 10) === lastMonth) { + out.push({ stream: s, record: r, label: "1 month ago" }); + if (out.length >= 4) return out; + } } } } + return out; } - return out; -} -// ═══════════════════════════════════════════════════════════════════════ -// YearStrip — Apple Photos "Years" zoom level -// -// One cell per year across the grant's full time horizon. Density encodes -// per-year record count. Click to add a `year:YYYY` chip. This is the -// move that makes a 20-year personal archive feel navigable. -// ═══════════════════════════════════════════════════════════════════════ + // ═══════════════════════════════════════════════════════════════════════ + // YearStrip — Apple Photos "Years" zoom level + // + // One cell per year across the grant's full time horizon. Density encodes + // per-year record count. Click to add a `year:YYYY` chip. This is the + // move that makes a 20-year personal archive feel navigable. + // ═══════════════════════════════════════════════════════════════════════ -function YearStrip({ streams, onAddChip }) { - const years = useMemo(() => { - const map = new Map(); - let earliest = null, latest = null; - for (const s of streams) { - const tf = s.schema.fields.find((f) => f.type === "timestamp")?.name; - if (!tf) continue; - for (const r of s.records) { - const t = r[tf]; - if (!t) continue; - const y = Number(t.slice(0, 4)); - if (Number.isFinite(y)) { - map.set(y, (map.get(y) ?? 0) + 1); - if (earliest == null || y < earliest) earliest = y; - if (latest == null || y > latest) latest = y; + function YearStrip({ streams, onAddChip }) { + const years = useMemo(() => { + const map = new Map(); + let earliest = null, + latest = null; + for (const s of streams) { + const tf = s.schema.fields.find((f) => f.type === "timestamp")?.name; + if (!tf) continue; + for (const r of s.records) { + const t = r[tf]; + if (!t) continue; + const y = Number(t.slice(0, 4)); + if (Number.isFinite(y)) { + map.set(y, (map.get(y) ?? 0) + 1); + if (earliest == null || y < earliest) earliest = y; + if (latest == null || y > latest) latest = y; + } } } - } - if (earliest == null) return []; - const list = []; - for (let y = earliest; y <= latest; y++) { - list.push({ year: y, count: map.get(y) ?? 0 }); - } - return list; - }, [streams]); + if (earliest == null) return []; + const list = []; + for (let y = earliest; y <= latest; y++) { + list.push({ year: y, count: map.get(y) ?? 0 }); + } + return list; + }, [streams]); - if (years.length < 2) return null; - const max = Math.max(1, ...years.map((y) => y.count)); - const total = years.reduce((x, y) => x + y.count, 0); - const span = years.length; - const thisYear = new Date(NOW).getFullYear(); + if (years.length < 2) return null; + const max = Math.max(1, ...years.map((y) => y.count)); + const total = years.reduce((x, y) => x + y.count, 0); + const span = years.length; + const thisYear = new Date(NOW).getFullYear(); - return ( -
-
-
- all time - {total.toLocaleString()} records - spans {span} year{span === 1 ? "" : "s"} · {years[0].year} → {years[years.length - 1].year} + return ( +
+
+
+ all time + {total.toLocaleString()} records + + spans {span} year{span === 1 ? "" : "s"} · {years[0].year} → {years[years.length - 1].year} + +
-
-
- {years.map(({ year, count }) => { - const intensity = count === 0 ? 0 : 0.2 + 0.8 * (count / max); - const isThis = year === thisYear; - return ( - - ); - })} -
-
- ); -} - -window.PDPP_DISCOVER = { - DayStory, ActivityStrip, PeopleRail, MerchantRail, ChannelRail, YearStrip, findMemories, -}; +
+ {years.map(({ year, count }) => { + const intensity = count === 0 ? 0 : 0.2 + 0.8 * (count / max); + const isThis = year === thisYear; + return ( + + ); + })} +
+ + ); + } + window.PDPP_DISCOVER = { + DayStory, + ActivityStrip, + PeopleRail, + MerchantRail, + ChannelRail, + YearStrip, + findMemories, + }; })(); diff --git a/docs/design-system/ink-carbon/project/explorer/dispatch.js b/docs/design-system/ink-carbon/project/explorer/dispatch.js index 81c49d54d..b57e35999 100644 --- a/docs/design-system/ink-carbon/project/explorer/dispatch.js +++ b/docs/design-system/ink-carbon/project/explorer/dispatch.js @@ -21,21 +21,28 @@ const VIEW_ORDER = [ // (ledger > conversation > gallery > calendar > map > reader > chart > // timeline > table). A stream that lights up multiple gets the most // informative default. Users can override via the view switcher. - "ledger", "conversation", "gallery", "calendar", "map", - "reader", "chart", "timeline", "table", + "ledger", + "conversation", + "gallery", + "calendar", + "map", + "reader", + "chart", + "timeline", + "table", ]; const FIELD_HINTS = { // Lexical clues used in addition to type. Always lowercased before match. - author: ["author", "from", "sender", "user", "user_id", "actor"], - body: ["body", "text", "message", "content", "snippet"], - thread: ["thread_id", "thread_ts", "channel", "channel_id", "conversation_id", "conv_id"], - geo: ["lat", "lng", "longitude", "latitude", "geo", "location", "polyline", "coords"], - amount: ["amount", "value", "price", "total"], - ts_lex: ["date", "ts", "occurred_at", "created_at", "started_at", "taken_at", "posted_at", "night_of", "ordered_at"], - start: ["start", "starts_at", "start_at", "begin"], - end: ["end", "ends_at", "end_at", "finish"], - title: ["title", "subject", "name", "headline"], + author: ["author", "from", "sender", "user", "user_id", "actor"], + body: ["body", "text", "message", "content", "snippet"], + thread: ["thread_id", "thread_ts", "channel", "channel_id", "conversation_id", "conv_id"], + geo: ["lat", "lng", "longitude", "latitude", "geo", "location", "polyline", "coords"], + amount: ["amount", "value", "price", "total"], + ts_lex: ["date", "ts", "occurred_at", "created_at", "started_at", "taken_at", "posted_at", "night_of", "ordered_at"], + start: ["start", "starts_at", "start_at", "begin"], + end: ["end", "ends_at", "end_at", "finish"], + title: ["title", "subject", "name", "headline"], }; function namesByHint(fields, hintKey) { @@ -79,7 +86,9 @@ function detect(stream) { // ─── gallery ────────────────────────────────────────────────────── // A blob field with image media type, or a clearly-named thumb/image. const blobImg = fields.find((f) => f.type === "blob" && (f.media_type ?? "").startsWith("image/")); - const namedImg = fields.find((f) => /thumb|image|photo|picture|avatar/i.test(f.name) && (f.type === "blob" || f.type === "url")); + const namedImg = fields.find( + (f) => /thumb|image|photo|picture|avatar/i.test(f.name) && (f.type === "blob" || f.type === "url") + ); if (blobImg || namedImg) declare("gallery", [(blobImg ?? namedImg).name]); // ─── ledger ────────────────────────────────────────────────────── @@ -122,9 +131,7 @@ function detect(stream) { // Temporal anchor + at least one numeric measure. We exclude obvious // non-measures (lat/lng/id-ish fields) so location streams don't // accidentally light up the chart view. - const isMeasure = (f) => - f.type === "number" && - !/^(lat|lng|longitude|latitude|id|.*_id|.*_count)$/i.test(f.name); + const isMeasure = (f) => f.type === "number" && !/^(lat|lng|longitude|latitude|id|.*_id|.*_count)$/i.test(f.name); const measures = fields.filter(isMeasure).map((f) => f.name); if (timeField && measures.length) declare("chart", [timeField, ...measures.slice(0, 3)]); diff --git a/docs/design-system/ink-carbon/project/explorer/explorer.css b/docs/design-system/ink-carbon/project/explorer/explorer.css index 19d3b6ac6..d49e747a6 100644 --- a/docs/design-system/ink-carbon/project/explorer/explorer.css +++ b/docs/design-system/ink-carbon/project/explorer/explorer.css @@ -9,9 +9,18 @@ media queries layer up to tablet & desktop. ═══════════════════════════════════════════════════════════════════════════ */ -* { box-sizing: border-box; } -html, body { margin: 0; padding: 0; } -html, body { height: 100%; } +* { + box-sizing: border-box; +} +html, +body { + margin: 0; + padding: 0; +} +html, +body { + height: 100%; +} body { background: var(--background); color: var(--foreground); @@ -19,15 +28,34 @@ body { text-rendering: optimizeLegibility; overflow-x: hidden; } -#app { min-height: 100vh; } -button, input, textarea { font: inherit; color: inherit; } -button { background: none; border: 0; padding: 0; cursor: pointer; } -input:focus { outline: none; } +#app { + min-height: 100vh; +} +button, +input, +textarea { + font: inherit; + color: inherit; +} +button { + background: none; + border: 0; + padding: 0; + cursor: pointer; +} +input:focus { + outline: none; +} -::selection { background: var(--human-wash); color: var(--human); } +::selection { + background: var(--human-wash); + color: var(--human); +} /* Scrollbar polish */ -.feed, .exp-peek, .exp-main__scroll { +.feed, +.exp-peek, +.exp-main__scroll { scrollbar-width: thin; scrollbar-color: oklch(0.85 0 0) transparent; } @@ -42,9 +70,15 @@ input:focus { outline: none; } flex-direction: column; background: var(--background); } -.exp-main { flex: 1; min-height: 0; display: flex; flex-direction: column; } +.exp-main { + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; +} .exp-main__scroll { - flex: 1; min-height: 0; + flex: 1; + min-height: 0; overflow-y: auto; -webkit-overflow-scrolling: touch; padding: 0 0 3rem; @@ -68,27 +102,54 @@ input:focus { outline: none; } border-bottom: 1px solid var(--border); flex-shrink: 0; } -.exp-topbar__brand { grid-area: brand; } -.exp-topbar__center { grid-area: search; min-width: 0; position: relative; } -.exp-topbar__right { grid-area: chrome; justify-self: end; } +.exp-topbar__brand { + grid-area: brand; +} +.exp-topbar__center { + grid-area: search; + min-width: 0; + position: relative; +} +.exp-topbar__right { + grid-area: chrome; + justify-self: end; +} /* Brand: just a tiny mark on mobile; word + tag on desktop */ .exp-topbar__brand { - display: flex; align-items: center; gap: 0.4rem; + display: flex; + align-items: center; + gap: 0.4rem; flex-shrink: 0; } -.exp-topbar__brand img { width: 22px; height: 22px; border-radius: 5px; } -.exp-topbar__brand-name { display: none; font-weight: 600; font-size: 0.95rem; letter-spacing: -0.015em; } +.exp-topbar__brand img { + width: 22px; + height: 22px; + border-radius: 5px; +} +.exp-topbar__brand-name { + display: none; + font-weight: 600; + font-size: 0.95rem; + letter-spacing: -0.015em; +} -.exp-topbar__center { min-width: 0; position: relative; } +.exp-topbar__center { + min-width: 0; + position: relative; +} .exp-topbar__right { - display: flex; align-items: center; gap: 0.4rem; + display: flex; + align-items: center; + gap: 0.4rem; flex-shrink: 0; } /* ─── Grant chip ─────────────────────────────────────────────────── */ .exp-grant-chip { - display: inline-flex; align-items: center; gap: 0.35rem; + display: inline-flex; + align-items: center; + gap: 0.35rem; padding: 0.3rem 0.5rem; border-radius: 999px; background: var(--card); @@ -98,17 +159,26 @@ input:focus { outline: none; } transition: var(--motion-state); flex-shrink: 0; } -.exp-grant-chip:hover { border-color: oklch(0.85 0 0); color: var(--foreground); } +.exp-grant-chip:hover { + border-color: oklch(0.85 0 0); + color: var(--foreground); +} .exp-grant-chip__bug { - width: 18px; height: 18px; + width: 18px; + height: 18px; border-radius: 4px; background: var(--human); color: white; - display: inline-flex; align-items: center; justify-content: center; + display: inline-flex; + align-items: center; + justify-content: center; font-family: var(--font-mono); - font-size: 9px; font-weight: 700; + font-size: 9px; + font-weight: 700; +} +.exp-grant-chip__label { + display: none; } -.exp-grant-chip__label { display: none; } .exp-grant-chip__expires { font-family: var(--font-mono); font-size: 0.66rem; @@ -125,7 +195,9 @@ input:focus { outline: none; } ═══════════════════════════════════════════════════════════════════════ */ .exp-query { - display: flex; align-items: center; flex-wrap: wrap; + display: flex; + align-items: center; + flex-wrap: wrap; gap: 0.3rem; padding: 0.3rem 0.4rem; background: var(--muted); @@ -143,7 +215,9 @@ input:focus { outline: none; } } .exp-chip { - display: inline-flex; align-items: center; gap: 0.25rem; + display: inline-flex; + align-items: center; + gap: 0.25rem; padding: 0.15rem 0.3rem 0.15rem 0.5rem; background: var(--card); border: 1px solid var(--border); @@ -152,7 +226,9 @@ input:focus { outline: none; } white-space: nowrap; transition: var(--motion-state); } -.exp-chip:hover { border-color: oklch(0.78 0 0); } +.exp-chip:hover { + border-color: oklch(0.78 0 0); +} .exp-chip__field { color: var(--muted-foreground); font-family: var(--font-mono); @@ -164,17 +240,26 @@ input:focus { outline: none; } font-size: 0.7rem; padding: 0 1px; } -.exp-chip__value { color: var(--foreground); font-weight: 500; } +.exp-chip__value { + color: var(--foreground); + font-weight: 500; +} .exp-chip__remove { - width: 16px; height: 16px; - display: inline-flex; align-items: center; justify-content: center; + width: 16px; + height: 16px; + display: inline-flex; + align-items: center; + justify-content: center; border-radius: 3px; color: var(--muted-foreground); margin-left: 0.1rem; font-size: 14px; line-height: 1; } -.exp-chip:hover .exp-chip__remove { background: var(--muted); color: var(--foreground); } +.exp-chip:hover .exp-chip__remove { + background: var(--muted); + color: var(--foreground); +} .exp-query__text { flex: 1; @@ -186,9 +271,13 @@ input:focus { outline: none; } color: var(--foreground); padding: 0.25rem 0.4rem; } -.exp-query__text::placeholder { color: oklch(0.62 0 0); } +.exp-query__text::placeholder { + color: oklch(0.62 0 0); +} -.exp-query__hint { display: none; } +.exp-query__hint { + display: none; +} .exp-query__mode { display: inline-flex; @@ -207,25 +296,42 @@ input:focus { outline: none; } padding: 0.25rem 0.4rem; border-radius: 4px; } -.exp-query__mode-btn[data-on="true"] { background: var(--foreground); color: var(--background); } -.exp-query__mode-btn:not([data-on="true"]):active { background: var(--muted); } +.exp-query__mode-btn[data-on="true"] { + background: var(--foreground); + color: var(--background); +} +.exp-query__mode-btn:not([data-on="true"]):active { + background: var(--muted); +} /* On very narrow screens, collapse search modes to single-letter pills */ @media (max-width: 519px) { - .exp-query__mode { margin-left: 0.15rem; padding-left: 0.25rem; gap: 0; } - .exp-query__mode-btn { padding: 0.2rem 0.32rem; font-size: 0.6rem; } - .exp-query__mode-btn::first-letter { /* no-op, just keeping selector reserved */ } + .exp-query__mode { + margin-left: 0.15rem; + padding-left: 0.25rem; + gap: 0; + } + .exp-query__mode-btn { + padding: 0.2rem 0.32rem; + font-size: 0.6rem; + } + .exp-query__mode-btn::first-letter { + /* no-op, just keeping selector reserved */ + } } /* Suggestion popover under the query bar */ .exp-suggest { position: absolute; - left: 0; right: 0; + left: 0; + right: 0; top: calc(100% + 0.4rem); background: var(--card); border: 1px solid var(--border); border-radius: 12px; - box-shadow: 0 16px 40px rgb(0 0 0 / 0.10), 0 4px 12px rgb(0 0 0 / 0.05); + box-shadow: + 0 16px 40px rgb(0 0 0 / 0.1), + 0 4px 12px rgb(0 0 0 / 0.05); overflow: hidden; z-index: 40; max-height: 60vh; @@ -241,11 +347,15 @@ input:focus { outline: none; } color: var(--muted-foreground); } .exp-suggest__row { - display: flex; align-items: center; gap: 0.55rem; + display: flex; + align-items: center; + gap: 0.55rem; padding: 0.5rem 0.875rem; font-size: 0.86rem; } -.exp-suggest__row[data-selected="true"] { background: var(--human-wash); } +.exp-suggest__row[data-selected="true"] { + background: var(--human-wash); +} .exp-suggest__row-kind { font-family: var(--font-mono); font-size: 0.64rem; @@ -256,13 +366,30 @@ input:focus { outline: none; } border-radius: 4px; text-transform: lowercase; } -.exp-suggest__row-label { color: var(--foreground); flex: 1; min-width: 0; } -.exp-suggest__row-hint { color: var(--muted-foreground); font-size: 0.74rem; } -.exp-suggest__divider { height: 1px; background: var(--border); margin: 0.3rem 0; } +.exp-suggest__row-label { + color: var(--foreground); + flex: 1; + min-width: 0; +} +.exp-suggest__row-hint { + color: var(--muted-foreground); + font-size: 0.74rem; +} +.exp-suggest__divider { + height: 1px; + background: var(--border); + margin: 0.3rem 0; +} @keyframes pop-in { - from { opacity: 0; transform: translateY(-4px); } - to { opacity: 1; transform: translateY(0); } + from { + opacity: 0; + transform: translateY(-4px); + } + to { + opacity: 1; + transform: translateY(0); + } } /* ═══════════════════════════════════════════════════════════════════════ @@ -281,24 +408,40 @@ input:focus { outline: none; } color: var(--muted-foreground); padding: 0.3rem 0.45rem; border-radius: 6px; - display: inline-flex; align-items: center; gap: 0.3rem; + display: inline-flex; + align-items: center; + gap: 0.3rem; transition: var(--motion-state); } .exp-views__btn[data-active="true"] { background: var(--card); color: var(--foreground); - box-shadow: 0 1px 2px rgb(0 0 0 / 0.05), 0 0 0 1px var(--border); + box-shadow: + 0 1px 2px rgb(0 0 0 / 0.05), + 0 0 0 1px var(--border); +} +.exp-views__btn[disabled] { + color: oklch(0.78 0 0); + cursor: not-allowed; +} +.exp-views__glyph { + font-size: 0.84rem; + font-family: var(--font-mono); + width: 14px; + text-align: center; +} +.exp-views__btn-label { + display: none; } -.exp-views__btn[disabled] { color: oklch(0.78 0 0); cursor: not-allowed; } -.exp-views__glyph { font-size: 0.84rem; font-family: var(--font-mono); width: 14px; text-align: center; } -.exp-views__btn-label { display: none; } /* ═══════════════════════════════════════════════════════════════════════ Facet pill strip ═══════════════════════════════════════════════════════════════════════ */ .exp-facets { - display: flex; align-items: center; gap: 0.4rem; + display: flex; + align-items: center; + gap: 0.4rem; padding: 0.45rem 0.875rem; background: var(--card); border-bottom: 1px solid var(--border); @@ -311,7 +454,9 @@ input:focus { outline: none; } white-space: nowrap; flex-shrink: 0; } -.exp-facets::-webkit-scrollbar { display: none; } +.exp-facets::-webkit-scrollbar { + display: none; +} .exp-facets__count { color: var(--foreground); @@ -319,8 +464,16 @@ input:focus { outline: none; } font-variant-numeric: tabular-nums; flex-shrink: 0; } -.exp-facets__sep { opacity: 0.4; flex-shrink: 0; } -.exp-facets__group { display: inline-flex; align-items: center; gap: 0.25rem; flex-shrink: 0; } +.exp-facets__sep { + opacity: 0.4; + flex-shrink: 0; +} +.exp-facets__group { + display: inline-flex; + align-items: center; + gap: 0.25rem; + flex-shrink: 0; +} .exp-facet { font-family: var(--font-mono); font-size: 0.74rem; @@ -332,7 +485,8 @@ input:focus { outline: none; } transition: var(--motion-state); flex-shrink: 0; } -.exp-facet:hover, .exp-facet:active { +.exp-facet:hover, +.exp-facet:active { background: var(--human-wash); border-color: oklch(0.52 0.09 45 / 0.3); color: var(--human); @@ -348,25 +502,36 @@ input:focus { outline: none; } ═══════════════════════════════════════════════════════════════════════ */ .feed { - display: flex; flex-direction: column; + display: flex; + flex-direction: column; padding: 0.5rem 0.875rem 5rem; gap: 1.5rem; max-width: 720px; margin: 0 auto; width: 100%; } -.feed__group { display: flex; flex-direction: column; gap: 0.625rem; } +.feed__group { + display: flex; + flex-direction: column; + gap: 0.625rem; +} .feed__day { - display: flex; align-items: baseline; gap: 0.6rem; + display: flex; + align-items: baseline; + gap: 0.6rem; padding: 1rem 0.25rem 0.5rem; - position: sticky; top: 0; z-index: 5; + position: sticky; + top: 0; + z-index: 5; background: var(--background); } .feed__day::after { /* Subtle fade so cards passing underneath don't pop hard */ content: ""; position: absolute; - left: 0; right: 0; bottom: -8px; + left: 0; + right: 0; + bottom: -8px; height: 8px; background: linear-gradient(to bottom, var(--background), transparent); pointer-events: none; @@ -386,34 +551,54 @@ input:focus { outline: none; } padding: 0.15rem 0.5rem; border-radius: 999px; } -.feed__cards { display: flex; flex-direction: column; gap: 0.6rem; } +.feed__cards { + display: flex; + flex-direction: column; + gap: 0.6rem; +} .feed__empty { padding: 4rem 1rem; text-align: center; color: var(--muted-foreground); } -.feed__empty-title { font-size: 1.05rem; font-weight: 500; color: var(--foreground); margin-bottom: 0.4rem; } -.feed__empty-sub { font-size: 0.86rem; font-family: var(--font-mono); } +.feed__empty-title { + font-size: 1.05rem; + font-weight: 500; + color: var(--foreground); + margin-bottom: 0.4rem; +} +.feed__empty-sub { + font-size: 0.86rem; + font-family: var(--font-mono); +} /* ═══════════════════════════════════════════════════════════════════════ Card — base ═══════════════════════════════════════════════════════════════════════ */ .card { - display: flex; flex-direction: column; + display: flex; + flex-direction: column; gap: 0.5rem; padding: 0.875rem 1rem 1rem; background: var(--card); border: 1px solid var(--border); border-radius: 14px; cursor: pointer; - transition: background var(--motion-state), border-color var(--motion-state), transform var(--motion-state); + transition: + background var(--motion-state), + border-color var(--motion-state), + transform var(--motion-state); position: relative; overflow: hidden; } -.card:hover { border-color: oklch(0.85 0 0); } -.card:active { transform: scale(0.995); } +.card:hover { + border-color: oklch(0.85 0 0); +} +.card:active { + transform: scale(0.995); +} .card[data-selected="true"] { border-color: var(--human); background: var(--human-wash); @@ -421,34 +606,52 @@ input:focus { outline: none; } .card[data-selected="true"]::before { content: ""; position: absolute; - left: 0; top: 14px; bottom: 14px; - width: 3px; background: var(--human); + left: 0; + top: 14px; + bottom: 14px; + width: 3px; + background: var(--human); border-radius: 0 2px 2px 0; } .card__eyebrow { - display: flex; align-items: center; gap: 0.4rem; + display: flex; + align-items: center; + gap: 0.4rem; font-family: var(--font-mono); font-size: 0.66rem; color: var(--muted-foreground); letter-spacing: 0.02em; } -.card__eyebrow.is-light { color: oklch(1 0 0 / 0.85); } +.card__eyebrow.is-light { + color: oklch(1 0 0 / 0.85); +} .card__eyebrow-glyph { - width: 14px; height: 14px; + width: 14px; + height: 14px; background: var(--muted); border-radius: 3px; - display: inline-flex; align-items: center; justify-content: center; - font-size: 0.7rem; color: var(--foreground); + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 0.7rem; + color: var(--foreground); flex-shrink: 0; } .card__eyebrow.is-light .card__eyebrow-glyph { background: oklch(0 0 0 / 0.35); color: oklch(1 0 0 / 0.95); } -.card__eyebrow-stream { font-weight: 500; } -.card__eyebrow-conn { opacity: 0.7; } -.card__eyebrow-time { font-variant-numeric: tabular-nums; opacity: 0.85; } +.card__eyebrow-stream { + font-weight: 500; +} +.card__eyebrow-conn { + opacity: 0.7; +} +.card__eyebrow-time { + font-variant-numeric: tabular-nums; + opacity: 0.85; +} .card__title { font-size: 1rem; @@ -461,7 +664,7 @@ input:focus { outline: none; } } .card__body { font-size: 0.9rem; - color: oklch(0.40 0 0); + color: oklch(0.4 0 0); line-height: 1.5; text-wrap: pretty; } @@ -471,11 +674,20 @@ input:focus { outline: none; } -webkit-box-orient: vertical; overflow: hidden; } -.card__body--mono { font-family: var(--font-mono); font-size: 0.82rem; } +.card__body--mono { + font-family: var(--font-mono); + font-size: 0.82rem; +} -.card__meta-row { display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap; } +.card__meta-row { + display: flex; + align-items: center; + gap: 0.4rem; + flex-wrap: wrap; +} .card__chip { - display: inline-flex; align-items: center; + display: inline-flex; + align-items: center; padding: 0.1rem 0.45rem; background: var(--muted); border-radius: 999px; @@ -490,23 +702,39 @@ input:focus { outline: none; } /* ─── Message card ────────────────────────────────────────────────── */ .card--message .card__row--message { - display: flex; align-items: center; gap: 0.6rem; + display: flex; + align-items: center; + gap: 0.6rem; } .card__avatar { - width: 30px; height: 30px; + width: 30px; + height: 30px; border-radius: 50%; - display: inline-flex; align-items: center; justify-content: center; + display: inline-flex; + align-items: center; + justify-content: center; color: white; font-family: var(--font-mono); font-size: 0.72rem; font-weight: 600; flex-shrink: 0; } -.card__col { min-width: 0; } -.card__name { font-weight: 600; font-size: 0.92rem; color: var(--foreground); } -.card__channel { font-family: var(--font-mono); font-size: 0.7rem; color: var(--muted-foreground); } +.card__col { + min-width: 0; +} +.card__name { + font-weight: 600; + font-size: 0.92rem; + color: var(--foreground); +} +.card__channel { + font-family: var(--font-mono); + font-size: 0.7rem; + color: var(--muted-foreground); +} .card__react { - display: inline-flex; gap: 0.3rem; + display: inline-flex; + gap: 0.3rem; margin-top: 0.1rem; } .card__react span { @@ -529,32 +757,53 @@ input:focus { outline: none; } line-height: 1; margin: 0.25rem 0; } -.card--money .card__amount.is-pos { color: var(--success); } -.card--money .card__title { font-size: 1.05rem; } +.card--money .card__amount.is-pos { + color: var(--success); +} +.card--money .card__title { + font-size: 1.05rem; +} /* ─── Photo card ──────────────────────────────────────────────────── */ -.card--photo { padding: 0; } -.card__photo { position: relative; display: block; } +.card--photo { + padding: 0; +} +.card__photo { + position: relative; + display: block; +} .card__photo img { - width: 100%; height: auto; - aspect-ratio: 4/3; + width: 100%; + height: auto; + aspect-ratio: 4 / 3; object-fit: cover; display: block; border-radius: 14px; } .card__photo-scrim { - position: absolute; inset: 0; - background: - linear-gradient(to bottom, oklch(0 0 0 / 0.42) 0%, transparent 28%, transparent 60%, oklch(0 0 0 / 0.55) 100%); + position: absolute; + inset: 0; + background: linear-gradient( + to bottom, + oklch(0 0 0 / 0.42) 0%, + transparent 28%, + transparent 60%, + oklch(0 0 0 / 0.55) 100% + ); border-radius: 14px; pointer-events: none; } .card__photo .card__eyebrow { - position: absolute; top: 0.7rem; left: 0.8rem; right: 0.8rem; + position: absolute; + top: 0.7rem; + left: 0.8rem; + right: 0.8rem; } .card__photo-caption { position: absolute; - left: 0.9rem; right: 0.9rem; bottom: 0.7rem; + left: 0.9rem; + right: 0.9rem; + bottom: 0.7rem; font-size: 1rem; font-weight: 600; letter-spacing: -0.01em; @@ -575,11 +824,15 @@ input:focus { outline: none; } font-weight: 500; letter-spacing: 0.01em; } -.card--event .card__title { font-size: 1.1rem; } +.card--event .card__title { + font-size: 1.1rem; +} /* ─── Activity card ───────────────────────────────────────────────── */ .card--activity .card__stats { - display: flex; gap: 1.25rem; align-items: baseline; + display: flex; + gap: 1.25rem; + align-items: baseline; margin-top: 0.2rem; } .card--activity .card__stat-value { @@ -601,15 +854,26 @@ input:focus { outline: none; } } /* ─── Reader card ─────────────────────────────────────────────────── */ -.card--reader .card__title { font-size: 1.05rem; } +.card--reader .card__title { + font-size: 1.05rem; +} /* ─── Location card ───────────────────────────────────────────────── */ -.card--location .card__body--mono { color: var(--muted-foreground); } +.card--location .card__body--mono { + color: var(--muted-foreground); +} /* ─── Generic card ────────────────────────────────────────────────── */ -.card__kv-list { display: flex; flex-direction: column; gap: 0.2rem; margin-top: 0.2rem; } +.card__kv-list { + display: flex; + flex-direction: column; + gap: 0.2rem; + margin-top: 0.2rem; +} .card__kv { - display: grid; grid-template-columns: 110px 1fr; gap: 0.6rem; + display: grid; + grid-template-columns: 110px 1fr; + gap: 0.6rem; font-size: 0.8rem; } .card__kv-k { @@ -619,7 +883,9 @@ input:focus { outline: none; } } .card__kv-v { color: var(--foreground); - overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } /* ═══════════════════════════════════════════════════════════════════════ @@ -627,7 +893,8 @@ input:focus { outline: none; } ═══════════════════════════════════════════════════════════════════════ */ .exp-peek-backdrop { - position: fixed; inset: 0; + position: fixed; + inset: 0; background: oklch(0 0 0 / 0.25); z-index: 60; animation: fade-in 0.16s ease-out; @@ -640,7 +907,8 @@ input:focus { outline: none; } border-top-left-radius: 18px; border-top-right-radius: 18px; z-index: 70; - display: flex; flex-direction: column; + display: flex; + flex-direction: column; box-shadow: 0 -12px 40px rgb(0 0 0 / 0.12); animation: slide-up 0.2s var(--ease-enter); overflow: hidden; @@ -648,23 +916,32 @@ input:focus { outline: none; } .exp-peek::before { content: ""; display: block; - width: 36px; height: 4px; + width: 36px; + height: 4px; background: oklch(0.85 0 0); border-radius: 999px; margin: 0.5rem auto 0; flex-shrink: 0; } .exp-peek__close { - position: absolute; top: 0.7rem; right: 0.7rem; + position: absolute; + top: 0.7rem; + right: 0.7rem; font-size: 1.3rem; color: var(--muted-foreground); - width: 32px; height: 32px; - display: inline-flex; align-items: center; justify-content: center; + width: 32px; + height: 32px; + display: inline-flex; + align-items: center; + justify-content: center; border-radius: 999px; background: oklch(0.97 0.002 95); z-index: 2; } -.exp-peek__close:hover { background: var(--muted); color: var(--foreground); } +.exp-peek__close:hover { + background: var(--muted); + color: var(--foreground); +} .exp-peek__head { padding: 0.875rem 1.1rem 0.75rem; @@ -672,7 +949,9 @@ input:focus { outline: none; } flex-shrink: 0; } .exp-peek__eyebrow { - display: inline-flex; align-items: center; gap: 0.4rem; + display: inline-flex; + align-items: center; + gap: 0.4rem; font-family: var(--font-mono); font-size: 0.66rem; letter-spacing: 0.06em; @@ -680,10 +959,13 @@ input:focus { outline: none; } margin-bottom: 0.5rem; } .exp-peek__eyebrow-glyph { - width: 14px; height: 14px; + width: 14px; + height: 14px; background: var(--muted); border-radius: 3px; - display: inline-flex; align-items: center; justify-content: center; + display: inline-flex; + align-items: center; + justify-content: center; font-size: 0.66rem; } .exp-peek__title { @@ -693,29 +975,46 @@ input:focus { outline: none; } margin: 0 2rem 0.5rem 0; line-height: 1.4; } -.exp-peek__meta { font-family: var(--font-mono); font-size: 0.7rem; color: var(--muted-foreground); } -.exp-peek__meta b { color: var(--foreground); font-weight: 500; } +.exp-peek__meta { + font-family: var(--font-mono); + font-size: 0.7rem; + color: var(--muted-foreground); +} +.exp-peek__meta b { + color: var(--foreground); + font-weight: 500; +} .exp-peek__body { padding: 0.875rem 1.1rem 2.5rem; overflow-y: auto; - flex: 1; min-height: 0; + flex: 1; + min-height: 0; } .exp-peek__field { - display: grid; grid-template-columns: 110px 1fr; + display: grid; + grid-template-columns: 110px 1fr; gap: 0.75rem; padding: 0.5rem 0; border-bottom: 1px solid oklch(0.96 0.002 95); font-size: 0.84rem; } -.exp-peek__field:last-child { border-bottom: none; } +.exp-peek__field:last-child { + border-bottom: none; +} .exp-peek__field-name { font-family: var(--font-mono); font-size: 0.7rem; color: var(--muted-foreground); } -.exp-peek__field-value { color: var(--foreground); line-height: 1.5; word-break: break-word; } -.exp-peek__field-value.long { white-space: pre-wrap; } +.exp-peek__field-value { + color: var(--foreground); + line-height: 1.5; + word-break: break-word; +} +.exp-peek__field-value.long { + white-space: pre-wrap; +} .exp-peek__field[data-redacted="true"] .exp-peek__field-name { color: oklch(0.75 0 0); text-decoration: line-through; @@ -738,7 +1037,10 @@ input:focus { outline: none; } line-height: 1.55; white-space: pre-wrap; } -.exp-peek__source b { color: var(--primary); font-weight: 500; } +.exp-peek__source b { + color: var(--primary); + font-weight: 500; +} /* ═══════════════════════════════════════════════════════════════════════ Why-this-view popover @@ -746,35 +1048,58 @@ input:focus { outline: none; } .exp-why { position: fixed; - top: 64px; right: 0.875rem; + top: 64px; + right: 0.875rem; background: var(--card); border: 1px solid var(--border); border-radius: 12px; - box-shadow: 0 16px 40px rgb(0 0 0 / 0.10); + box-shadow: 0 16px 40px rgb(0 0 0 / 0.1); padding: 0.875rem 1rem; z-index: 40; width: min(360px, calc(100vw - 1.75rem)); font-size: 0.78rem; animation: pop-in 0.16s var(--ease-enter); } -.exp-why__title { font-size: 0.82rem; font-weight: 600; margin-bottom: 0.3rem; } -.exp-why__sub { color: var(--muted-foreground); font-size: 0.72rem; margin-bottom: 0.7rem; } +.exp-why__title { + font-size: 0.82rem; + font-weight: 600; + margin-bottom: 0.3rem; +} +.exp-why__sub { + color: var(--muted-foreground); + font-size: 0.72rem; + margin-bottom: 0.7rem; +} .exp-why__row { - display: flex; align-items: baseline; gap: 0.5rem; + display: flex; + align-items: baseline; + gap: 0.5rem; font-size: 0.76rem; padding: 0.2rem 0; } -.exp-why__row-cap { font-family: var(--font-mono); color: var(--foreground); min-width: 92px; } -.exp-why__row-fields { font-family: var(--font-mono); color: var(--muted-foreground); font-size: 0.7rem; } -.exp-why__row[data-active="true"] .exp-why__row-cap { color: var(--human); } -.exp-why__row[data-active="true"] .exp-why__row-fields { color: var(--foreground); } - -kbd.exp-kbd { +.exp-why__row-cap { font-family: var(--font-mono); - font-size: 0.66rem; - padding: 1px 5px; - border: 1px solid var(--border); - border-radius: 4px; + color: var(--foreground); + min-width: 92px; +} +.exp-why__row-fields { + font-family: var(--font-mono); + color: var(--muted-foreground); + font-size: 0.7rem; +} +.exp-why__row[data-active="true"] .exp-why__row-cap { + color: var(--human); +} +.exp-why__row[data-active="true"] .exp-why__row-fields { + color: var(--foreground); +} + +kbd.exp-kbd { + font-family: var(--font-mono); + font-size: 0.66rem; + padding: 1px 5px; + border: 1px solid var(--border); + border-radius: 4px; background: var(--card); color: var(--muted-foreground); } @@ -784,7 +1109,11 @@ kbd.exp-kbd { These layouts preserve the v1 styles, retuned for mobile. ═══════════════════════════════════════════════════════════════════════ */ -.view-container { max-width: 920px; margin: 0 auto; padding: 0.5rem 0.875rem 5rem; } +.view-container { + max-width: 920px; + margin: 0 auto; + padding: 0.5rem 0.875rem 5rem; +} /* Tables */ .exp-table { @@ -813,29 +1142,54 @@ kbd.exp-kbd { border-bottom: 1px solid oklch(0.96 0.002 95); vertical-align: top; } -.exp-table tr:last-child td { border-bottom: none; } -.exp-table tr { cursor: pointer; } -.exp-table tr:hover td { background: oklch(0.985 0.002 95); } -.exp-table tr[data-selected="true"] td { background: var(--human-wash); } -.exp-table .mono { font-family: var(--font-mono); font-size: 0.76rem; color: var(--muted-foreground); } -.exp-table .num { font-variant-numeric: tabular-nums; } -.exp-table .pos { color: var(--success); } -.exp-table .truncate { max-width: 320px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.exp-table tr:last-child td { + border-bottom: none; +} +.exp-table tr { + cursor: pointer; +} +.exp-table tr:hover td { + background: oklch(0.985 0.002 95); +} +.exp-table tr[data-selected="true"] td { + background: var(--human-wash); +} +.exp-table .mono { + font-family: var(--font-mono); + font-size: 0.76rem; + color: var(--muted-foreground); +} +.exp-table .num { + font-variant-numeric: tabular-nums; +} +.exp-table .pos { + color: var(--success); +} +.exp-table .truncate { + max-width: 320px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} /* Conversation */ .exp-conv { - display: grid; grid-template-columns: 1fr; + display: grid; + grid-template-columns: 1fr; gap: 0.75rem; } .exp-conv__channels { - display: flex; flex-direction: row; + display: flex; + flex-direction: row; overflow-x: auto; border-bottom: 1px solid var(--border); padding: 0 0 0.5rem 0; gap: 0.25rem; scrollbar-width: none; } -.exp-conv__channels::-webkit-scrollbar { display: none; } +.exp-conv__channels::-webkit-scrollbar { + display: none; +} .exp-conv__channel { white-space: nowrap; padding: 0.35rem 0.7rem; @@ -847,14 +1201,23 @@ kbd.exp-kbd { flex-shrink: 0; border: 1px solid transparent; } -.exp-conv__channel:hover { color: var(--foreground); } +.exp-conv__channel:hover { + color: var(--foreground); +} .exp-conv__channel[data-active="true"] { background: var(--card); color: var(--human); border-color: oklch(0.52 0.09 45 / 0.3); } -.exp-conv__channel-count { opacity: 0.7; margin-left: 0.3rem; } -.exp-conv__list { display: flex; flex-direction: column; gap: 0.5rem; } +.exp-conv__channel-count { + opacity: 0.7; + margin-left: 0.3rem; +} +.exp-conv__list { + display: flex; + flex-direction: column; + gap: 0.5rem; +} .exp-conv__msg { display: grid; grid-template-columns: 32px minmax(0, 1fr); @@ -865,11 +1228,19 @@ kbd.exp-kbd { background: var(--card); border: 1px solid var(--border); } -.exp-conv__msg:hover { border-color: oklch(0.85 0 0); } -.exp-conv__msg[data-selected="true"] { background: var(--human-wash); border-color: var(--human); } +.exp-conv__msg:hover { + border-color: oklch(0.85 0 0); +} +.exp-conv__msg[data-selected="true"] { + background: var(--human-wash); + border-color: var(--human); +} .exp-conv__avatar { - width: 30px; height: 30px; - display: inline-flex; align-items: center; justify-content: center; + width: 30px; + height: 30px; + display: inline-flex; + align-items: center; + justify-content: center; border-radius: 50%; background: var(--human); color: white; @@ -878,14 +1249,37 @@ kbd.exp-kbd { font-family: var(--font-mono); } .exp-conv__head { - display: flex; align-items: baseline; gap: 0.5rem; margin-bottom: 0.2rem; - font-size: 0.86rem; flex-wrap: wrap; -} -.exp-conv__head-name { font-weight: 600; } -.exp-conv__head-time { font-size: 0.7rem; color: var(--muted-foreground); font-family: var(--font-mono); } -.exp-conv__head-channel { font-size: 0.7rem; color: var(--muted-foreground); font-family: var(--font-mono); } -.exp-conv__text { font-size: 0.88rem; color: var(--foreground); line-height: 1.5; } -.exp-conv__react { display: inline-flex; gap: 0.3rem; margin-top: 0.3rem; flex-wrap: wrap; } + display: flex; + align-items: baseline; + gap: 0.5rem; + margin-bottom: 0.2rem; + font-size: 0.86rem; + flex-wrap: wrap; +} +.exp-conv__head-name { + font-weight: 600; +} +.exp-conv__head-time { + font-size: 0.7rem; + color: var(--muted-foreground); + font-family: var(--font-mono); +} +.exp-conv__head-channel { + font-size: 0.7rem; + color: var(--muted-foreground); + font-family: var(--font-mono); +} +.exp-conv__text { + font-size: 0.88rem; + color: var(--foreground); + line-height: 1.5; +} +.exp-conv__react { + display: inline-flex; + gap: 0.3rem; + margin-top: 0.3rem; + flex-wrap: wrap; +} .exp-conv__react span { font-size: 0.72rem; padding: 0.05rem 0.45rem; @@ -896,31 +1290,54 @@ kbd.exp-kbd { } /* Ledger */ -.exp-ledger { display: grid; grid-template-columns: 1fr; gap: 1.25rem; } +.exp-ledger { + display: grid; + grid-template-columns: 1fr; + gap: 1.25rem; +} .exp-ledger__month-strip { - display: flex; gap: 1px; + display: flex; + gap: 1px; border: 1px solid var(--border); border-radius: 10px; overflow-x: auto; background: var(--border); scrollbar-width: none; } -.exp-ledger__month-strip::-webkit-scrollbar { display: none; } +.exp-ledger__month-strip::-webkit-scrollbar { + display: none; +} .exp-ledger__month { background: var(--card); padding: 0.55rem 0.8rem; font-family: var(--font-mono); font-size: 0.72rem; color: var(--muted-foreground); - display: flex; flex-direction: column; gap: 0.15rem; align-items: flex-start; + display: flex; + flex-direction: column; + gap: 0.15rem; + align-items: flex-start; flex-shrink: 0; min-width: 92px; } -.exp-ledger__month[data-active="true"] { background: var(--human-wash); color: var(--human); } -.exp-ledger__month-label { font-size: 0.64rem; letter-spacing: 0.1em; text-transform: uppercase; } -.exp-ledger__month-amount { font-size: 0.84rem; color: var(--foreground); font-weight: 500; font-variant-numeric: tabular-nums; } +.exp-ledger__month[data-active="true"] { + background: var(--human-wash); + color: var(--human); +} +.exp-ledger__month-label { + font-size: 0.64rem; + letter-spacing: 0.1em; + text-transform: uppercase; +} +.exp-ledger__month-amount { + font-size: 0.84rem; + color: var(--foreground); + font-weight: 500; + font-variant-numeric: tabular-nums; +} .exp-ledger__rows { - display: flex; flex-direction: column; + display: flex; + flex-direction: column; border: 1px solid var(--border); border-radius: 12px; background: var(--card); @@ -936,12 +1353,33 @@ kbd.exp-kbd { padding: 0.75rem 1rem; border-bottom: 1px solid oklch(0.96 0.002 95); } -.exp-ledger__row:last-child { border-bottom: none; } -.exp-ledger__row:hover { background: oklch(0.985 0.002 95); } -.exp-ledger__row[data-selected="true"] { background: var(--human-wash); } -.exp-ledger__row-date { grid-area: meta; font-family: var(--font-mono); font-size: 0.7rem; color: var(--muted-foreground); } -.exp-ledger__row-merchant { grid-area: title; font-size: 0.92rem; font-weight: 500; } -.exp-ledger__row-merchant small { display: block; font-size: 0.72rem; color: var(--muted-foreground); font-weight: 400; margin-top: 0.1rem; } +.exp-ledger__row:last-child { + border-bottom: none; +} +.exp-ledger__row:hover { + background: oklch(0.985 0.002 95); +} +.exp-ledger__row[data-selected="true"] { + background: var(--human-wash); +} +.exp-ledger__row-date { + grid-area: meta; + font-family: var(--font-mono); + font-size: 0.7rem; + color: var(--muted-foreground); +} +.exp-ledger__row-merchant { + grid-area: title; + font-size: 0.92rem; + font-weight: 500; +} +.exp-ledger__row-merchant small { + display: block; + font-size: 0.72rem; + color: var(--muted-foreground); + font-weight: 400; + margin-top: 0.1rem; +} .exp-ledger__row-cat { grid-area: meta; justify-self: start; @@ -950,7 +1388,10 @@ kbd.exp-kbd { color: var(--muted-foreground); margin-left: 0.6rem; } -.exp-ledger__row-cat::before { content: "· "; opacity: 0.5; } +.exp-ledger__row-cat::before { + content: "· "; + opacity: 0.5; +} .exp-ledger__row-amount { grid-area: amount; align-self: start; @@ -960,7 +1401,9 @@ kbd.exp-kbd { font-variant-numeric: tabular-nums; font-weight: 600; } -.exp-ledger__row-amount.pos { color: var(--success); } +.exp-ledger__row-amount.pos { + color: var(--success); +} .exp-ledger__side { border: 1px solid var(--border); border-radius: 12px; @@ -976,29 +1419,51 @@ kbd.exp-kbd { margin-bottom: 0.6rem; } .exp-ledger__side-row { - display: flex; justify-content: space-between; align-items: baseline; gap: 0.5rem; - padding: 0.35rem 0; font-size: 0.82rem; + display: flex; + justify-content: space-between; + align-items: baseline; + gap: 0.5rem; + padding: 0.35rem 0; + font-size: 0.82rem; +} +.exp-ledger__side-row-cat { + color: var(--foreground); + flex-shrink: 0; } -.exp-ledger__side-row-cat { color: var(--foreground); flex-shrink: 0; } .exp-ledger__side-row-amt { - font-family: var(--font-mono); color: var(--muted-foreground); + font-family: var(--font-mono); + color: var(--muted-foreground); font-variant-numeric: tabular-nums; } .exp-ledger__side-bar { - flex: 1; height: 4px; margin: 0 0.6rem; - background: var(--muted); border-radius: 999px; position: relative; + flex: 1; + height: 4px; + margin: 0 0.6rem; + background: var(--muted); + border-radius: 999px; + position: relative; } .exp-ledger__side-bar::after { - content: ""; position: absolute; inset: 0 auto 0 0; - width: var(--pct, 0%); background: var(--foreground); border-radius: 999px; + content: ""; + position: absolute; + inset: 0 auto 0 0; + width: var(--pct, 0%); + background: var(--foreground); + border-radius: 999px; } /* Timeline (single stream) */ -.exp-tl { display: grid; grid-template-columns: 1fr; } +.exp-tl { + display: grid; + grid-template-columns: 1fr; +} .exp-tl__list { - display: flex; flex-direction: column; gap: 0.3rem; + display: flex; + flex-direction: column; + gap: 0.3rem; border-left: 2px solid var(--border); - padding-left: 1.25rem; padding-bottom: 3rem; + padding-left: 1.25rem; + padding-bottom: 3rem; } .exp-tl__day-label { font-family: var(--font-mono); @@ -1009,27 +1474,52 @@ kbd.exp-kbd { position: relative; } .exp-tl__day-label::before { - content: ""; position: absolute; left: -5px; top: 50%; - width: 8px; height: 8px; border-radius: 50%; - background: var(--card); border: 2px solid var(--foreground); + content: ""; + position: absolute; + left: -5px; + top: 50%; + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--card); + border: 2px solid var(--foreground); transform: translateY(-50%); } .exp-tl__row { - display: grid; grid-template-columns: 64px 1fr; + display: grid; + grid-template-columns: 64px 1fr; gap: 0.8rem; padding: 0.45rem 0.5rem; border-radius: 6px; } -.exp-tl__row:hover { background: oklch(0.985 0.002 95); } -.exp-tl__row[data-selected="true"] { background: var(--human-wash); } -.exp-tl__time { font-family: var(--font-mono); font-size: 0.72rem; color: var(--muted-foreground); padding-top: 0.1rem; } -.exp-tl__content { font-size: 0.86rem; } -.exp-tl__content b { font-weight: 500; } +.exp-tl__row:hover { + background: oklch(0.985 0.002 95); +} +.exp-tl__row[data-selected="true"] { + background: var(--human-wash); +} +.exp-tl__time { + font-family: var(--font-mono); + font-size: 0.72rem; + color: var(--muted-foreground); + padding-top: 0.1rem; +} +.exp-tl__content { + font-size: 0.86rem; +} +.exp-tl__content b { + font-weight: 500; +} .exp-tl__content small { - display: block; font-size: 0.72rem; color: var(--muted-foreground); - margin-top: 0.15rem; font-family: var(--font-mono); + display: block; + font-size: 0.72rem; + color: var(--muted-foreground); + margin-top: 0.15rem; + font-family: var(--font-mono); +} +.exp-tl__scrubber { + display: none; } -.exp-tl__scrubber { display: none; } /* Gallery */ .exp-gal { @@ -1045,16 +1535,31 @@ kbd.exp-kbd { aspect-ratio: 1; cursor: pointer; } -.exp-gal__item[data-selected="true"] { outline: 2px solid var(--human); outline-offset: 2px; } -.exp-gal__item img { width: 100%; height: 100%; object-fit: cover; display: block; } +.exp-gal__item[data-selected="true"] { + outline: 2px solid var(--human); + outline-offset: 2px; +} +.exp-gal__item img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} .exp-gal__cap { - position: absolute; left: 6px; right: 6px; bottom: 4px; - font-size: 0.7rem; color: white; + position: absolute; + left: 6px; + right: 6px; + bottom: 4px; + font-size: 0.7rem; + color: white; text-shadow: 0 1px 3px rgb(0 0 0 / 0.6); - font-weight: 500; opacity: 0; + font-weight: 500; + opacity: 0; transition: opacity var(--motion-state); } -.exp-gal__item:hover .exp-gal__cap { opacity: 1; } +.exp-gal__item:hover .exp-gal__cap { + opacity: 1; +} /* Map */ .exp-map { @@ -1065,20 +1570,34 @@ kbd.exp-kbd { border-radius: 12px; overflow: hidden; background-image: - radial-gradient(ellipse 80% 60% at 30% 40%, oklch(0.94 0.018 165 / 0.5), transparent 60%), - radial-gradient(ellipse 60% 50% at 75% 65%, oklch(0.94 0.018 165 / 0.45), transparent 60%); + radial-gradient( + ellipse 80% 60% at 30% 40%, + oklch(0.94 0.018 165 / 0.5), + transparent 60% + ), + radial-gradient( + ellipse 60% 50% at 75% 65%, + oklch(0.94 0.018 165 / 0.45), + transparent 60% + ); } .exp-map__grid { - position: absolute; inset: 0; + position: absolute; + inset: 0; background-image: linear-gradient(to right, oklch(0 0 0 / 0.03) 1px, transparent 1px), linear-gradient(to bottom, oklch(0 0 0 / 0.03) 1px, transparent 1px); background-size: 60px 60px; pointer-events: none; } -.exp-map__pin { position: absolute; transform: translate(-50%, -100%); cursor: pointer; } +.exp-map__pin { + position: absolute; + transform: translate(-50%, -100%); + cursor: pointer; +} .exp-map__pin-dot { - width: 12px; height: 12px; + width: 12px; + height: 12px; border-radius: 50%; background: var(--human); border: 2px solid white; @@ -1089,21 +1608,29 @@ kbd.exp-kbd { box-shadow: 0 0 0 6px var(--primary-wash); } .exp-map__pin-label { - position: absolute; top: -28px; left: 50%; transform: translateX(-50%); + position: absolute; + top: -28px; + left: 50%; + transform: translateX(-50%); white-space: nowrap; padding: 0.15rem 0.5rem; background: var(--card); border: 1px solid var(--border); border-radius: 999px; - font-size: 0.66rem; color: var(--foreground); + font-size: 0.66rem; + color: var(--foreground); opacity: 0; pointer-events: none; font-family: var(--font-mono); } .exp-map__pin:hover .exp-map__pin-label, -.exp-map__pin[data-selected="true"] .exp-map__pin-label { opacity: 1; } +.exp-map__pin[data-selected="true"] .exp-map__pin-label { + opacity: 1; +} .exp-map__legend { - position: absolute; bottom: 0.8rem; left: 0.8rem; + position: absolute; + bottom: 0.8rem; + left: 0.8rem; background: var(--card); border: 1px solid var(--border); border-radius: 6px; @@ -1115,7 +1642,8 @@ kbd.exp-kbd { /* Calendar */ .exp-cal { - display: grid; grid-template-columns: repeat(7, 1fr); + display: grid; + grid-template-columns: repeat(7, 1fr); gap: 1px; background: var(--border); border: 1px solid var(--border); @@ -1136,26 +1664,48 @@ kbd.exp-kbd { background: var(--card); padding: 0.35rem 0.3rem; min-height: 72px; - display: flex; flex-direction: column; gap: 0.2rem; + display: flex; + flex-direction: column; + gap: 0.2rem; +} +.exp-cal__day[data-other="true"] { + background: oklch(0.985 0.002 95); +} +.exp-cal__day[data-today="true"] { + background: var(--human-wash); +} +.exp-cal__day-num { + font-family: var(--font-mono); + font-size: 0.72rem; + color: var(--muted-foreground); +} +.exp-cal__day[data-today="true"] .exp-cal__day-num { + color: var(--human); + font-weight: 600; } -.exp-cal__day[data-other="true"] { background: oklch(0.985 0.002 95); } -.exp-cal__day[data-today="true"] { background: var(--human-wash); } -.exp-cal__day-num { font-family: var(--font-mono); font-size: 0.72rem; color: var(--muted-foreground); } -.exp-cal__day[data-today="true"] .exp-cal__day-num { color: var(--human); font-weight: 600; } .exp-cal__event { font-size: 0.66rem; - background: oklch(0.580 0.172 253.7 / 0.12); + background: oklch(0.58 0.172 253.7 / 0.12); color: var(--primary); padding: 0.1rem 0.3rem; border-radius: 3px; cursor: pointer; display: block; - overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.exp-cal__event[data-selected="true"] { + background: var(--primary); + color: white; } -.exp-cal__event[data-selected="true"] { background: var(--primary); color: white; } /* Chart */ -.exp-chart-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } +.exp-chart-grid { + display: grid; + grid-template-columns: 1fr; + gap: 1rem; +} .exp-chart-card { border: 1px solid var(--border); border-radius: 12px; @@ -1163,37 +1713,68 @@ kbd.exp-kbd { padding: 1rem; } .exp-chart-card__head { - display: flex; align-items: baseline; justify-content: space-between; + display: flex; + align-items: baseline; + justify-content: space-between; margin-bottom: 0.7rem; gap: 0.5rem; } -.exp-chart-card__title { font-size: 0.92rem; font-weight: 600; } -.exp-chart-card__sub { font-size: 0.72rem; color: var(--muted-foreground); font-family: var(--font-mono); text-align: right; } +.exp-chart-card__title { + font-size: 0.92rem; + font-weight: 600; +} +.exp-chart-card__sub { + font-size: 0.72rem; + color: var(--muted-foreground); + font-family: var(--font-mono); + text-align: right; +} .exp-heatmap { - display: grid; grid-auto-flow: column; + display: grid; + grid-auto-flow: column; grid-template-rows: repeat(7, 9px); gap: 2px; font-family: var(--font-mono); } -.exp-heatmap__cell { width: 9px; height: 9px; background: var(--muted); border-radius: 2px; } +.exp-heatmap__cell { + width: 9px; + height: 9px; + background: var(--muted); + border-radius: 2px; +} /* Reader */ -.exp-rdr { max-width: 720px; margin: 0 auto; } -.exp-rdr__list { display: flex; flex-direction: column; } +.exp-rdr { + max-width: 720px; + margin: 0 auto; +} +.exp-rdr__list { + display: flex; + flex-direction: column; +} .exp-rdr__item { padding: 1rem 0; border-bottom: 1px solid var(--border); } -.exp-rdr__item:hover { background: oklch(0.985 0.002 95); } +.exp-rdr__item:hover { + background: oklch(0.985 0.002 95); +} .exp-rdr__item-meta { - display: flex; gap: 0.5rem; align-items: baseline; + display: flex; + gap: 0.5rem; + align-items: baseline; font-family: var(--font-mono); font-size: 0.7rem; color: var(--muted-foreground); margin-bottom: 0.3rem; flex-wrap: wrap; } -.exp-rdr__item-title { font-size: 1.05rem; font-weight: 600; margin: 0 0 0.3rem 0; letter-spacing: -0.012em; } +.exp-rdr__item-title { + font-size: 1.05rem; + font-weight: 600; + margin: 0 0 0.3rem 0; + letter-spacing: -0.012em; +} .exp-rdr__item-body { font-size: 0.86rem; color: var(--muted-foreground); @@ -1211,7 +1792,9 @@ kbd.exp-kbd { /* ─── Day Story (Spotify Wrapped × Stripe) ───────────────────────── */ .day-story { padding: 1.5rem 0.25rem 0.75rem; - display: flex; flex-direction: column; gap: 1rem; + display: flex; + flex-direction: column; + gap: 1rem; } .day-story__date { font-family: var(--font-mono); @@ -1243,7 +1826,9 @@ kbd.exp-kbd { border: 1px solid var(--border); border-radius: 12px; padding: 0.8rem 0.95rem; - display: flex; flex-direction: column; gap: 0.25rem; + display: flex; + flex-direction: column; + gap: 0.25rem; } .day-story__stat-value { font-family: var(--font-mono); @@ -1260,11 +1845,16 @@ kbd.exp-kbd { letter-spacing: 0.06em; color: var(--muted-foreground); text-transform: lowercase; - display: flex; align-items: center; gap: 0.35rem; + display: flex; + align-items: center; + gap: 0.35rem; } .day-story__stat-glyph { - width: 14px; height: 14px; - display: inline-flex; align-items: center; justify-content: center; + width: 14px; + height: 14px; + display: inline-flex; + align-items: center; + justify-content: center; border-radius: 3px; background: var(--muted); font-size: 0.7rem; @@ -1276,7 +1866,9 @@ kbd.exp-kbd { padding: 0.5rem 0.25rem 0.5rem; } .actstrip__head { - display: flex; justify-content: space-between; align-items: baseline; + display: flex; + justify-content: space-between; + align-items: baseline; margin-bottom: 0.45rem; } .actstrip__title { @@ -1287,13 +1879,16 @@ kbd.exp-kbd { color: var(--muted-foreground); } .actstrip__legend { - display: inline-flex; align-items: center; gap: 0.2rem; + display: inline-flex; + align-items: center; + gap: 0.2rem; font-family: var(--font-mono); font-size: 0.62rem; color: var(--muted-foreground); } .actstrip__legend-cell { - width: 8px; height: 8px; + width: 8px; + height: 8px; border-radius: 2px; } .actstrip__cells { @@ -1307,16 +1902,26 @@ kbd.exp-kbd { border-radius: 3px; overflow: hidden; background: var(--muted); - display: flex; align-items: flex-end; justify-content: center; + display: flex; + align-items: flex-end; + justify-content: center; padding: 0; border: 0; cursor: pointer; transition: transform var(--motion-state); } -.actstrip__cell:hover { transform: scale(1.15); z-index: 1; } +.actstrip__cell:hover { + transform: scale(1.15); + z-index: 1; +} .actstrip__cell-fill { - position: absolute; inset: 0; - background: color-mix(in oklab, var(--foreground) calc(var(--intensity, 0) * 100%), transparent); + position: absolute; + inset: 0; + background: color-mix( + in oklab, + var(--foreground) calc(var(--intensity, 0) * 100%), + transparent + ); } .actstrip__cell-day { position: relative; @@ -1333,14 +1938,21 @@ kbd.exp-kbd { /* ─── Year strip (Apple Photos year view, generalized) ─────────── */ .yearstrip { padding: 0.5rem 0 0.5rem; - display: flex; flex-direction: column; gap: 0.5rem; + display: flex; + flex-direction: column; + gap: 0.5rem; } .yearstrip__head { - display: flex; align-items: baseline; justify-content: space-between; + display: flex; + align-items: baseline; + justify-content: space-between; padding: 0 0.25rem; } .yearstrip__title { - display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap; + display: flex; + align-items: baseline; + gap: 0.5rem; + flex-wrap: wrap; } .yearstrip__eyebrow { font-family: var(--font-mono); @@ -1371,11 +1983,15 @@ kbd.exp-kbd { scrollbar-width: none; height: 72px; } -.yearstrip__cells::-webkit-scrollbar { display: none; } +.yearstrip__cells::-webkit-scrollbar { + display: none; +} .yearstrip__cell { position: relative; height: 100%; - display: flex; flex-direction: column; justify-content: flex-end; + display: flex; + flex-direction: column; + justify-content: flex-end; padding: 0; border: 0; background: transparent; @@ -1385,7 +2001,9 @@ kbd.exp-kbd { transition: transform var(--motion-state); min-width: 28px; } -.yearstrip__cell:hover { transform: translateY(-2px); } +.yearstrip__cell:hover { + transform: translateY(-2px); +} .yearstrip__bar { display: block; width: 100%; @@ -1414,13 +2032,19 @@ kbd.exp-kbd { color: var(--human); font-weight: 600; } -.yearstrip__cell:hover .yearstrip__year { color: var(--foreground); } +.yearstrip__cell:hover .yearstrip__year { + color: var(--foreground); +} .rail { padding: 0.5rem 0 0.5rem; - display: flex; flex-direction: column; gap: 0.45rem; + display: flex; + flex-direction: column; + gap: 0.45rem; } .rail__head { - display: flex; align-items: baseline; justify-content: space-between; + display: flex; + align-items: baseline; + justify-content: space-between; padding: 0 0.25rem; } .rail__label { @@ -1441,13 +2065,18 @@ kbd.exp-kbd { margin: 0 -0.875rem; padding: 0 0.875rem; } -.rail__scroll::-webkit-scrollbar { display: none; } +.rail__scroll::-webkit-scrollbar { + display: none; +} .rail__row { - display: inline-flex; gap: 0.45rem; + display: inline-flex; + gap: 0.45rem; padding: 0.25rem 0; } .rail__entity { - display: flex; flex-direction: column; align-items: center; + display: flex; + flex-direction: column; + align-items: center; gap: 0.35rem; padding: 0.55rem 0.55rem 0.65rem; border-radius: 12px; @@ -1456,25 +2085,37 @@ kbd.exp-kbd { min-width: 76px; transition: var(--motion-state); } -.rail__entity:hover { border-color: oklch(0.78 0 0); } -.rail__entity:active { transform: scale(0.97); } +.rail__entity:hover { + border-color: oklch(0.78 0 0); +} +.rail__entity:active { + transform: scale(0.97); +} .rail__avatar { - width: 40px; height: 40px; + width: 40px; + height: 40px; border-radius: 50%; - display: inline-flex; align-items: center; justify-content: center; + display: inline-flex; + align-items: center; + justify-content: center; color: white; font-family: var(--font-mono); font-size: 0.86rem; font-weight: 600; letter-spacing: 0.01em; } -.rail__avatar--mono { border-radius: 10px; font-size: 1rem; } +.rail__avatar--mono { + border-radius: 10px; + font-size: 1rem; +} .rail__entity-name { font-size: 0.78rem; font-weight: 500; color: var(--foreground); max-width: 80px; - overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .rail__entity-count { font-family: var(--font-mono); @@ -1487,15 +2128,21 @@ kbd.exp-kbd { padding: 0.45rem 0.75rem; min-width: 0; } -.rail__entity--channel .rail__entity-name { max-width: 160px; } +.rail__entity--channel .rail__entity-name { + max-width: 160px; +} /* ─── Memory section (Apple Photos "On this day") ───────────────── */ .memories { padding: 0.5rem 0; - display: flex; flex-direction: column; gap: 0.6rem; + display: flex; + flex-direction: column; + gap: 0.6rem; } .memories__head { - display: flex; align-items: baseline; justify-content: space-between; + display: flex; + align-items: baseline; + justify-content: space-between; padding: 0 0.25rem; } .memories__title { @@ -1509,10 +2156,18 @@ kbd.exp-kbd { font-size: 0.68rem; color: var(--muted-foreground); } -.memories__cards { display: flex; flex-direction: column; gap: 0.6rem; } -.memory { position: relative; } +.memories__cards { + display: flex; + flex-direction: column; + gap: 0.6rem; +} +.memory { + position: relative; +} .memory__label { - position: absolute; top: 0.6rem; right: 0.7rem; + position: absolute; + top: 0.6rem; + right: 0.7rem; z-index: 1; font-family: var(--font-mono); font-size: 0.64rem; @@ -1525,14 +2180,26 @@ kbd.exp-kbd { box-shadow: 0 1px 3px oklch(0 0 0 / 0.15); } /* On photo memories, push the eyebrow's right side in to clear the badge. */ -.memory .card--photo .card__eyebrow { right: 5.5rem; } -.memory .card .card__eyebrow { padding-right: 5.5rem; } +.memory .card--photo .card__eyebrow { + right: 5.5rem; +} +.memory .card .card__eyebrow { + padding-right: 5.5rem; +} /* ─── Photo card aspect ratios (Apple Photos masonry vibe) ──────── */ -.card--photo.is-4x3 .card__photo img { aspect-ratio: 4/3; } -.card--photo.is-3x4 .card__photo img { aspect-ratio: 3/4; } -.card--photo.is-1x1 .card__photo img { aspect-ratio: 1/1; } -.card--photo.is-16x9 .card__photo img { aspect-ratio: 16/9; } +.card--photo.is-4x3 .card__photo img { + aspect-ratio: 4 / 3; +} +.card--photo.is-3x4 .card__photo img { + aspect-ratio: 3 / 4; +} +.card--photo.is-1x1 .card__photo img { + aspect-ratio: 1 / 1; +} +.card--photo.is-16x9 .card__photo img { + aspect-ratio: 16 / 9; +} /* ─── Bigger day labels — Things 3 flavor ───────────────────────── */ .feed__day-label { @@ -1541,16 +2208,38 @@ kbd.exp-kbd { } @media (min-width: 640px) { - .day-story__title { font-size: 2.2rem; } - .day-story__stats { grid-template-columns: repeat(4, minmax(0, 1fr)); } - .feed__day-label { font-size: 1.85rem; } + .day-story__title { + font-size: 2.2rem; + } + .day-story__stats { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } + .feed__day-label { + font-size: 1.85rem; + } } /* ═══════════════════════════════════════════════════════════════════════ Animations ═══════════════════════════════════════════════════════════════════════ */ -@keyframes slide-up { from { transform: translateY(8px); opacity: 0; } to { transform: translateY(0); opacity: 1; } } -@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } } +@keyframes slide-up { + from { + transform: translateY(8px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} +@keyframes fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} /* ═══════════════════════════════════════════════════════════════════════ Desktop / tablet — additive @@ -1563,14 +2252,24 @@ kbd.exp-kbd { padding: 0.75rem 1.25rem; row-gap: 0; } - .exp-facets { padding: 0.55rem 1.25rem; } - .exp-topbar__brand-name { display: inline; } - .exp-grant-chip__label { display: inline; } - .feed { padding: 0.5rem 1.25rem 5rem; } + .exp-facets { + padding: 0.55rem 1.25rem; + } + .exp-topbar__brand-name { + display: inline; + } + .exp-grant-chip__label { + display: inline; + } + .feed { + padding: 0.5rem 1.25rem 5rem; + } } @media (min-width: 880px) { - .exp-views__btn-label { display: inline; } + .exp-views__btn-label { + display: inline; + } } @media (min-width: 1000px) { @@ -1585,11 +2284,17 @@ kbd.exp-kbd { .exp-app .exp-main { transition: padding-right var(--motion-moderate) var(--ease-enter); } - .exp-app[data-peek="open"] .exp-peek-backdrop { display: none; } + .exp-app[data-peek="open"] .exp-peek-backdrop { + display: none; + } .exp-app[data-peek="open"] .exp-peek { position: fixed; - top: 0; right: 0; bottom: 0; left: auto; - width: 440px; height: 100vh; + top: 0; + right: 0; + bottom: 0; + left: auto; + width: 440px; + height: 100vh; border-top-left-radius: 0; border-top-right-radius: 0; border-left: 1px solid var(--border); @@ -1597,26 +2302,60 @@ kbd.exp-kbd { animation: slide-from-right 0.22s var(--ease-enter); z-index: 50; } - .exp-app[data-peek="open"] .exp-peek::before { display: none; } + .exp-app[data-peek="open"] .exp-peek::before { + display: none; + } - .exp-conv { grid-template-columns: 220px minmax(0, 1fr); } - .exp-conv__channels { flex-direction: column; border-right: 1px solid var(--border); border-bottom: 0; padding: 0 0.75rem 0 0; } - .exp-conv__channel { width: 100%; text-align: left; padding: 0.45rem 0.6rem; } - .exp-ledger { grid-template-columns: 1fr 280px; } + .exp-conv { + grid-template-columns: 220px minmax(0, 1fr); + } + .exp-conv__channels { + flex-direction: column; + border-right: 1px solid var(--border); + border-bottom: 0; + padding: 0 0.75rem 0 0; + } + .exp-conv__channel { + width: 100%; + text-align: left; + padding: 0.45rem 0.6rem; + } + .exp-ledger { + grid-template-columns: 1fr 280px; + } .exp-ledger__row { grid-template-columns: 80px 1fr 120px 140px; grid-template-areas: "date title cat amount"; padding: 0.7rem 1rem; } - .exp-ledger__row-date { grid-area: date; } - .exp-ledger__row-merchant { grid-area: title; } - .exp-ledger__row-cat { grid-area: cat; margin-left: 0; } - .exp-ledger__row-cat::before { content: none; } - .exp-ledger__row-amount { grid-area: amount; } - .exp-table .truncate { max-width: 540px; } + .exp-ledger__row-date { + grid-area: date; + } + .exp-ledger__row-merchant { + grid-area: title; + } + .exp-ledger__row-cat { + grid-area: cat; + margin-left: 0; + } + .exp-ledger__row-cat::before { + content: none; + } + .exp-ledger__row-amount { + grid-area: amount; + } + .exp-table .truncate { + max-width: 540px; + } } @keyframes slide-from-right { - from { transform: translateX(12px); opacity: 0; } - to { transform: translateX(0); opacity: 1; } + from { + transform: translateX(12px); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } } diff --git a/docs/design-system/ink-carbon/project/explorer/feed.jsx b/docs/design-system/ink-carbon/project/explorer/feed.jsx index 0767cc6f3..7795b4576 100644 --- a/docs/design-system/ink-carbon/project/explorer/feed.jsx +++ b/docs/design-system/ink-carbon/project/explorer/feed.jsx @@ -13,309 +13,365 @@ * separated by sticky day headers. This is the canonical view. */ -;(() => { +(() => { + const { useMemo } = React; + const { fmtRelative, fmtClock, fmtDay, fmtCurrency, fmtDuration, fmtDistance, initials } = window.PDPPPrim; + const { detect } = window.PDPP_DISPATCH; -const { useMemo } = React; -const { fmtRelative, fmtClock, fmtDay, fmtCurrency, fmtDuration, fmtDistance, initials } = window.PDPPPrim; -const { detect } = window.PDPP_DISPATCH; - -// ─── Per-record card kind ───────────────────────────────────────────── -// Reuses the stream-level dispatch to pick a card. "feed-generic" is -// the universal fallback. -const KIND_FOR_CAP = { - conversation: "message", - ledger: "money", - gallery: "photo", - calendar: "event", - chart: "activity", - reader: "reader", - map: "location", - timeline: "generic", - table: "generic", -}; -function cardKindForStream(stream) { - const { capabilities } = detect(stream); - // The first non-table capability wins — same priority order as views. - for (const cap of capabilities) { - if (cap === "table") continue; - return KIND_FOR_CAP[cap] ?? "generic"; + // ─── Per-record card kind ───────────────────────────────────────────── + // Reuses the stream-level dispatch to pick a card. "feed-generic" is + // the universal fallback. + const KIND_FOR_CAP = { + conversation: "message", + ledger: "money", + gallery: "photo", + calendar: "event", + chart: "activity", + reader: "reader", + map: "location", + timeline: "generic", + table: "generic", + }; + function cardKindForStream(stream) { + const { capabilities } = detect(stream); + // The first non-table capability wins — same priority order as views. + for (const cap of capabilities) { + if (cap === "table") continue; + return KIND_FOR_CAP[cap] ?? "generic"; + } + return "generic"; } - return "generic"; -} -// ─── Field probes (lexical only — no connector branches) ────────────── -function findField(stream, regex) { - return stream.schema.fields.find((f) => regex.test(f.name))?.name; -} -function findFieldByType(stream, type) { - return stream.schema.fields.find((f) => f.type === type)?.name; -} -function findImageField(stream) { - const blob = stream.schema.fields.find((f) => f.type === "blob" && (f.media_type ?? "").startsWith("image/")); - if (blob) return blob.name; - return stream.schema.fields.find((f) => /thumb|image|photo|picture/i.test(f.name) && (f.type === "blob" || f.type === "url"))?.name; -} + // ─── Field probes (lexical only — no connector branches) ────────────── + function findField(stream, regex) { + return stream.schema.fields.find((f) => regex.test(f.name))?.name; + } + function findFieldByType(stream, type) { + return stream.schema.fields.find((f) => f.type === type)?.name; + } + function findImageField(stream) { + const blob = stream.schema.fields.find((f) => f.type === "blob" && (f.media_type ?? "").startsWith("image/")); + if (blob) return blob.name; + return stream.schema.fields.find( + (f) => /thumb|image|photo|picture/i.test(f.name) && (f.type === "blob" || f.type === "url") + )?.name; + } -function recordTimeISO(stream, r) { - const tf = findFieldByType(stream, "timestamp") ?? findField(stream, /date|at$|night_of/i); - return tf ? r[tf] : null; -} + function recordTimeISO(stream, r) { + const tf = findFieldByType(stream, "timestamp") ?? findField(stream, /date|at$|night_of/i); + return tf ? r[tf] : null; + } -function cleanPerson(s) { - if (!s) return ""; - return String(s).replace(/<[^>]+>/g, "").trim(); -} + function cleanPerson(s) { + if (!s) return ""; + return String(s) + .replace(/<[^>]+>/g, "") + .trim(); + } -// ═══════════════════════════════════════════════════════════════════════ -// Card components — each is small, opinionated, generic over the schema. -// ═══════════════════════════════════════════════════════════════════════ + // ═══════════════════════════════════════════════════════════════════════ + // Card components — each is small, opinionated, generic over the schema. + // ═══════════════════════════════════════════════════════════════════════ -function CardEyebrow({ stream, time, light }) { - return ( -
- {stream.icon} - {stream.connector_id} - · {stream.connection_display} - - {time ? fmtClock(time) : "—"} -
- ); -} + function CardEyebrow({ stream, time, light }) { + return ( +
+ {stream.icon} + {stream.connector_id} + · {stream.connection_display} + + {time ? fmtClock(time) : "—"} +
+ ); + } -function MessageCard({ stream, record, selected, onClick }) { - const authorField = findField(stream, /^from$|author|sender|actor/i) ?? findFieldByType(stream, "person"); - const bodyField = findField(stream, /body|snippet|text|message|content/i); - const subjField = findField(stream, /subject|title/i); - const chanField = findField(stream, /channel/i); - const author = cleanPerson(record[authorField]); - const time = recordTimeISO(stream, record); - return ( -
- -
- - {initials(author)} - -
-
{author}
- {chanField && record[chanField] ?
{record[chanField]}
: null} + function MessageCard({ stream, record, selected, onClick }) { + const authorField = findField(stream, /^from$|author|sender|actor/i) ?? findFieldByType(stream, "person"); + const bodyField = findField(stream, /body|snippet|text|message|content/i); + const subjField = findField(stream, /subject|title/i); + const chanField = findField(stream, /channel/i); + const author = cleanPerson(record[authorField]); + const time = recordTimeISO(stream, record); + return ( +
+ +
+ + {initials(author)} + +
+
{author}
+ {chanField && record[chanField] ?
{record[chanField]}
: null} +
-
- {subjField && record[subjField] ?
{record[subjField]}
: null} -
{record[bodyField]}
- {Array.isArray(record.reactions) && record.reactions.length ? ( -
- {record.reactions.map((rx, i) => {rx.emoji} {rx.count})} + {subjField && record[subjField] ?
{record[subjField]}
: null} +
{record[bodyField]}
+ {Array.isArray(record.reactions) && record.reactions.length ? ( +
+ {record.reactions.map((rx, i) => ( + + {rx.emoji} {rx.count} + + ))} +
+ ) : null} +
+ ); + } + + function MoneyCard({ stream, record, selected, onClick }) { + const amtField = findFieldByType(stream, "currency") ?? findField(stream, /^amount$/i); + const merchField = findField(stream, /merchant|payee|counterparty|seller/i); + const catField = findField(stream, /category|kind/i); + const memoField = findField(stream, /memo|note|description/i); + const amount = record[amtField] ?? 0; + const time = recordTimeISO(stream, record); + return ( +
+ +
0 ? "is-pos" : ""}`}>{fmtCurrency(amount)}
+
{record[merchField] ?? record.title}
+
+ {record[catField] ? {record[catField]} : null} + {record[memoField] ? {record[memoField]} : null}
- ) : null} -
- ); -} + + ); + } -function MoneyCard({ stream, record, selected, onClick }) { - const amtField = findFieldByType(stream, "currency") ?? findField(stream, /^amount$/i); - const merchField = findField(stream, /merchant|payee|counterparty|seller/i); - const catField = findField(stream, /category|kind/i); - const memoField = findField(stream, /memo|note|description/i); - const amount = record[amtField] ?? 0; - const time = recordTimeISO(stream, record); - return ( -
- -
0 ? "is-pos" : ""}`}>{fmtCurrency(amount)}
-
{record[merchField] ?? record.title}
-
- {record[catField] ? {record[catField]} : null} - {record[memoField] ? {record[memoField]} : null} -
-
- ); -} + function PhotoCard({ stream, record, selected, onClick }) { + const imgField = findImageField(stream); + const capField = findField(stream, /caption|title|subject/i); + const time = recordTimeISO(stream, record); + // Variable aspect ratios so the feed breathes (Apple Photos masonry feel). + // Deterministic per record so it doesn't reshuffle. + const aspectClass = ["is-4x3", "is-3x4", "is-1x1", "is-16x9"][hashId(record.id) % 4]; + return ( +
+
+ {record[capField] +
+ +
{record[capField]}
+
+
+ ); + } -function PhotoCard({ stream, record, selected, onClick }) { - const imgField = findImageField(stream); - const capField = findField(stream, /caption|title|subject/i); - const time = recordTimeISO(stream, record); - // Variable aspect ratios so the feed breathes (Apple Photos masonry feel). - // Deterministic per record so it doesn't reshuffle. - const aspectClass = ["is-4x3", "is-3x4", "is-1x1", "is-16x9"][hashId(record.id) % 4]; - return ( -
-
- {record[capField] -
- -
{record[capField]}
-
-
- ); -} + function hashId(s) { + let h = 0; + for (let i = 0; i < (s ?? "").length; i++) h = (h * 31 + s.charCodeAt(i)) | 0; + return Math.abs(h); + } -function hashId(s) { - let h = 0; - for (let i = 0; i < (s ?? "").length; i++) h = (h * 31 + s.charCodeAt(i)) | 0; - return Math.abs(h); -} + function EventCard({ stream, record, selected, onClick }) { + const titleField = findField(stream, /title|subject|name/i); + const locField = findField(stream, /location|place/i); + const startField = findField(stream, /^start/i) ?? findFieldByType(stream, "timestamp"); + const endField = findField(stream, /^end/i); + const attendField = findField(stream, /attendees|participants/i); + const start = record[startField]; + const end = record[endField]; + return ( +
+ +
+ {start ? fmtClock(start) : ""} {end ? `– ${fmtClock(end)}` : ""} +
+
{record[titleField]}
+ {record[locField] ?
{record[locField]}
: null} + {Array.isArray(record[attendField]) && record[attendField].length ? ( +
+ + {record[attendField].length} attendee{record[attendField].length === 1 ? "" : "s"} + +
+ ) : null} +
+ ); + } -function EventCard({ stream, record, selected, onClick }) { - const titleField = findField(stream, /title|subject|name/i); - const locField = findField(stream, /location|place/i); - const startField = findField(stream, /^start/i) ?? findFieldByType(stream, "timestamp"); - const endField = findField(stream, /^end/i); - const attendField = findField(stream, /attendees|participants/i); - const start = record[startField]; - const end = record[endField]; - return ( -
- -
- {start ? fmtClock(start) : ""} {end ? `– ${fmtClock(end)}` : ""} -
-
{record[titleField]}
- {record[locField] ?
{record[locField]}
: null} - {Array.isArray(record[attendField]) && record[attendField].length ? ( -
- {record[attendField].length} attendee{record[attendField].length === 1 ? "" : "s"} + function ActivityCard({ stream, record, selected, onClick }) { + const titleField = findField(stream, /title|name/i); + const typeField = findField(stream, /^type$/i); + const distField = findField(stream, /distance/i); + const durField = findField(stream, /^duration|^elapsed/i); + const elevField = findField(stream, /elev/i); + // Sleep-like streams: score + sleep stages + const scoreField = findField(stream, /^score|^value$/i); + const time = recordTimeISO(stream, record); + const stats = []; + if (distField && record[distField] != null) + stats.push({ label: "distance", value: fmtDistance(record[distField]) }); + if (durField && record[durField] != null) stats.push({ label: "duration", value: fmtDuration(record[durField]) }); + if (elevField && record[elevField] != null) + stats.push({ label: "elevation", value: `${Math.round(record[elevField])}m` }); + if (!stats.length && scoreField && record[scoreField] != null) + stats.push({ label: scoreField, value: String(record[scoreField]) }); + return ( +
+ +
{record[titleField] ?? record[typeField] ?? "Activity"}
+
+ {stats.map((s) => ( +
+
{s.value}
+
{s.label}
+
+ ))}
- ) : null} -
- ); -} +
+ ); + } -function ActivityCard({ stream, record, selected, onClick }) { - const titleField = findField(stream, /title|name/i); - const typeField = findField(stream, /^type$/i); - const distField = findField(stream, /distance/i); - const durField = findField(stream, /^duration|^elapsed/i); - const elevField = findField(stream, /elev/i); - // Sleep-like streams: score + sleep stages - const scoreField = findField(stream, /^score|^value$/i); - const time = recordTimeISO(stream, record); - const stats = []; - if (distField && record[distField] != null) stats.push({ label: "distance", value: fmtDistance(record[distField]) }); - if (durField && record[durField] != null) stats.push({ label: "duration", value: fmtDuration(record[durField]) }); - if (elevField && record[elevField] != null) stats.push({ label: "elevation", value: `${Math.round(record[elevField])}m` }); - if (!stats.length && scoreField && record[scoreField] != null) stats.push({ label: scoreField, value: String(record[scoreField]) }); - return ( -
- -
{record[titleField] ?? record[typeField] ?? "Activity"}
-
- {stats.map((s) => ( -
-
{s.value}
-
{s.label}
+ function ReaderCard({ stream, record, selected, onClick }) { + const titleField = findField(stream, /title|subject/i); + const bodyField = findField(stream, /body|content/i); + const actorField = findField(stream, /actor|author|user/i); + const typeField = findField(stream, /^type$/i); + const repoField = findField(stream, /repo|project/i); + const time = recordTimeISO(stream, record); + return ( +
+ + {record[typeField] || record[repoField] ? ( +
+ {record[typeField] ? {record[typeField]} : null} + {record[repoField] ? {record[repoField]} : null}
- ))} -
-
- ); -} + ) : null} +
{record[titleField]}
+ {record[bodyField] ?
{record[bodyField]}
: null} + {record[actorField] ? ( +
+ by {cleanPerson(record[actorField])} +
+ ) : null} + + ); + } -function ReaderCard({ stream, record, selected, onClick }) { - const titleField = findField(stream, /title|subject/i); - const bodyField = findField(stream, /body|content/i); - const actorField = findField(stream, /actor|author|user/i); - const typeField = findField(stream, /^type$/i); - const repoField = findField(stream, /repo|project/i); - const time = recordTimeISO(stream, record); - return ( -
- - {record[typeField] || record[repoField] ? ( -
- {record[typeField] ? {record[typeField]} : null} - {record[repoField] ? {record[repoField]} : null} + function LocationCard({ stream, record, selected, onClick }) { + const titleField = findField(stream, /title|caption|name/i); + const latField = findField(stream, /^lat/i); + const lngField = findField(stream, /^l(ng|on)/i); + const time = recordTimeISO(stream, record); + return ( +
+ +
{record[titleField] ?? "Location"}
+
+ {Number(record[latField]).toFixed(4)}, {Number(record[lngField]).toFixed(4)}
- ) : null} -
{record[titleField]}
- {record[bodyField] ?
{record[bodyField]}
: null} - {record[actorField] ?
by {cleanPerson(record[actorField])}
: null} -
- ); -} - -function LocationCard({ stream, record, selected, onClick }) { - const titleField = findField(stream, /title|caption|name/i); - const latField = findField(stream, /^lat/i); - const lngField = findField(stream, /^l(ng|on)/i); - const time = recordTimeISO(stream, record); - return ( -
- -
{record[titleField] ?? "Location"}
-
- {Number(record[latField]).toFixed(4)}, {Number(record[lngField]).toFixed(4)} -
-
- ); -} +
+ ); + } -function GenericCard({ stream, record, selected, onClick }) { - // Best-effort: pick a title-ish field, time, and 1–2 secondary values - const titleField = findField(stream, /title|subject|name|merchant|caption/i); - const time = recordTimeISO(stream, record); - const secondary = stream.schema.fields.filter((f) => f.granted && f.name !== titleField && f.type !== "id" && f.type !== "blob" && f.type !== "geo" && record[f.name] != null).slice(0, 3); - return ( -
- -
{record[titleField] ?? record.id}
-
- {secondary.map((f) => ( -
- {f.name} - {String(record[f.name]).slice(0, 90)} -
- ))} -
-
- ); -} + function GenericCard({ stream, record, selected, onClick }) { + // Best-effort: pick a title-ish field, time, and 1–2 secondary values + const titleField = findField(stream, /title|subject|name|merchant|caption/i); + const time = recordTimeISO(stream, record); + const secondary = stream.schema.fields + .filter( + (f) => + f.granted && + f.name !== titleField && + f.type !== "id" && + f.type !== "blob" && + f.type !== "geo" && + record[f.name] != null + ) + .slice(0, 3); + return ( +
+ +
{record[titleField] ?? record.id}
+
+ {secondary.map((f) => ( +
+ {f.name} + {String(record[f.name]).slice(0, 90)} +
+ ))} +
+
+ ); + } -const CARD_BY_KIND = { - message: MessageCard, - money: MoneyCard, - photo: PhotoCard, - event: EventCard, - activity: ActivityCard, - reader: ReaderCard, - location: LocationCard, - generic: GenericCard, -}; + const CARD_BY_KIND = { + message: MessageCard, + money: MoneyCard, + photo: PhotoCard, + event: EventCard, + activity: ActivityCard, + reader: ReaderCard, + location: LocationCard, + generic: GenericCard, + }; -// ─── Stable color from a string (used for message-card avatars) ───── -function avatarColor(s) { - let h = 0; - for (let i = 0; i < (s ?? "").length; i++) h = (h * 31 + s.charCodeAt(i)) | 0; - const hue = Math.abs(h) % 360; - return `oklch(0.52 0.13 ${hue})`; -} + // ─── Stable color from a string (used for message-card avatars) ───── + function avatarColor(s) { + let h = 0; + for (let i = 0; i < (s ?? "").length; i++) h = (h * 31 + s.charCodeAt(i)) | 0; + const hue = Math.abs(h) % 360; + return `oklch(0.52 0.13 ${hue})`; + } -// ═══════════════════════════════════════════════════════════════════════ -// FeedView — groups hits by day, picks a card per record's stream kind. -// ═══════════════════════════════════════════════════════════════════════ + // ═══════════════════════════════════════════════════════════════════════ + // FeedView — groups hits by day, picks a card per record's stream kind. + // ═══════════════════════════════════════════════════════════════════════ -function FeedView({ hits, selectedId, onSelect, showDiscover, streams, onAddChip }) { - // Group by day - const groups = useMemo(() => { - const out = []; - let lastDay = null; - for (const h of hits) { - const iso = recordTimeISO(h.stream, h.record); - const day = iso ? iso.slice(0, 10) : "__no_date__"; - if (day !== lastDay) { - out.push({ day, items: [] }); - lastDay = day; + function FeedView({ hits, selectedId, onSelect, showDiscover, streams, onAddChip }) { + // Group by day + const groups = useMemo(() => { + const out = []; + let lastDay = null; + for (const h of hits) { + const iso = recordTimeISO(h.stream, h.record); + const day = iso ? iso.slice(0, 10) : "__no_date__"; + if (day !== lastDay) { + out.push({ day, items: [] }); + lastDay = day; + } + out[out.length - 1].items.push(h); } - out[out.length - 1].items.push(h); - } - return out; - }, [hits]); + return out; + }, [hits]); - const memories = useMemo(() => showDiscover ? window.PDPP_DISCOVER.findMemories(streams ?? []) : [], [showDiscover, streams]); - const { DayStory, ActivityStrip, PeopleRail, MerchantRail, ChannelRail, YearStrip } = window.PDPP_DISCOVER ?? {}; + const memories = useMemo( + () => (showDiscover ? window.PDPP_DISCOVER.findMemories(streams ?? []) : []), + [showDiscover, streams] + ); + const { DayStory, ActivityStrip, PeopleRail, MerchantRail, ChannelRail, YearStrip } = window.PDPP_DISCOVER ?? {}; + + if (!hits.length) { + return ( +
+ {showDiscover && DayStory ? ( + <> + + + + + + +
+
All caught up for today.
+
+ Pick a person, a merchant, or a channel above to wander further back. +
+
+ + ) : ( +
+
Nothing here.
+
Remove a filter, or try semantic search.
+
+ )} +
+ ); + } - if (!hits.length) { return (
{showDiscover && DayStory ? ( @@ -326,85 +382,61 @@ function FeedView({ hits, selectedId, onSelect, showDiscover, streams, onAddChip -
-
All caught up for today.
-
Pick a person, a merchant, or a channel above to wander further back.
-
- ) : ( -
-
Nothing here.
-
Remove a filter, or try semantic search.
-
- )} -
- ); - } - - return ( -
- {showDiscover && DayStory ? ( - <> - - - - - - - - ) : null} - {memories.length ? ( -
-
-

On this day

- {memories.length} memor{memories.length === 1 ? "y" : "ies"} -
-
- {memories.map(({ stream, record, label }) => { - const kind = cardKindForStream(stream); - const Card = CARD_BY_KIND[kind] ?? GenericCard; - return ( -
-
{label}
+ ) : null} + {memories.length ? ( +
+
+

On this day

+ + {memories.length} memor{memories.length === 1 ? "y" : "ies"} + +
+
+ {memories.map(({ stream, record, label }) => { + const kind = cardKindForStream(stream); + const Card = CARD_BY_KIND[kind] ?? GenericCard; + return ( +
+
{label}
+ onSelect(stream, record)} + record={record} + selected={selectedId === record.id} + stream={stream} + /> +
+ ); + })} +
+
+ ) : null} + {groups.map(({ day, items }) => ( +
+
+

{day === "__no_date__" ? "Undated" : fmtDay(day + "T12:00:00Z")}

+ {items.length} +
+
+ {items.map(({ stream, record }) => { + const kind = cardKindForStream(stream); + const Card = CARD_BY_KIND[kind] ?? GenericCard; + return ( onSelect(stream, record)} record={record} selected={selectedId === record.id} stream={stream} /> -
- ); - })} -
-
- ) : null} - {groups.map(({ day, items }) => ( -
-
-

{day === "__no_date__" ? "Undated" : fmtDay(day + "T12:00:00Z")}

- {items.length} -
-
- {items.map(({ stream, record }) => { - const kind = cardKindForStream(stream); - const Card = CARD_BY_KIND[kind] ?? GenericCard; - return ( - onSelect(stream, record)} - record={record} - selected={selectedId === record.id} - stream={stream} - /> - ); - })} -
-
- ))} -
- ); -} - -window.FeedView = FeedView; + ); + })} +
+ + ))} + + ); + } + window.FeedView = FeedView; })(); diff --git a/docs/design-system/ink-carbon/project/explorer/peek.jsx b/docs/design-system/ink-carbon/project/explorer/peek.jsx index 1eb0a0116..984b4514f 100644 --- a/docs/design-system/ink-carbon/project/explorer/peek.jsx +++ b/docs/design-system/ink-carbon/project/explorer/peek.jsx @@ -2,103 +2,147 @@ // SPDX-License-Identifier: Apache-2.0 /* IIFE-WRAPPED */ -;(() => { -/* PDPP Explorer — Peek panel (record detail) - * - * Right pane that slides in when a record is selected. Surfaces: - * - Title + meta - * - Each schema field, with granted/redacted state explicit - * - The actual /v1/streams/.../records/ URL the explorer is reading - */ +(() => { + /* PDPP Explorer — Peek panel (record detail) + * + * Right pane that slides in when a record is selected. Surfaces: + * - Title + meta + * - Each schema field, with granted/redacted state explicit + * - The actual /v1/streams/.../records/ URL the explorer is reading + */ -const { fmtRelative, fmtDate, fmtCurrency, fmtDuration, fmtDistance, Avatar: PeekAvatar } = window.PDPPPrim; + const { fmtRelative, fmtDate, fmtCurrency, fmtDuration, fmtDistance, Avatar: PeekAvatar } = window.PDPPPrim; -function Peek({ stream, record, onClose, projection }) { - if (!stream || !record) return null; - const fields = stream.schema.fields; - const granted = fields.filter((f) => f.granted); - const redacted = fields.filter((f) => !f.granted); - const visibleFields = projection ? granted : fields; - const title = record.subject ?? record.title ?? record.merchant ?? record.text ?? record.caption ?? record.id; - const timeField = fields.find((f) => f.type === "timestamp")?.name; + function Peek({ stream, record, onClose, projection }) { + if (!stream || !record) return null; + const fields = stream.schema.fields; + const granted = fields.filter((f) => f.granted); + const redacted = fields.filter((f) => !f.granted); + const visibleFields = projection ? granted : fields; + const title = record.subject ?? record.title ?? record.merchant ?? record.text ?? record.caption ?? record.id; + const timeField = fields.find((f) => f.type === "timestamp")?.name; - function renderValue(field) { - const v = record[field.name]; - if (v == null) return ; - if (field.type === "timestamp") - return {new Date(v).toLocaleString("en-US", { dateStyle: "medium", timeStyle: "short" })}; - if (field.type === "currency") return 0 ? "var(--success)" : "var(--foreground)" }}>{fmtCurrency(v)}; - if (field.type === "number") { - if (field.unit === "meters") return {fmtDistance(v)} ({v.toLocaleString()} m); - if (field.unit === "seconds") return {fmtDuration(v)}; - return {v.toLocaleString()}{field.unit ? {field.unit} : null}; - } - if (field.type === "id") return {v}; - if (field.type === "url") return {v}; - if (field.type === "blob") { - if ((field.media_type ?? "").startsWith("image/")) { - return ; + function renderValue(field) { + const v = record[field.name]; + if (v == null) return ; + if (field.type === "timestamp") + return ( + + {new Date(v).toLocaleString("en-US", { dateStyle: "medium", timeStyle: "short" })} + + ); + if (field.type === "currency") + return ( + 0 ? "var(--success)" : "var(--foreground)" }}> + {fmtCurrency(v)} + + ); + if (field.type === "number") { + if (field.unit === "meters") + return ( + + {fmtDistance(v)} ({v.toLocaleString()} m) + + ); + if (field.unit === "seconds") return {fmtDuration(v)}; + return ( + + {v.toLocaleString()} + {field.unit ? {field.unit} : null} + + ); } - return blob: {v}; - } - if (field.type === "person") return {String(v).replace(/<[^>]+>/g, "").trim()}; - if (Array.isArray(v)) { - if (v.length === 0) return []; - if (typeof v[0] === "object") return
{JSON.stringify(v, null, 2)}
; - return {v.join(", ")}; + if (field.type === "id") + return {v}; + if (field.type === "url") + return ( + + {v} + + ); + if (field.type === "blob") { + if ((field.media_type ?? "").startsWith("image/")) { + return ; + } + return blob: {v}; + } + if (field.type === "person") + return ( + + {String(v) + .replace(/<[^>]+>/g, "") + .trim()} + + ); + if (Array.isArray(v)) { + if (v.length === 0) return []; + if (typeof v[0] === "object") + return ( +
+              {JSON.stringify(v, null, 2)}
+            
+ ); + return {v.join(", ")}; + } + return 80 ? "long" : ""}`}>{String(v)}; } - return 80 ? "long" : ""}`}>{String(v)}; - } - return ( - - ); -} - -window.Peek = Peek; + + ); + } + window.Peek = Peek; })(); diff --git a/docs/design-system/ink-carbon/project/explorer/primitives.jsx b/docs/design-system/ink-carbon/project/explorer/primitives.jsx index 4bedf1404..98acfd14d 100644 --- a/docs/design-system/ink-carbon/project/explorer/primitives.jsx +++ b/docs/design-system/ink-carbon/project/explorer/primitives.jsx @@ -2,206 +2,232 @@ // SPDX-License-Identifier: Apache-2.0 /* IIFE-WRAPPED */ -;(() => { -/* PDPP Explorer — shared primitives + format helpers - * - * Tiny, view-agnostic building blocks. Exported on window so the - * babel-split view files can pick them up. - */ - -const { useState, useEffect, useMemo, useRef, useCallback } = React; - -// ─── Format helpers ─────────────────────────────────────────────────── - -const NOW = window.PDPP_DATA?.now ?? Date.now(); - -function fmtRelative(iso) { - const t = new Date(iso).getTime(); - const d = NOW - t; - if (Math.abs(d) < 60_000) return d < 0 ? "in <1m" : "just now"; - if (Math.abs(d) < 3_600_000) { - const m = Math.round(Math.abs(d) / 60_000); - return d < 0 ? `in ${m}m` : `${m}m ago`; +(() => { + /* PDPP Explorer — shared primitives + format helpers + * + * Tiny, view-agnostic building blocks. Exported on window so the + * babel-split view files can pick them up. + */ + + const { useState, useEffect, useMemo, useRef, useCallback } = React; + + // ─── Format helpers ─────────────────────────────────────────────────── + + const NOW = window.PDPP_DATA?.now ?? Date.now(); + + function fmtRelative(iso) { + const t = new Date(iso).getTime(); + const d = NOW - t; + if (Math.abs(d) < 60_000) return d < 0 ? "in <1m" : "just now"; + if (Math.abs(d) < 3_600_000) { + const m = Math.round(Math.abs(d) / 60_000); + return d < 0 ? `in ${m}m` : `${m}m ago`; + } + if (Math.abs(d) < 86_400_000) { + const h = Math.round(Math.abs(d) / 3_600_000); + return d < 0 ? `in ${h}h` : `${h}h ago`; + } + const days = Math.round(Math.abs(d) / 86_400_000); + if (days < 30) return d < 0 ? `in ${days}d` : `${days}d ago`; + return new Date(iso).toLocaleDateString("en-US", { month: "short", day: "numeric" }); + } + + function fmtClock(iso) { + return new Date(iso).toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" }); } - if (Math.abs(d) < 86_400_000) { - const h = Math.round(Math.abs(d) / 3_600_000); - return d < 0 ? `in ${h}h` : `${h}h ago`; + function fmtDate(iso) { + return new Date(iso).toLocaleDateString("en-US", { month: "short", day: "numeric" }); } - const days = Math.round(Math.abs(d) / 86_400_000); - if (days < 30) return d < 0 ? `in ${days}d` : `${days}d ago`; - return new Date(iso).toLocaleDateString("en-US", { month: "short", day: "numeric" }); -} - -function fmtClock(iso) { - return new Date(iso).toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" }); -} -function fmtDate(iso) { - return new Date(iso).toLocaleDateString("en-US", { month: "short", day: "numeric" }); -} -function fmtDay(iso) { - const d = new Date(iso); - const today = new Date(NOW); - const sameDay = (a, b) => - a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate(); - if (sameDay(d, today)) return "Today"; - const y = new Date(NOW - 86_400_000); - if (sameDay(d, y)) return "Yesterday"; - return d.toLocaleDateString("en-US", { weekday: "long", month: "short", day: "numeric" }); -} -function fmtCurrency(n) { - const sign = n < 0 ? "−" : n > 0 ? "+" : ""; - const abs = Math.abs(n).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); - return `${sign}$${abs}`; -} -function fmtDuration(seconds) { - const m = Math.round(seconds / 60); - if (m < 60) return `${m}m`; - const h = Math.floor(m / 60); - const mm = m % 60; - return mm === 0 ? `${h}h` : `${h}h ${mm}m`; -} -function fmtDistance(meters) { - const mi = meters / 1609.34; - return `${mi.toFixed(mi < 10 ? 2 : 1)} mi`; -} - -// ─── Initials avatar ────────────────────────────────────────────────── - -function initials(label) { - if (!label) return "·"; - const cleaned = label.replace(/<[^>]+>/g, "").trim(); - const parts = cleaned.split(/\s+/).filter(Boolean); - if (parts.length === 0) return "·"; - if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase(); - return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase(); -} - -function Avatar({ label, size = 28 }) { - return ( - - {initials(label)} - - ); -} - -// ─── Heatmap (GitHub-style; weeks across, days down) ────────────────── - -function Heatmap({ values, days = 84, color = "var(--foreground)" }) { - // values: array of {date: iso, count: number} keyed by yyyy-mm-dd - const map = new Map(); - for (const v of values || []) { - const key = v.date.slice(0, 10); - map.set(key, (map.get(key) ?? 0) + (v.count ?? 1)); + function fmtDay(iso) { + const d = new Date(iso); + const today = new Date(NOW); + const sameDay = (a, b) => + a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate(); + if (sameDay(d, today)) return "Today"; + const y = new Date(NOW - 86_400_000); + if (sameDay(d, y)) return "Yesterday"; + return d.toLocaleDateString("en-US", { weekday: "long", month: "short", day: "numeric" }); } - const max = Math.max(1, ...map.values()); - - const cells = []; - for (let i = days - 1; i >= 0; i--) { - const d = new Date(NOW - i * 86_400_000); - const key = d.toISOString().slice(0, 10); - const v = map.get(key) ?? 0; - const intensity = v === 0 ? 0 : Math.min(1, 0.18 + 0.82 * (v / max)); - cells.push( + function fmtCurrency(n) { + const sign = n < 0 ? "−" : n > 0 ? "+" : ""; + const abs = Math.abs(n).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); + return `${sign}$${abs}`; + } + function fmtDuration(seconds) { + const m = Math.round(seconds / 60); + if (m < 60) return `${m}m`; + const h = Math.floor(m / 60); + const mm = m % 60; + return mm === 0 ? `${h}h` : `${h}h ${mm}m`; + } + function fmtDistance(meters) { + const mi = meters / 1609.34; + return `${mi.toFixed(mi < 10 ? 2 : 1)} mi`; + } + + // ─── Initials avatar ────────────────────────────────────────────────── + + function initials(label) { + if (!label) return "·"; + const cleaned = label.replace(/<[^>]+>/g, "").trim(); + const parts = cleaned.split(/\s+/).filter(Boolean); + if (parts.length === 0) return "·"; + if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase(); + return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase(); + } + + function Avatar({ label, size = 28 }) { + return ( + className="exp-conv__avatar" + style={{ width: size, height: size, fontSize: size * 0.4, borderRadius: Math.round(size * 0.22) }} + > + {initials(label)} + ); } - return
{cells}
; -} - -// ─── Tiny sparkline ──────────────────────────────────────────────────── - -function Sparkline({ values, width = 120, height = 28, color = "var(--foreground)" }) { - if (!values?.length) return null; - const min = Math.min(...values); - const max = Math.max(...values); - const range = max - min || 1; - const step = width / (values.length - 1 || 1); - const points = values - .map((v, i) => `${(i * step).toFixed(1)},${(height - ((v - min) / range) * height).toFixed(1)}`) - .join(" "); - return ( - - - - - ); -} - -// ─── Capability icons (glyphs, not icons-as-an-iconset) ──────────────── - -const CAP_GLYPH = { - table: "▦", - timeline: "│", - conversation: "❝", - ledger: "$", - gallery: "▥", - map: "◎", - calendar: "▤", - chart: "↟", - reader: "¶", -}; -const CAP_LABEL = { - table: "Table", timeline: "Timeline", conversation: "Conversation", - ledger: "Ledger", gallery: "Gallery", map: "Map", calendar: "Calendar", - chart: "Chart", reader: "Reader", -}; - -// ─── Useful: useKeyboard for cmd-k ───────────────────────────────────── - -function useGlobalKey(key, modifiers, handler) { - useEffect(() => { - function onKey(e) { - const meta = modifiers.includes("meta") ? (e.metaKey || e.ctrlKey) : true; - if (e.key.toLowerCase() === key.toLowerCase() && meta) { - e.preventDefault(); - handler(e); - } + + // ─── Heatmap (GitHub-style; weeks across, days down) ────────────────── + + function Heatmap({ values, days = 84, color = "var(--foreground)" }) { + // values: array of {date: iso, count: number} keyed by yyyy-mm-dd + const map = new Map(); + for (const v of values || []) { + const key = v.date.slice(0, 10); + map.set(key, (map.get(key) ?? 0) + (v.count ?? 1)); + } + const max = Math.max(1, ...map.values()); + + const cells = []; + for (let i = days - 1; i >= 0; i--) { + const d = new Date(NOW - i * 86_400_000); + const key = d.toISOString().slice(0, 10); + const v = map.get(key) ?? 0; + const intensity = v === 0 ? 0 : Math.min(1, 0.18 + 0.82 * (v / max)); + cells.push( + + ); } - window.addEventListener("keydown", onKey); - return () => window.removeEventListener("keydown", onKey); - }, [key, handler, modifiers]); -} - -// ─── Stream-record utilities ─────────────────────────────────────────── - -/** Find the timestamp field in a stream's records by checking the schema. */ -function getTimeField(stream) { - const fs = stream.schema?.fields ?? []; - return (fs.find((f) => f.type === "timestamp") ?? fs.find((f) => /date|ts|at_$|^at$|night_of/i.test(f.name)))?.name; -} -function getRecordTime(stream, record) { - const f = getTimeField(stream); - return f ? record[f] : null; -} - -/** Returns the field labelled as "title" for the record, with sensible fallbacks. */ -function getRecordTitle(stream, record) { - return record.subject ?? record.title ?? record.merchant ?? record.text ?? record.caption ?? record.snippet ?? record.id; -} - -window.PDPPPrim = { - fmtRelative, fmtClock, fmtDate, fmtDay, fmtCurrency, fmtDuration, fmtDistance, - Avatar, Heatmap, Sparkline, - CAP_GLYPH, CAP_LABEL, - useGlobalKey, - getTimeField, getRecordTime, getRecordTitle, - initials, - NOW, -}; + return
{cells}
; + } + + // ─── Tiny sparkline ──────────────────────────────────────────────────── + + function Sparkline({ values, width = 120, height = 28, color = "var(--foreground)" }) { + if (!values?.length) return null; + const min = Math.min(...values); + const max = Math.max(...values); + const range = max - min || 1; + const step = width / (values.length - 1 || 1); + const points = values + .map((v, i) => `${(i * step).toFixed(1)},${(height - ((v - min) / range) * height).toFixed(1)}`) + .join(" "); + return ( + + + + + ); + } + + // ─── Capability icons (glyphs, not icons-as-an-iconset) ──────────────── + + const CAP_GLYPH = { + table: "▦", + timeline: "│", + conversation: "❝", + ledger: "$", + gallery: "▥", + map: "◎", + calendar: "▤", + chart: "↟", + reader: "¶", + }; + const CAP_LABEL = { + table: "Table", + timeline: "Timeline", + conversation: "Conversation", + ledger: "Ledger", + gallery: "Gallery", + map: "Map", + calendar: "Calendar", + chart: "Chart", + reader: "Reader", + }; + + // ─── Useful: useKeyboard for cmd-k ───────────────────────────────────── + + function useGlobalKey(key, modifiers, handler) { + useEffect(() => { + function onKey(e) { + const meta = modifiers.includes("meta") ? e.metaKey || e.ctrlKey : true; + if (e.key.toLowerCase() === key.toLowerCase() && meta) { + e.preventDefault(); + handler(e); + } + } + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }, [key, handler, modifiers]); + } + + // ─── Stream-record utilities ─────────────────────────────────────────── + + /** Find the timestamp field in a stream's records by checking the schema. */ + function getTimeField(stream) { + const fs = stream.schema?.fields ?? []; + return (fs.find((f) => f.type === "timestamp") ?? fs.find((f) => /date|ts|at_$|^at$|night_of/i.test(f.name)))?.name; + } + function getRecordTime(stream, record) { + const f = getTimeField(stream); + return f ? record[f] : null; + } + + /** Returns the field labelled as "title" for the record, with sensible fallbacks. */ + function getRecordTitle(stream, record) { + return ( + record.subject ?? record.title ?? record.merchant ?? record.text ?? record.caption ?? record.snippet ?? record.id + ); + } + window.PDPPPrim = { + fmtRelative, + fmtClock, + fmtDate, + fmtDay, + fmtCurrency, + fmtDuration, + fmtDistance, + Avatar, + Heatmap, + Sparkline, + CAP_GLYPH, + CAP_LABEL, + useGlobalKey, + getTimeField, + getRecordTime, + getRecordTitle, + initials, + NOW, + }; })(); diff --git a/docs/design-system/ink-carbon/project/explorer/query-bar.jsx b/docs/design-system/ink-carbon/project/explorer/query-bar.jsx index f0b2621e7..159075072 100644 --- a/docs/design-system/ink-carbon/project/explorer/query-bar.jsx +++ b/docs/design-system/ink-carbon/project/explorer/query-bar.jsx @@ -8,187 +8,212 @@ * below as the user types and can be added via ↓+Enter or by clicking. */ -;(() => { +(() => { + const { useState, useEffect, useRef, useMemo } = React; + const { suggestChips } = window.PDPP_QUERY; -const { useState, useEffect, useRef, useMemo } = React; -const { suggestChips } = window.PDPP_QUERY; - -function chipLabel(c) { - if (c.field === "stream") { - const v = Array.isArray(c.value) ? c.value.join(", ") : c.value; - return { field: "stream", op: "in", value: v }; + function chipLabel(c) { + if (c.field === "stream") { + const v = Array.isArray(c.value) ? c.value.join(", ") : c.value; + return { field: "stream", op: "in", value: v }; + } + if (c.field === "amount") return { field: "amount", op: c.op, value: `$${c.value}` }; + if (c.field === "month") return { field: "month", op: "", value: monthLabel(c.value) }; + if (c.field === "year") return { field: "year", op: "", value: String(c.value) }; + if (c.field === "has") return { field: "has", op: "", value: c.value }; + if (c.field === "category") return { field: "category", op: "", value: c.value }; + return { field: c.field, op: "", value: String(c.value) }; + } + function monthLabel(s) { + if (!s) return ""; + const d = new Date(s + "-15"); + return d.toLocaleDateString("en-US", { month: "short", year: "numeric" }).toLowerCase(); } - if (c.field === "amount") return { field: "amount", op: c.op, value: `$${c.value}` }; - if (c.field === "month") return { field: "month", op: "", value: monthLabel(c.value) }; - if (c.field === "year") return { field: "year", op: "", value: String(c.value) }; - if (c.field === "has") return { field: "has", op: "", value: c.value }; - if (c.field === "category") return { field: "category", op: "", value: c.value }; - return { field: c.field, op: "", value: String(c.value) }; -} -function monthLabel(s) { - if (!s) return ""; - const d = new Date(s + "-15"); - return d.toLocaleDateString("en-US", { month: "short", year: "numeric" }).toLowerCase(); -} -function QueryBar({ query, onChange, streams, mode, onModeChange, focused, onFocusedChange }) { - const [text, setText] = useState(query.text ?? ""); - const [suggestionIdx, setSuggestionIdx] = useState(0); - const inputRef = useRef(null); + function QueryBar({ query, onChange, streams, mode, onModeChange, focused, onFocusedChange }) { + const [text, setText] = useState(query.text ?? ""); + const [suggestionIdx, setSuggestionIdx] = useState(0); + const inputRef = useRef(null); - useEffect(() => { setText(query.text ?? ""); }, [query.text]); + useEffect(() => { + setText(query.text ?? ""); + }, [query.text]); - const suggestions = useMemo(() => { - if (!text.trim()) return []; - return suggestChips(text, streams); - }, [text, streams]); + const suggestions = useMemo(() => { + if (!text.trim()) return []; + return suggestChips(text, streams); + }, [text, streams]); - useEffect(() => { setSuggestionIdx(0); }, [suggestions.length]); + useEffect(() => { + setSuggestionIdx(0); + }, [suggestions.length]); - function commitText(newText) { - onChange({ ...query, text: newText }); - } - function addChip(chip) { - // Replace existing chip with same field+op (avoid dup `from:` etc), except for `stream:` which we accumulate. - let chips = [...query.chips]; - if (chip.field === "stream") { - const existing = chips.find((c) => c.field === "stream"); - if (existing) { - const merged = Array.isArray(existing.value) ? existing.value : [existing.value]; - const newV = Array.isArray(chip.value) ? chip.value : [chip.value]; - existing.value = Array.from(new Set([...merged, ...newV])); + function commitText(newText) { + onChange({ ...query, text: newText }); + } + function addChip(chip) { + // Replace existing chip with same field+op (avoid dup `from:` etc), except for `stream:` which we accumulate. + let chips = [...query.chips]; + if (chip.field === "stream") { + const existing = chips.find((c) => c.field === "stream"); + if (existing) { + const merged = Array.isArray(existing.value) ? existing.value : [existing.value]; + const newV = Array.isArray(chip.value) ? chip.value : [chip.value]; + existing.value = Array.from(new Set([...merged, ...newV])); + } else { + chips.push(chip); + } } else { + chips = chips.filter((c) => !(c.field === chip.field && c.op === chip.op)); chips.push(chip); } - } else { - chips = chips.filter((c) => !(c.field === chip.field && c.op === chip.op)); - chips.push(chip); + onChange({ chips, text: "" }); + setText(""); + inputRef.current?.focus(); } - onChange({ chips, text: "" }); - setText(""); - inputRef.current?.focus(); - } - function removeChipAt(i) { - const chips = [...query.chips]; - chips.splice(i, 1); - onChange({ ...query, chips }); - inputRef.current?.focus(); - } - - function onKeyDown(e) { - if (e.key === "Backspace" && text === "" && query.chips.length > 0) { - removeChipAt(query.chips.length - 1); - return; - } - if (suggestions.length === 0) { - if (e.key === "Enter") commitText(text); - return; + function removeChipAt(i) { + const chips = [...query.chips]; + chips.splice(i, 1); + onChange({ ...query, chips }); + inputRef.current?.focus(); } - if (e.key === "ArrowDown") { e.preventDefault(); setSuggestionIdx((i) => Math.min(i + 1, suggestions.length - 1)); } - if (e.key === "ArrowUp") { e.preventDefault(); setSuggestionIdx((i) => Math.max(i - 1, 0)); } - if (e.key === "Enter") { - e.preventDefault(); - const sel = suggestions[suggestionIdx]; - if (sel) addChip(sel.chip); - else commitText(text); + + function onKeyDown(e) { + if (e.key === "Backspace" && text === "" && query.chips.length > 0) { + removeChipAt(query.chips.length - 1); + return; + } + if (suggestions.length === 0) { + if (e.key === "Enter") commitText(text); + return; + } + if (e.key === "ArrowDown") { + e.preventDefault(); + setSuggestionIdx((i) => Math.min(i + 1, suggestions.length - 1)); + } + if (e.key === "ArrowUp") { + e.preventDefault(); + setSuggestionIdx((i) => Math.max(i - 1, 0)); + } + if (e.key === "Enter") { + e.preventDefault(); + const sel = suggestions[suggestionIdx]; + if (sel) addChip(sel.chip); + else commitText(text); + } + if (e.key === "Escape") { + setText(""); + commitText(""); + } } - if (e.key === "Escape") { setText(""); commitText(""); } - } - // Debounce text changes into the query - useEffect(() => { - const t = setTimeout(() => { if (text !== query.text) commitText(text); }, 180); - return () => clearTimeout(t); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [text]); + // Debounce text changes into the query + useEffect(() => { + const t = setTimeout(() => { + if (text !== query.text) commitText(text); + }, 180); + return () => clearTimeout(t); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [text]); - const showSuggest = focused && suggestions.length > 0; + const showSuggest = focused && suggestions.length > 0; - return ( -
-
inputRef.current?.focus()} - > - {query.chips.map((c, i) => { - const lab = chipLabel(c); - return ( - - ); - })} - setTimeout(() => onFocusedChange(false), 120)} - onChange={(e) => setText(e.target.value)} - onFocus={() => onFocusedChange(true)} - onKeyDown={onKeyDown} - placeholder={query.chips.length ? "narrow…" : "Search everything"} - ref={inputRef} - spellCheck="false" - type="text" - value={text} - /> - - / - - - {["lex", "sem", "hyb"].map((m) => ( - - ))} - -
- {showSuggest ? ( -
-
add filter
- {suggestions.map((s, i) => ( + return ( +
+
inputRef.current?.focus()}> + {query.chips.map((c, i) => { + const lab = chipLabel(c); + return ( + + ); + })} + setTimeout(() => onFocusedChange(false), 120)} + onChange={(e) => setText(e.target.value)} + onFocus={() => onFocusedChange(true)} + onKeyDown={onKeyDown} + placeholder={query.chips.length ? "narrow…" : "Search everything"} + ref={inputRef} + spellCheck="false" + type="text" + value={text} + /> + + / + + + {["lex", "sem", "hyb"].map((m) => ( + + ))} + +
+ {showSuggest ? ( +
+
add filter
+ {suggestions.map((s, i) => ( +
{ + e.preventDefault(); + addChip(s.chip); + }} + onMouseEnter={() => setSuggestionIdx(i)} + > + {s.chip.field} + {s.label.replace(/^[^:]+:\s*/, "")} + {s.hint} +
+ ))} +
{ e.preventDefault(); addChip(s.chip); }} - onMouseEnter={() => setSuggestionIdx(i)} + onMouseDown={(e) => { + e.preventDefault(); + commitText(text); + }} + style={{ paddingTop: "0.5rem", paddingBottom: "0.5rem" }} > - {s.chip.field} - {s.label.replace(/^[^:]+:\s*/, "")} - {s.hint} + text + search for “{text}” + + +
- ))} -
-
{ e.preventDefault(); commitText(text); }} style={{ paddingTop: "0.5rem", paddingBottom: "0.5rem" }}> - text - search for “{text}” - - -
-
- ) : null} -
- ); -} - -window.QueryBar = QueryBar; -window.queryChipLabel = chipLabel; + ) : null} +
+ ); + } + window.QueryBar = QueryBar; + window.queryChipLabel = chipLabel; })(); diff --git a/docs/design-system/ink-carbon/project/explorer/query.js b/docs/design-system/ink-carbon/project/explorer/query.js index e4cadc346..8b8670f44 100644 --- a/docs/design-system/ink-carbon/project/explorer/query.js +++ b/docs/design-system/ink-carbon/project/explorer/query.js @@ -21,252 +21,281 @@ * schemas in the matched streams) to pick the right view. */ -;(() => { - -const FIELD_ALIASES = { - from: ["from", "sender", "author", "actor", "user"], - to: ["to", "recipient", "recipients"], - text: ["subject", "snippet", "body", "text", "message", "content", "title", "caption", "merchant", "memo"], - amount: ["amount", "value", "total"], - cat: ["category"], - type: ["type"], -}; - -function normPerson(p) { - if (p == null) return ""; - if (Array.isArray(p)) return p.map(normPerson).join(" "); - return String(p).toLowerCase(); -} +(() => { + const FIELD_ALIASES = { + from: ["from", "sender", "author", "actor", "user"], + to: ["to", "recipient", "recipients"], + text: ["subject", "snippet", "body", "text", "message", "content", "title", "caption", "merchant", "memo"], + amount: ["amount", "value", "total"], + cat: ["category"], + type: ["type"], + }; -/** Reduce a person field's raw text to a short, human-friendly first token. - * Handles "Maya Chen ", "the owner@example.com", and arrays. - */ -function firstNameToken(person) { - if (!person) return ""; - if (Array.isArray(person)) person = person[0]; - let s = String(person).replace(/<[^>]+>/g, "").trim(); - if (!s) return ""; - // If it's an email-only string, take the local part and split on punctuation - if (s.includes("@") && !/\s/.test(s)) { - s = s.split("@")[0].replace(/[._-]+/g, " "); + function normPerson(p) { + if (p == null) return ""; + if (Array.isArray(p)) return p.map(normPerson).join(" "); + return String(p).toLowerCase(); } - const tok = s.split(/\s+/)[0]; - return tok.toLowerCase(); -} -function recordMatchesChip(stream, r, chip) { - switch (chip.field) { - case "stream": { - const v = Array.isArray(chip.value) ? chip.value : [chip.value]; - return v.some((x) => stream.connector_id === x || stream.name === x || `${stream.connector_id}/${stream.name}` === x); - } - case "connection": { - const v = Array.isArray(chip.value) ? chip.value : [chip.value]; - return v.includes(stream.connection_id); - } - case "from": { - const target = String(chip.value).toLowerCase(); - const candidateFields = stream.schema.fields.filter((f) => - FIELD_ALIASES.from.some((alias) => f.name.toLowerCase().includes(alias)) || - f.type === "person" || f.type === "person[]" - ); - return candidateFields.some((f) => normPerson(r[f.name]).includes(target)); - } - case "with": case "to": { - const target = String(chip.value).toLowerCase(); - // matches author OR recipient OR text body - const fs = stream.schema.fields; - return fs.some((f) => - (f.type === "person" || f.type === "person[]" || f.type === "text") && - normPerson(r[f.name]).includes(target) - ); - } - case "month": { - const fs = stream.schema.fields; - const tf = fs.find((f) => f.type === "timestamp")?.name; - return tf ? r[tf]?.slice(0, 7) === chip.value : false; - } - case "year": { - const fs = stream.schema.fields; - const tf = fs.find((f) => f.type === "timestamp")?.name; - return tf ? r[tf]?.slice(0, 4) === String(chip.value) : false; - } - case "since": { - const fs = stream.schema.fields; - const tf = fs.find((f) => f.type === "timestamp")?.name; - return tf ? new Date(r[tf]).getTime() >= new Date(chip.value).getTime() : false; - } - case "amount": { - const fs = stream.schema.fields; - const af = fs.find((f) => f.type === "currency")?.name ?? fs.find((f) => /amount/i.test(f.name))?.name; - if (!af) return false; - const x = Math.abs(r[af] ?? 0); - if (chip.op === ">") return x > Number(chip.value); - if (chip.op === "<") return x < Number(chip.value); - if (chip.op === "=") return x === Number(chip.value); - return false; + /** Reduce a person field's raw text to a short, human-friendly first token. + * Handles "Maya Chen ", "the owner@example.com", and arrays. + */ + function firstNameToken(person) { + if (!person) return ""; + if (Array.isArray(person)) person = person[0]; + let s = String(person) + .replace(/<[^>]+>/g, "") + .trim(); + if (!s) return ""; + // If it's an email-only string, take the local part and split on punctuation + if (s.includes("@") && !/\s/.test(s)) { + s = s.split("@")[0].replace(/[._-]+/g, " "); } - case "category": { - const fs = stream.schema.fields; - const cf = fs.find((f) => /category/i.test(f.name))?.name; - return cf ? r[cf] === chip.value : false; - } - case "type": { - return r.type === chip.value; - } - case "channel": { - const target = String(chip.value).toLowerCase(); - return ["channel", "channel_id"].some((k) => String(r[k] ?? "").toLowerCase().includes(target)); - } - case "has": { - if (chip.value === "image") { - return stream.schema.fields.some((f) => f.type === "blob" && (f.media_type ?? "").startsWith("image/")) || - stream.schema.fields.some((f) => /thumb|image|photo|picture/i.test(f.name) && (f.type === "blob" || f.type === "url") && r[f.name]); + const tok = s.split(/\s+/)[0]; + return tok.toLowerCase(); + } + + function recordMatchesChip(stream, r, chip) { + switch (chip.field) { + case "stream": { + const v = Array.isArray(chip.value) ? chip.value : [chip.value]; + return v.some( + (x) => stream.connector_id === x || stream.name === x || `${stream.connector_id}/${stream.name}` === x + ); + } + case "connection": { + const v = Array.isArray(chip.value) ? chip.value : [chip.value]; + return v.includes(stream.connection_id); + } + case "from": { + const target = String(chip.value).toLowerCase(); + const candidateFields = stream.schema.fields.filter( + (f) => + FIELD_ALIASES.from.some((alias) => f.name.toLowerCase().includes(alias)) || + f.type === "person" || + f.type === "person[]" + ); + return candidateFields.some((f) => normPerson(r[f.name]).includes(target)); + } + case "with": + case "to": { + const target = String(chip.value).toLowerCase(); + // matches author OR recipient OR text body + const fs = stream.schema.fields; + return fs.some( + (f) => + (f.type === "person" || f.type === "person[]" || f.type === "text") && + normPerson(r[f.name]).includes(target) + ); + } + case "month": { + const fs = stream.schema.fields; + const tf = fs.find((f) => f.type === "timestamp")?.name; + return tf ? r[tf]?.slice(0, 7) === chip.value : false; + } + case "year": { + const fs = stream.schema.fields; + const tf = fs.find((f) => f.type === "timestamp")?.name; + return tf ? r[tf]?.slice(0, 4) === String(chip.value) : false; } - if (chip.value === "geo") { - return ["lat","latitude"].some((n) => r[n] != null); + case "since": { + const fs = stream.schema.fields; + const tf = fs.find((f) => f.type === "timestamp")?.name; + return tf ? new Date(r[tf]).getTime() >= new Date(chip.value).getTime() : false; } - if (chip.value === "attachment") { - return r.has_attachment === true; + case "amount": { + const fs = stream.schema.fields; + const af = fs.find((f) => f.type === "currency")?.name ?? fs.find((f) => /amount/i.test(f.name))?.name; + if (!af) return false; + const x = Math.abs(r[af] ?? 0); + if (chip.op === ">") return x > Number(chip.value); + if (chip.op === "<") return x < Number(chip.value); + if (chip.op === "=") return x === Number(chip.value); + return false; } - return false; + case "category": { + const fs = stream.schema.fields; + const cf = fs.find((f) => /category/i.test(f.name))?.name; + return cf ? r[cf] === chip.value : false; + } + case "type": { + return r.type === chip.value; + } + case "channel": { + const target = String(chip.value).toLowerCase(); + return ["channel", "channel_id"].some((k) => + String(r[k] ?? "") + .toLowerCase() + .includes(target) + ); + } + case "has": { + if (chip.value === "image") { + return ( + stream.schema.fields.some((f) => f.type === "blob" && (f.media_type ?? "").startsWith("image/")) || + stream.schema.fields.some( + (f) => /thumb|image|photo|picture/i.test(f.name) && (f.type === "blob" || f.type === "url") && r[f.name] + ) + ); + } + if (chip.value === "geo") { + return ["lat", "latitude"].some((n) => r[n] != null); + } + if (chip.value === "attachment") { + return r.has_attachment === true; + } + return false; + } + default: + return true; } - default: - return true; } -} -function textMatches(stream, r, text) { - if (!text || !text.trim()) return true; - const q = text.toLowerCase(); - const fields = stream.schema.fields.filter((f) => - ["text", "person"].includes(f.type) || - /title|subject|snippet|body|text|caption|merchant|memo|name/i.test(f.name) - ); - for (const f of fields) { - const v = r[f.name]; - if (typeof v === "string" && v.toLowerCase().includes(q)) return true; + function textMatches(stream, r, text) { + if (!text || !text.trim()) return true; + const q = text.toLowerCase(); + const fields = stream.schema.fields.filter( + (f) => + ["text", "person"].includes(f.type) || + /title|subject|snippet|body|text|caption|merchant|memo|name/i.test(f.name) + ); + for (const f of fields) { + const v = r[f.name]; + if (typeof v === "string" && v.toLowerCase().includes(q)) return true; + } + return false; } - return false; -} -/** Run a query and return flat hits across streams. */ -function runQuery(query, allStreams) { - const hits = []; - for (const s of allStreams) { - // Cheap stream-level rejection: if there's a `stream:` chip and this stream doesn't match, skip. - const streamChips = query.chips.filter((c) => c.field === "stream" || c.field === "connection"); - if (streamChips.length && !streamChips.every((c) => recordMatchesChip(s, s.records[0] ?? {}, c))) continue; + /** Run a query and return flat hits across streams. */ + function runQuery(query, allStreams) { + const hits = []; + for (const s of allStreams) { + // Cheap stream-level rejection: if there's a `stream:` chip and this stream doesn't match, skip. + const streamChips = query.chips.filter((c) => c.field === "stream" || c.field === "connection"); + if (streamChips.length && !streamChips.every((c) => recordMatchesChip(s, s.records[0] ?? {}, c))) continue; - for (const r of s.records) { - // Apply non-stream chips - const passes = query.chips - .filter((c) => c.field !== "stream" && c.field !== "connection") - .every((c) => recordMatchesChip(s, r, c)); - if (!passes) continue; - if (!textMatches(s, r, query.text)) continue; - hits.push({ stream: s, record: r }); + for (const r of s.records) { + // Apply non-stream chips + const passes = query.chips + .filter((c) => c.field !== "stream" && c.field !== "connection") + .every((c) => recordMatchesChip(s, r, c)); + if (!passes) continue; + if (!textMatches(s, r, query.text)) continue; + hits.push({ stream: s, record: r }); + } } + // Sort by time field if available, otherwise by id (stable enough for the prototype) + hits.sort((a, b) => { + const ta = recordTime(a.stream, a.record); + const tb = recordTime(b.stream, b.record); + return (tb ?? 0) - (ta ?? 0); + }); + return hits; } - // Sort by time field if available, otherwise by id (stable enough for the prototype) - hits.sort((a, b) => { - const ta = recordTime(a.stream, a.record); - const tb = recordTime(b.stream, b.record); - return (tb ?? 0) - (ta ?? 0); - }); - return hits; -} -function recordTime(s, r) { - const tf = s.schema.fields.find((f) => f.type === "timestamp")?.name; - return tf && r[tf] ? new Date(r[tf]).getTime() : null; -} + function recordTime(s, r) { + const tf = s.schema.fields.find((f) => f.type === "timestamp")?.name; + return tf && r[tf] ? new Date(r[tf]).getTime() : null; + } -/* ─── facets ─────────────────────────────────────────────────────────── - * Given a set of hits, compute small facet breakdowns the user can click - * to add chips. All facets are derived from schema, not connector id. - */ -function computeFacets(hits) { - const streamCounts = new Map(); - const monthCounts = new Map(); - const peopleCounts = new Map(); - const catCounts = new Map(); - for (const { stream, record } of hits) { - streamCounts.set(stream, (streamCounts.get(stream) ?? 0) + 1); - const t = recordTime(stream, record); - if (t) { - const m = new Date(t).toISOString().slice(0, 7); - monthCounts.set(m, (monthCounts.get(m) ?? 0) + 1); - } - for (const f of stream.schema.fields) { - if (f.type === "person" || f.type === "person[]") { - const v = record[f.name]; - const list = Array.isArray(v) ? v : v ? [v] : []; - for (const p of list) { - const name = String(p).replace(/<[^>]+>/g, "").trim(); - if (name) peopleCounts.set(name, (peopleCounts.get(name) ?? 0) + 1); - } + /* ─── facets ─────────────────────────────────────────────────────────── + * Given a set of hits, compute small facet breakdowns the user can click + * to add chips. All facets are derived from schema, not connector id. + */ + function computeFacets(hits) { + const streamCounts = new Map(); + const monthCounts = new Map(); + const peopleCounts = new Map(); + const catCounts = new Map(); + for (const { stream, record } of hits) { + streamCounts.set(stream, (streamCounts.get(stream) ?? 0) + 1); + const t = recordTime(stream, record); + if (t) { + const m = new Date(t).toISOString().slice(0, 7); + monthCounts.set(m, (monthCounts.get(m) ?? 0) + 1); } - if (/category/i.test(f.name)) { - const v = record[f.name]; - if (v) catCounts.set(v, (catCounts.get(v) ?? 0) + 1); + for (const f of stream.schema.fields) { + if (f.type === "person" || f.type === "person[]") { + const v = record[f.name]; + const list = Array.isArray(v) ? v : v ? [v] : []; + for (const p of list) { + const name = String(p) + .replace(/<[^>]+>/g, "") + .trim(); + if (name) peopleCounts.set(name, (peopleCounts.get(name) ?? 0) + 1); + } + } + if (/category/i.test(f.name)) { + const v = record[f.name]; + if (v) catCounts.set(v, (catCounts.get(v) ?? 0) + 1); + } } } + return { + streams: [...streamCounts.entries()].sort((a, b) => b[1] - a[1]), + months: [...monthCounts.entries()].sort((a, b) => b[0].localeCompare(a[0])), + people: [...peopleCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 8), + categories: [...catCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 8), + }; } - return { - streams: [...streamCounts.entries()].sort((a, b) => b[1] - a[1]), - months: [...monthCounts.entries()].sort((a, b) => b[0].localeCompare(a[0])), - people: [...peopleCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 8), - categories: [...catCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 8), - }; -} -/* ─── chip suggestions for typed text ───────────────────────────────── - * Tokens a user might type at the end of the query box, before adding - * a chip. Returns up to 6 suggestions. - */ -function suggestChips(text, allStreams) { - const t = (text ?? "").trim().toLowerCase(); - if (!t) return []; - const out = []; + /* ─── chip suggestions for typed text ───────────────────────────────── + * Tokens a user might type at the end of the query box, before adding + * a chip. Returns up to 6 suggestions. + */ + function suggestChips(text, allStreams) { + const t = (text ?? "").trim().toLowerCase(); + if (!t) return []; + const out = []; - // Stream suggestions - const seenStreams = new Set(); - for (const s of allStreams) { - if (s.connector_id.includes(t) || s.name.includes(t) || s.title.toLowerCase().includes(t)) { - const key = s.connector_id; - if (!seenStreams.has(key)) { - seenStreams.add(key); - out.push({ kind: "chip", chip: { field: "stream", op: "in", value: [key] }, label: `stream: ${key}`, hint: s.title }); + // Stream suggestions + const seenStreams = new Set(); + for (const s of allStreams) { + if (s.connector_id.includes(t) || s.name.includes(t) || s.title.toLowerCase().includes(t)) { + const key = s.connector_id; + if (!seenStreams.has(key)) { + seenStreams.add(key); + out.push({ + kind: "chip", + chip: { field: "stream", op: "in", value: [key] }, + label: `stream: ${key}`, + hint: s.title, + }); + } } } - } - // Person suggestions (across streams) - const seenPpl = new Set(); - for (const s of allStreams) { - const personFields = s.schema.fields.filter((f) => f.type === "person" || f.type === "person[]"); - for (const r of s.records) { - for (const f of personFields) { - const v = r[f.name]; - const list = Array.isArray(v) ? v : v ? [v] : []; - for (const p of list) { - const display = String(p).replace(/<[^>]+>/g, "").trim(); - const first = firstNameToken(p); - if (!first || !first.includes(t)) continue; - if (seenPpl.has(first)) continue; - seenPpl.add(first); - out.push({ kind: "chip", chip: { field: "from", op: "is", value: first }, label: `from: ${first}`, hint: display }); - if (out.length > 4) break; + // Person suggestions (across streams) + const seenPpl = new Set(); + for (const s of allStreams) { + const personFields = s.schema.fields.filter((f) => f.type === "person" || f.type === "person[]"); + for (const r of s.records) { + for (const f of personFields) { + const v = r[f.name]; + const list = Array.isArray(v) ? v : v ? [v] : []; + for (const p of list) { + const display = String(p) + .replace(/<[^>]+>/g, "") + .trim(); + const first = firstNameToken(p); + if (!first || !first.includes(t)) continue; + if (seenPpl.has(first)) continue; + seenPpl.add(first); + out.push({ + kind: "chip", + chip: { field: "from", op: "is", value: first }, + label: `from: ${first}`, + hint: display, + }); + if (out.length > 4) break; + } + if (out.length > 6) break; } if (out.length > 6) break; } if (out.length > 6) break; } - if (out.length > 6) break; + return out.slice(0, 6); } - return out.slice(0, 6); -} - -window.PDPP_QUERY = { runQuery, computeFacets, suggestChips, recordTime, firstNameToken }; + window.PDPP_QUERY = { runQuery, computeFacets, suggestChips, recordTime, firstNameToken }; })(); diff --git a/docs/design-system/ink-carbon/project/explorer/tweaks-panel.jsx b/docs/design-system/ink-carbon/project/explorer/tweaks-panel.jsx index cb0b0f598..a71dc28a4 100644 --- a/docs/design-system/ink-carbon/project/explorer/tweaks-panel.jsx +++ b/docs/design-system/ink-carbon/project/explorer/tweaks-panel.jsx @@ -167,13 +167,12 @@ function useTweaks(defaults) { // useState-style call doesn't write a "[object Object]" key into the persisted // JSON block. const setTweak = React.useCallback((keyOrEdits, val) => { - const edits = typeof keyOrEdits === 'object' && keyOrEdits !== null - ? keyOrEdits : { [keyOrEdits]: val }; + const edits = typeof keyOrEdits === "object" && keyOrEdits !== null ? keyOrEdits : { [keyOrEdits]: val }; setValues((prev) => ({ ...prev, ...edits })); - window.parent.postMessage({ type: '__edit_mode_set_keys', edits }, '*'); + window.parent.postMessage({ type: "__edit_mode_set_keys", edits }, "*"); // Same-window signal so in-page listeners (deck-stage rail thumbnails) // can react — the parent message only reaches the host, not peers. - window.dispatchEvent(new CustomEvent('tweakchange', { detail: edits })); + window.dispatchEvent(new CustomEvent("tweakchange", { detail: edits })); }, []); return [values, setTweak]; } @@ -185,7 +184,7 @@ function useTweaks(defaults) { // The close button posts __edit_mode_dismissed so the host's toolbar toggle // flips off in lockstep; the host echoes __deactivate_edit_mode back which // is what actually hides the panel. -function TweaksPanel({ title = 'Tweaks', children }) { +function TweaksPanel({ title = "Tweaks", children }) { const [open, setOpen] = React.useState(false); const dragRef = React.useRef(null); const offsetRef = React.useRef({ x: 16, y: 16 }); @@ -194,23 +193,24 @@ function TweaksPanel({ title = 'Tweaks', children }) { const clampToViewport = React.useCallback(() => { const panel = dragRef.current; if (!panel) return; - const w = panel.offsetWidth, h = panel.offsetHeight; + const w = panel.offsetWidth, + h = panel.offsetHeight; const maxRight = Math.max(PAD, window.innerWidth - w - PAD); const maxBottom = Math.max(PAD, window.innerHeight - h - PAD); offsetRef.current = { x: Math.min(maxRight, Math.max(PAD, offsetRef.current.x)), y: Math.min(maxBottom, Math.max(PAD, offsetRef.current.y)), }; - panel.style.right = offsetRef.current.x + 'px'; - panel.style.bottom = offsetRef.current.y + 'px'; + panel.style.right = offsetRef.current.x + "px"; + panel.style.bottom = offsetRef.current.y + "px"; }, []); React.useEffect(() => { if (!open) return; clampToViewport(); - if (typeof ResizeObserver === 'undefined') { - window.addEventListener('resize', clampToViewport); - return () => window.removeEventListener('resize', clampToViewport); + if (typeof ResizeObserver === "undefined") { + window.addEventListener("resize", clampToViewport); + return () => window.removeEventListener("resize", clampToViewport); } const ro = new ResizeObserver(clampToViewport); ro.observe(document.documentElement); @@ -220,24 +220,25 @@ function TweaksPanel({ title = 'Tweaks', children }) { React.useEffect(() => { const onMsg = (e) => { const t = e?.data?.type; - if (t === '__activate_edit_mode') setOpen(true); - else if (t === '__deactivate_edit_mode') setOpen(false); + if (t === "__activate_edit_mode") setOpen(true); + else if (t === "__deactivate_edit_mode") setOpen(false); }; - window.addEventListener('message', onMsg); - window.parent.postMessage({ type: '__edit_mode_available' }, '*'); - return () => window.removeEventListener('message', onMsg); + window.addEventListener("message", onMsg); + window.parent.postMessage({ type: "__edit_mode_available" }, "*"); + return () => window.removeEventListener("message", onMsg); }, []); const dismiss = () => { setOpen(false); - window.parent.postMessage({ type: '__edit_mode_dismissed' }, '*'); + window.parent.postMessage({ type: "__edit_mode_dismissed" }, "*"); }; const onDragStart = (e) => { const panel = dragRef.current; if (!panel) return; const r = panel.getBoundingClientRect(); - const sx = e.clientX, sy = e.clientY; + const sx = e.clientX, + sy = e.clientY; const startRight = window.innerWidth - r.right; const startBottom = window.innerHeight - r.bottom; const move = (ev) => { @@ -248,28 +249,35 @@ function TweaksPanel({ title = 'Tweaks', children }) { clampToViewport(); }; const up = () => { - window.removeEventListener('mousemove', move); - window.removeEventListener('mouseup', up); + window.removeEventListener("mousemove", move); + window.removeEventListener("mouseup", up); }; - window.addEventListener('mousemove', move); - window.addEventListener('mouseup', up); + window.addEventListener("mousemove", move); + window.addEventListener("mouseup", up); }; if (!open) return null; return ( <> -
+
{title} - -
-
- {children} +
+
{children}
); @@ -288,7 +296,7 @@ function TweakSection({ label, children }) { function TweakRow({ label, value, children, inline = false }) { return ( -
+
{label} {value != null && {value}} @@ -300,11 +308,18 @@ function TweakRow({ label, value, children, inline = false }) { // ── Controls ──────────────────────────────────────────────────────────────── -function TweakSlider({ label, value, min = 0, max = 100, step = 1, unit = '', onChange }) { +function TweakSlider({ label, value, min = 0, max = 100, step = 1, unit = "", onChange }) { return ( - onChange(Number(e.target.value))} /> + onChange(Number(e.target.value))} + /> ); } @@ -312,10 +327,19 @@ function TweakSlider({ label, value, min = 0, max = 100, step = 1, unit = '', on function TweakToggle({ label, value, onChange }) { return (
-
{label}
- +
+ {label} +
+
); } @@ -333,21 +357,23 @@ function TweakRadio({ label, value, options, onChange }) { // to its own padding, and 11.5px system-ui averages ~6.3px/char — so 2 // options fit ~16 chars each, 3 fit ~10. Past that (or >3 options), fall // back to a dropdown rather than wrap. - const labelLen = (o) => String(typeof o === 'object' ? o.label : o).length; + const labelLen = (o) => String(typeof o === "object" ? o.label : o).length; const maxLen = options.reduce((m, o) => Math.max(m, labelLen(o)), 0); const fitsAsSegments = maxLen <= ({ 2: 16, 3: 10 }[options.length] ?? 0); if (!fitsAsSegments) { // onChange(e.target.value)}> {options.map((o) => { - const v = typeof o === 'object' ? o.value : o; - const l = typeof o === 'object' ? o.label : o; - return ; + const v = typeof o === "object" ? o.value : o; + const l = typeof o === "object" ? o.label : o; + return ( + + ); })} @@ -409,13 +444,18 @@ function TweakSelect({ label, value, options, onChange }) { function TweakText({ label, value, placeholder, onChange }) { return ( - onChange(e.target.value)} /> + onChange(e.target.value)} + /> ); } -function TweakNumber({ label, value, min, max, step = 1, unit = '', onChange }) { +function TweakNumber({ label, value, min, max, step = 1, unit = "", onChange }) { const clamp = (n) => { if (min != null && n < min) return min; if (max != null && n > max) return max; @@ -425,7 +465,7 @@ function TweakNumber({ label, value, min, max, step = 1, unit = '', onChange }) const onScrubStart = (e) => { e.preventDefault(); startRef.current = { x: e.clientX, val: value }; - const decimals = (String(step).split('.')[1] || '').length; + const decimals = (String(step).split(".")[1] || "").length; const move = (ev) => { const dx = ev.clientX - startRef.current.x; const raw = startRef.current.val + dx * step; @@ -433,17 +473,25 @@ function TweakNumber({ label, value, min, max, step = 1, unit = '', onChange }) onChange(clamp(Number(snapped.toFixed(decimals)))); }; const up = () => { - window.removeEventListener('pointermove', move); - window.removeEventListener('pointerup', up); + window.removeEventListener("pointermove", move); + window.removeEventListener("pointerup", up); }; - window.addEventListener('pointermove', move); - window.addEventListener('pointerup', up); + window.addEventListener("pointermove", move); + window.addEventListener("pointerup", up); }; return (
- {label} - onChange(clamp(Number(e.target.value)))} /> + + {label} + + onChange(clamp(Number(e.target.value)))} + /> {unit && {unit}}
); @@ -453,19 +501,26 @@ function TweakNumber({ label, value, min, max, step = 1, unit = '', onChange }) // read on both #111 and #fafafa without per-option configuration. Hex input // only (#rgb / #rrggbb); named or rgb()/hsl() colors fall through to "light". function __twkIsLight(hex) { - const h = String(hex).replace('#', ''); - const x = h.length === 3 ? h.replace(/./g, (c) => c + c) : h.padEnd(6, '0'); + const h = String(hex).replace("#", ""); + const x = h.length === 3 ? h.replace(/./g, (c) => c + c) : h.padEnd(6, "0"); const n = parseInt(x.slice(0, 6), 16); if (Number.isNaN(n)) return true; - const r = (n >> 16) & 255, g = (n >> 8) & 255, b = n & 255; + const r = (n >> 16) & 255, + g = (n >> 8) & 255, + b = n & 255; return r * 299 + g * 587 + b * 114 > 148000; } const __TwkCheck = ({ light }) => ( ); @@ -479,9 +534,10 @@ function TweakColor({ label, value, options, onChange }) { if (!options || !options.length) { return (
-
{label}
- onChange(e.target.value)} /> +
+ {label} +
+ onChange(e.target.value)} />
); } @@ -499,14 +555,23 @@ function TweakColor({ label, value, options, onChange }) { const sup = rest.slice(0, 4); const on = key(o) === cur; return ( - + ); } Object.assign(window, { - useTweaks, TweaksPanel, TweakSection, TweakRow, - TweakSlider, TweakToggle, TweakRadio, TweakSelect, - TweakText, TweakNumber, TweakColor, TweakButton, + useTweaks, + TweaksPanel, + TweakSection, + TweakRow, + TweakSlider, + TweakToggle, + TweakRadio, + TweakSelect, + TweakText, + TweakNumber, + TweakColor, + TweakButton, }); diff --git a/docs/design-system/ink-carbon/project/explorer/views-1.jsx b/docs/design-system/ink-carbon/project/explorer/views-1.jsx index fb2b195f5..13c1484b4 100644 --- a/docs/design-system/ink-carbon/project/explorer/views-1.jsx +++ b/docs/design-system/ink-carbon/project/explorer/views-1.jsx @@ -2,269 +2,293 @@ // SPDX-License-Identifier: Apache-2.0 /* IIFE-WRAPPED */ -;(() => { -/* PDPP Explorer — views part 1: Table, Timeline, Conversation, Reader */ +(() => { + /* PDPP Explorer — views part 1: Table, Timeline, Conversation, Reader */ -const { fmtRelative, fmtClock, fmtDate, fmtDay, fmtCurrency, fmtDuration, fmtDistance, - Avatar, getTimeField, getRecordTime, getRecordTitle } = window.PDPPPrim; + const { + fmtRelative, + fmtClock, + fmtDate, + fmtDay, + fmtCurrency, + fmtDuration, + fmtDistance, + Avatar, + getTimeField, + getRecordTime, + getRecordTitle, + } = window.PDPPPrim; -/* ─── TABLE VIEW (the universal floor) ───────────────────────────────── - * - * Picks columns generically: prefer timestamp first, then human-readable - * scalars (text, currency, enum, number, person), then everything else - * stays in the peek. Currency right-aligns. Long text gets truncated. - */ -function TableView({ stream, selectedId, onSelect, projection }) { - const fields = stream.schema.fields; - const visibleFields = projection ? fields.filter((f) => f.granted) : fields; + /* ─── TABLE VIEW (the universal floor) ───────────────────────────────── + * + * Picks columns generically: prefer timestamp first, then human-readable + * scalars (text, currency, enum, number, person), then everything else + * stays in the peek. Currency right-aligns. Long text gets truncated. + */ + function TableView({ stream, selectedId, onSelect, projection }) { + const fields = stream.schema.fields; + const visibleFields = projection ? fields.filter((f) => f.granted) : fields; - // Column priority: timestamp first, then text/enum/person/currency/number, - // then booleans, then anything else. - const priority = (f) => { - if (f.type === "timestamp") return 0; - if (f.type === "text" && /title|subject|merchant|name/i.test(f.name)) return 1; - if (f.type === "person") return 2; - if (f.type === "text") return 3; - if (f.type === "enum") return 4; - if (f.type === "currency") return 5; - if (f.type === "number") return 6; - return 9; - }; - const cols = [...visibleFields] - .filter((f) => f.type !== "id" && f.type !== "blob" && f.type !== "geo" && f.type !== "json" && f.type !== "person[]") - .sort((a, b) => priority(a) - priority(b)) - .slice(0, 5); + // Column priority: timestamp first, then text/enum/person/currency/number, + // then booleans, then anything else. + const priority = (f) => { + if (f.type === "timestamp") return 0; + if (f.type === "text" && /title|subject|merchant|name/i.test(f.name)) return 1; + if (f.type === "person") return 2; + if (f.type === "text") return 3; + if (f.type === "enum") return 4; + if (f.type === "currency") return 5; + if (f.type === "number") return 6; + return 9; + }; + const cols = [...visibleFields] + .filter( + (f) => f.type !== "id" && f.type !== "blob" && f.type !== "geo" && f.type !== "json" && f.type !== "person[]" + ) + .sort((a, b) => priority(a) - priority(b)) + .slice(0, 5); - function renderCell(field, record) { - const v = record[field.name]; - if (v == null) return ; - if (field.type === "timestamp") { - return {fmtRelative(v)}; - } - if (field.type === "currency") { - return 0 ? "pos" : ""}`}>{fmtCurrency(v)}; - } - if (field.type === "number") { - let display = v.toLocaleString(); - if (field.unit === "meters") display = fmtDistance(v); - else if (field.unit === "seconds") display = fmtDuration(v); - return {display}; - } - if (field.type === "boolean") return {v ? "yes" : "—"}; - if (field.type === "enum" || field.type === "enum[]") { - const arr = Array.isArray(v) ? v : [v]; - return {arr.slice(0, 2).join(", ")}; + function renderCell(field, record) { + const v = record[field.name]; + if (v == null) return ; + if (field.type === "timestamp") { + return {fmtRelative(v)}; + } + if (field.type === "currency") { + return 0 ? "pos" : ""}`}>{fmtCurrency(v)}; + } + if (field.type === "number") { + let display = v.toLocaleString(); + if (field.unit === "meters") display = fmtDistance(v); + else if (field.unit === "seconds") display = fmtDuration(v); + return {display}; + } + if (field.type === "boolean") return {v ? "yes" : "—"}; + if (field.type === "enum" || field.type === "enum[]") { + const arr = Array.isArray(v) ? v : [v]; + return {arr.slice(0, 2).join(", ")}; + } + if (field.type === "person") + return ( + + {String(v) + .replace(/<[^>]+>/g, "") + .trim()} + + ); + return ( + + {String(v)} + + ); } - if (field.type === "person") return {String(v).replace(/<[^>]+>/g, "").trim()}; - return {String(v)}; - } - return ( -
- - - - {cols.map((c) => ( - - ))} - - - - {stream.records.map((r) => ( - onSelect(r)}> + return ( +
+
{c.name}
+ + {cols.map((c) => ( - + ))} - ))} - -
{renderCell(c, r)}{c.name}
-
- ); -} + + + {stream.records.map((r) => ( + onSelect(r)}> + {cols.map((c) => ( + {renderCell(c, r)} + ))} + + ))} + + +
+ ); + } -/* ─── TIMELINE VIEW ──────────────────────────────────────────────────── - * - * Day-grouped reverse-chrono list. Right-rail scrubber jumps to a month. - * Works for any stream with a timestamp field. - */ -function TimelineView({ stream, selectedId, onSelect }) { - const tf = getTimeField(stream); - if (!tf) return
No timestamp field in schema.
; - const sorted = [...stream.records].sort((a, b) => new Date(b[tf]) - new Date(a[tf])); + /* ─── TIMELINE VIEW ──────────────────────────────────────────────────── + * + * Day-grouped reverse-chrono list. Right-rail scrubber jumps to a month. + * Works for any stream with a timestamp field. + */ + function TimelineView({ stream, selectedId, onSelect }) { + const tf = getTimeField(stream); + if (!tf) return
No timestamp field in schema.
; + const sorted = [...stream.records].sort((a, b) => new Date(b[tf]) - new Date(a[tf])); - // Group by day - const days = []; - let lastDay = ""; - for (const r of sorted) { - const day = r[tf]?.slice(0, 10); - if (day !== lastDay) { - days.push({ day, items: [] }); - lastDay = day; + // Group by day + const days = []; + let lastDay = ""; + for (const r of sorted) { + const day = r[tf]?.slice(0, 10); + if (day !== lastDay) { + days.push({ day, items: [] }); + lastDay = day; + } + days[days.length - 1].items.push(r); } - days[days.length - 1].items.push(r); - } - // Months for the scrubber - const monthsSet = new Map(); - for (const r of sorted) { - const m = r[tf]?.slice(0, 7); - monthsSet.set(m, (monthsSet.get(m) ?? 0) + 1); - } - const months = [...monthsSet.entries()]; + // Months for the scrubber + const monthsSet = new Map(); + for (const r of sorted) { + const m = r[tf]?.slice(0, 7); + monthsSet.set(m, (monthsSet.get(m) ?? 0) + 1); + } + const months = [...monthsSet.entries()]; - return ( -
-
- {days.map(({ day, items }) => ( -
-
{fmtDay(day + "T12:00:00Z")} · {items.length} record{items.length === 1 ? "" : "s"}
- {items.map((r) => ( -
onSelect(r)} - > -
{fmtClock(r[tf])}
-
- {getRecordTitle(stream, r)} - {[r.from, r.author, r.actor, r.merchant, r.title, r.channel].filter(Boolean).slice(0, 1).join(" · ") - || r.id} -
+ return ( +
+
+ {days.map(({ day, items }) => ( +
+
+ {fmtDay(day + "T12:00:00Z")} · {items.length} record{items.length === 1 ? "" : "s"}
- ))} -
- ))} -
-
- {months.map(([m, count]) => ( - - {new Date(m + "-15").toLocaleDateString("en-US", { month: "short" })} {count} - - ))} + {items.map((r) => ( +
onSelect(r)}> +
{fmtClock(r[tf])}
+
+ {getRecordTitle(stream, r)} + + {[r.from, r.author, r.actor, r.merchant, r.title, r.channel] + .filter(Boolean) + .slice(0, 1) + .join(" · ") || r.id} + +
+
+ ))} +
+ ))} +
+
+ {months.map(([m, count]) => ( + + {new Date(m + "-15").toLocaleDateString("en-US", { month: "short" })}{" "} + {count} + + ))} +
-
- ); -} + ); + } -/* ─── CONVERSATION VIEW ───────────────────────────────────────────────── - * - * Two cols: channel rail + thread list. Works for any record with an - * (author|from) + (text|body) + (thread/channel|to) shape. - */ -function ConversationView({ stream, selectedId, onSelect }) { - // Determine field names from schema - const fields = stream.schema.fields; - const authorField = fields.find((f) => /author|from|sender|user/i.test(f.name))?.name; - const bodyField = fields.find((f) => /body|text|message|content|snippet/i.test(f.name))?.name; - const channelField = fields.find((f) => /channel|thread|conversation/i.test(f.name) && f.type !== "id")?.name - ?? fields.find((f) => /channel|thread|conversation/i.test(f.name))?.name; - const subjectField = fields.find((f) => /subject|title/i.test(f.name))?.name; - const timeField = getTimeField(stream); - const recipField = fields.find((f) => f.name === "to")?.name; + /* ─── CONVERSATION VIEW ───────────────────────────────────────────────── + * + * Two cols: channel rail + thread list. Works for any record with an + * (author|from) + (text|body) + (thread/channel|to) shape. + */ + function ConversationView({ stream, selectedId, onSelect }) { + // Determine field names from schema + const fields = stream.schema.fields; + const authorField = fields.find((f) => /author|from|sender|user/i.test(f.name))?.name; + const bodyField = fields.find((f) => /body|text|message|content|snippet/i.test(f.name))?.name; + const channelField = + fields.find((f) => /channel|thread|conversation/i.test(f.name) && f.type !== "id")?.name ?? + fields.find((f) => /channel|thread|conversation/i.test(f.name))?.name; + const subjectField = fields.find((f) => /subject|title/i.test(f.name))?.name; + const timeField = getTimeField(stream); + const recipField = fields.find((f) => f.name === "to")?.name; - // Group by channel/thread - const groups = new Map(); - for (const r of stream.records) { - const key = r[channelField] ?? r[subjectField] ?? (recipField ? `to:${(r[recipField] ?? []).join(",")}` : "—"); - if (!groups.has(key)) groups.set(key, []); - groups.get(key).push(r); - } - const channels = [...groups.entries()].sort((a, b) => { - const ta = Math.max(...a[1].map((r) => new Date(r[timeField] ?? 0))); - const tb = Math.max(...b[1].map((r) => new Date(r[timeField] ?? 0))); - return tb - ta; - }); + // Group by channel/thread + const groups = new Map(); + for (const r of stream.records) { + const key = r[channelField] ?? r[subjectField] ?? (recipField ? `to:${(r[recipField] ?? []).join(",")}` : "—"); + if (!groups.has(key)) groups.set(key, []); + groups.get(key).push(r); + } + const channels = [...groups.entries()].sort((a, b) => { + const ta = Math.max(...a[1].map((r) => new Date(r[timeField] ?? 0))); + const tb = Math.max(...b[1].map((r) => new Date(r[timeField] ?? 0))); + return tb - ta; + }); - const [active, setActive] = useState(channels[0]?.[0]); - const activeRecords = (groups.get(active) ?? []).slice().sort((a, b) => - new Date(a[timeField] ?? 0) - new Date(b[timeField] ?? 0) - ); + const [active, setActive] = useState(channels[0]?.[0]); + const activeRecords = (groups.get(active) ?? []) + .slice() + .sort((a, b) => new Date(a[timeField] ?? 0) - new Date(b[timeField] ?? 0)); - return ( -
-
- {channels.map(([name, items]) => ( - - ))} -
-
- {activeRecords.map((r) => ( -
onSelect(r)} - > - -
-
- {String(r[authorField] ?? "·").replace(/<[^>]+>/g, "").trim()} - {fmtRelative(r[timeField])} - {subjectField && r[subjectField] ? · {r[subjectField]} : null} -
-
{r[bodyField]}
- {Array.isArray(r.reactions) && r.reactions.length > 0 ? ( -
- {r.reactions.map((rx, i) => ( - {rx.emoji} {rx.count} - ))} + return ( +
+
+ {channels.map(([name, items]) => ( + + ))} +
+
+ {activeRecords.map((r) => ( +
onSelect(r)}> + +
+
+ + {String(r[authorField] ?? "·") + .replace(/<[^>]+>/g, "") + .trim()} + + {fmtRelative(r[timeField])} + {subjectField && r[subjectField] ? ( + · {r[subjectField]} + ) : null}
- ) : null} +
{r[bodyField]}
+ {Array.isArray(r.reactions) && r.reactions.length > 0 ? ( +
+ {r.reactions.map((rx, i) => ( + + {rx.emoji} {rx.count} + + ))} +
+ ) : null} +
-
- ))} + ))} +
-
- ); -} + ); + } -/* ─── READER VIEW ────────────────────────────────────────────────────── - * - * Title + long body. Used for GitHub PR/issue bodies, etc. - */ -function ReaderView({ stream, selectedId, onSelect }) { - const fields = stream.schema.fields; - const titleField = fields.find((f) => /title|subject/i.test(f.name))?.name; - const bodyField = fields.find((f) => /body|content/i.test(f.name))?.name; - const timeField = getTimeField(stream); - const actorField = fields.find((f) => /actor|author|from|user/i.test(f.name))?.name; - const sorted = [...stream.records].sort((a, b) => new Date(b[timeField] ?? 0) - new Date(a[timeField] ?? 0)); - return ( -
-
- {sorted.map((r) => ( -
onSelect(r)} - > -
- {r.type ?? r[actorField] ?? ""} - · - {r.repo ?? ""} - · - {fmtRelative(r[timeField])} + /* ─── READER VIEW ────────────────────────────────────────────────────── + * + * Title + long body. Used for GitHub PR/issue bodies, etc. + */ + function ReaderView({ stream, selectedId, onSelect }) { + const fields = stream.schema.fields; + const titleField = fields.find((f) => /title|subject/i.test(f.name))?.name; + const bodyField = fields.find((f) => /body|content/i.test(f.name))?.name; + const timeField = getTimeField(stream); + const actorField = fields.find((f) => /actor|author|from|user/i.test(f.name))?.name; + const sorted = [...stream.records].sort((a, b) => new Date(b[timeField] ?? 0) - new Date(a[timeField] ?? 0)); + return ( +
+
+ {sorted.map((r) => ( +
onSelect(r)}> +
+ {r.type ?? r[actorField] ?? ""} + · + {r.repo ?? ""} + · + {fmtRelative(r[timeField])} +
+

{r[titleField]}

+

{r[bodyField]}

-

{r[titleField]}

-

{r[bodyField]}

-
- ))} + ))} +
-
- ); -} - -Object.assign(window, { TableView, TimelineView, ConversationView, ReaderView }); + ); + } + Object.assign(window, { TableView, TimelineView, ConversationView, ReaderView }); })(); diff --git a/docs/design-system/ink-carbon/project/explorer/views-2.jsx b/docs/design-system/ink-carbon/project/explorer/views-2.jsx index 025cf4247..c809c3a80 100644 --- a/docs/design-system/ink-carbon/project/explorer/views-2.jsx +++ b/docs/design-system/ink-carbon/project/explorer/views-2.jsx @@ -2,338 +2,356 @@ // SPDX-License-Identifier: Apache-2.0 /* IIFE-WRAPPED */ -;(() => { -/* PDPP Explorer — views part 2: Ledger, Gallery, Map, Calendar, Chart */ +(() => { + /* PDPP Explorer — views part 2: Ledger, Gallery, Map, Calendar, Chart */ -const { fmtRelative, fmtClock, fmtDate, fmtDay, fmtCurrency, fmtDuration, fmtDistance, - Heatmap, Sparkline, getTimeField, getRecordTitle, NOW } = window.PDPPPrim; + const { + fmtRelative, + fmtClock, + fmtDate, + fmtDay, + fmtCurrency, + fmtDuration, + fmtDistance, + Heatmap, + Sparkline, + getTimeField, + getRecordTitle, + NOW, + } = window.PDPPPrim; -/* ─── LEDGER VIEW ────────────────────────────────────────────────────── - * - * Month-strip + transactions list + category breakdown. - * Generalized: works on any stream with a currency-typed field. - */ -function LedgerView({ stream, selectedId, onSelect }) { - const fields = stream.schema.fields; - const amountField = (fields.find((f) => f.type === "currency") ?? fields.find((f) => f.type === "number" && /amount/i.test(f.name)))?.name; - const merchantField = fields.find((f) => /merchant|payee|counterparty|seller/i.test(f.name))?.name; - const catField = fields.find((f) => /category|kind|type/i.test(f.name) && f.type === "enum")?.name; - const timeField = getTimeField(stream); - const memoField = fields.find((f) => /memo|note|description/i.test(f.name))?.name; + /* ─── LEDGER VIEW ────────────────────────────────────────────────────── + * + * Month-strip + transactions list + category breakdown. + * Generalized: works on any stream with a currency-typed field. + */ + function LedgerView({ stream, selectedId, onSelect }) { + const fields = stream.schema.fields; + const amountField = ( + fields.find((f) => f.type === "currency") ?? fields.find((f) => f.type === "number" && /amount/i.test(f.name)) + )?.name; + const merchantField = fields.find((f) => /merchant|payee|counterparty|seller/i.test(f.name))?.name; + const catField = fields.find((f) => /category|kind|type/i.test(f.name) && f.type === "enum")?.name; + const timeField = getTimeField(stream); + const memoField = fields.find((f) => /memo|note|description/i.test(f.name))?.name; - // Month strip - const monthBuckets = useMonths(stream.records, timeField); - const [activeMonth, setActiveMonth] = useState(monthBuckets[0]?.key ?? null); - const visible = stream.records.filter((r) => r[timeField]?.slice(0, 7) === activeMonth); - const sorted = [...visible].sort((a, b) => new Date(b[timeField]) - new Date(a[timeField])); + // Month strip + const monthBuckets = useMonths(stream.records, timeField); + const [activeMonth, setActiveMonth] = useState(monthBuckets[0]?.key ?? null); + const visible = stream.records.filter((r) => r[timeField]?.slice(0, 7) === activeMonth); + const sorted = [...visible].sort((a, b) => new Date(b[timeField]) - new Date(a[timeField])); - // Category breakdown - const cats = new Map(); - for (const r of visible) { - const cat = r[catField] ?? "Other"; - cats.set(cat, (cats.get(cat) ?? 0) + Math.abs(r[amountField] ?? 0)); - } - const catEntries = [...cats.entries()].sort((a, b) => b[1] - a[1]); - const catMax = Math.max(1, ...catEntries.map((c) => c[1])); + // Category breakdown + const cats = new Map(); + for (const r of visible) { + const cat = r[catField] ?? "Other"; + cats.set(cat, (cats.get(cat) ?? 0) + Math.abs(r[amountField] ?? 0)); + } + const catEntries = [...cats.entries()].sort((a, b) => b[1] - a[1]); + const catMax = Math.max(1, ...catEntries.map((c) => c[1])); - return ( -
-
-
- {monthBuckets.map((m) => ( - - ))} -
-
- {sorted.map((r) => ( -
onSelect(r)} - > -
{fmtDate(r[timeField])}
-
- {r[merchantField] ?? r.title ?? "—"} - {r[memoField] ? {r[memoField]} : null} -
-
{r[catField] ?? "—"}
-
0 ? "pos" : ""}`}> - {fmtCurrency(r[amountField] ?? 0)} + return ( +
+
+
+ {monthBuckets.map((m) => ( + + ))} +
+
+ {sorted.map((r) => ( +
onSelect(r)} + > +
{fmtDate(r[timeField])}
+
+ {r[merchantField] ?? r.title ?? "—"} + {r[memoField] ? {r[memoField]} : null} +
+
{r[catField] ?? "—"}
+
0 ? "pos" : ""}`}> + {fmtCurrency(r[amountField] ?? 0)} +
+ ))} +
+
+
+
- -
- ); -} - -function useMonths(records, timeField) { - const buckets = new Map(); - for (const r of records) { - const k = r[timeField]?.slice(0, 7); - if (!k) continue; - if (!buckets.has(k)) buckets.set(k, { key: k, net: 0, count: 0 }); - buckets.get(k).net += r.amount ?? 0; - buckets.get(k).count += 1; +
+ ); } - const list = [...buckets.values()].sort((a, b) => b.key.localeCompare(a.key)); - return list.slice(0, 6).map((b) => ({ - ...b, - label: new Date(b.key + "-15").toLocaleDateString("en-US", { month: "short" }), - })); -} -/* ─── GALLERY VIEW ───────────────────────────────────────────────────── - * - * Justified-grid masonry-ish for any stream with a blob image / url image field. - */ -function GalleryView({ stream, selectedId, onSelect }) { - const fields = stream.schema.fields; - const imgField = (fields.find((f) => f.type === "blob" && (f.media_type ?? "").startsWith("image/")) - ?? fields.find((f) => /thumb|image|photo|picture/i.test(f.name)))?.name; - const capField = fields.find((f) => /caption|title|subject/i.test(f.name))?.name; - const timeField = getTimeField(stream); - const sorted = [...stream.records].sort((a, b) => new Date(b[timeField] ?? 0) - new Date(a[timeField] ?? 0)); + /* ─── MAP VIEW ───────────────────────────────────────────────────────── + * + * Stylized rectangular projection — not a real map, just enough geography + * to read multiple pins as a place. For a generalized explorer this is + * meant as a quick locator; a real impl would mount mapbox/maplibre here. + */ + function MapView({ stream, selectedId, onSelect }) { + const fields = stream.schema.fields; + const latField = fields.find((f) => /^lat(itude)?$/i.test(f.name))?.name; + const lngField = fields.find((f) => /^l(ng|on|ongitude)$/i.test(f.name))?.name; + const labelField = fields.find((f) => /title|caption|subject/i.test(f.name))?.name; + const timeField = getTimeField(stream); - return ( -
- {sorted.map((r) => ( -
onSelect(r)} - > - {r[capField] - {capField ?
{r[capField]}
: null} + if (!latField || !lngField) return
Stream carries no usable geo fields.
; + + const pts = stream.records.filter((r) => r[latField] != null && r[lngField] != null); + if (!pts.length) return
No records carry coordinates in this window.
; + + const lats = pts.map((p) => p[latField]); + const lngs = pts.map((p) => p[lngField]); + // Pad bbox slightly so pins don't sit on the edges + const minLat = Math.min(...lats) - 0.005; + const maxLat = Math.max(...lats) + 0.005; + const minLng = Math.min(...lngs) - 0.005; + const maxLng = Math.max(...lngs) + 0.005; + + function projX(lng) { + const range = maxLng - minLng || 1; + return ((lng - minLng) / range) * 100; + } + function projY(lat) { + const range = maxLat - minLat || 1; + return (1 - (lat - minLat) / range) * 100; + } + + return ( +
+
+ {pts.map((r) => ( +
onSelect(r)} + style={{ + left: `${projX(r[lngField])}%`, + top: `${projY(r[latField])}%`, + }} + > + + + {(r[labelField] ?? r.title ?? "·").slice(0, 28)} · {fmtDate(r[timeField])} + +
+ ))} +
+ {pts.length} record{pts.length === 1 ? "" : "s"} · bbox {minLat.toFixed(2)},{minLng.toFixed(2)} →{" "} + {maxLat.toFixed(2)},{maxLng.toFixed(2)}
- ))} -
- ); -} +
+ ); + } -/* ─── MAP VIEW ───────────────────────────────────────────────────────── - * - * Stylized rectangular projection — not a real map, just enough geography - * to read multiple pins as a place. For a generalized explorer this is - * meant as a quick locator; a real impl would mount mapbox/maplibre here. - */ -function MapView({ stream, selectedId, onSelect }) { - const fields = stream.schema.fields; - const latField = fields.find((f) => /^lat(itude)?$/i.test(f.name))?.name; - const lngField = fields.find((f) => /^l(ng|on|ongitude)$/i.test(f.name))?.name; - const labelField = fields.find((f) => /title|caption|subject/i.test(f.name))?.name; - const timeField = getTimeField(stream); + /* ─── CALENDAR VIEW ──────────────────────────────────────────────────── + * + * 6-week month grid anchored on `today`. Records with start/end (or + * any timestamp field) render as inline event chips. + */ + function CalendarView({ stream, selectedId, onSelect }) { + const fields = stream.schema.fields; + const startField = fields.find((f) => /^start/i.test(f.name))?.name ?? getTimeField(stream); + const titleField = fields.find((f) => /title|subject|name/i.test(f.name))?.name; - if (!latField || !lngField) return
Stream carries no usable geo fields.
; + const today = new Date(NOW); + const monthStart = new Date(today.getFullYear(), today.getMonth(), 1); + // Start the grid on the Sunday before (or equal to) the 1st. + const gridStart = new Date(monthStart); + gridStart.setDate(monthStart.getDate() - monthStart.getDay()); - const pts = stream.records.filter((r) => r[latField] != null && r[lngField] != null); - if (!pts.length) return
No records carry coordinates in this window.
; + const cells = []; + for (let i = 0; i < 42; i++) { + const d = new Date(gridStart); + d.setDate(gridStart.getDate() + i); + cells.push(d); + } - const lats = pts.map((p) => p[latField]); - const lngs = pts.map((p) => p[lngField]); - // Pad bbox slightly so pins don't sit on the edges - const minLat = Math.min(...lats) - 0.005; - const maxLat = Math.max(...lats) + 0.005; - const minLng = Math.min(...lngs) - 0.005; - const maxLng = Math.max(...lngs) + 0.005; + const byDate = new Map(); + for (const r of stream.records) { + if (!r[startField]) continue; + const key = r[startField].slice(0, 10); + if (!byDate.has(key)) byDate.set(key, []); + byDate.get(key).push(r); + } - function projX(lng) { - const range = maxLng - minLng || 1; - return ((lng - minLng) / range) * 100; - } - function projY(lat) { - const range = maxLat - minLat || 1; - return (1 - (lat - minLat) / range) * 100; - } + const dayKey = (d) => d.toISOString().slice(0, 10); - return ( -
-
- {pts.map((r) => ( + return ( + <>
onSelect(r)} style={{ - left: `${projX(r[lngField])}%`, - top: `${projY(r[latField])}%`, + marginBottom: "0.75rem", + fontFamily: "var(--font-mono)", + fontSize: "0.78rem", + color: "var(--muted-foreground)", }} > - - - {(r[labelField] ?? r.title ?? "·").slice(0, 28)} · {fmtDate(r[timeField])} - + {today.toLocaleDateString("en-US", { month: "long", year: "numeric" })}
- ))} -
- {pts.length} record{pts.length === 1 ? "" : "s"} · bbox {minLat.toFixed(2)},{minLng.toFixed(2)} → {maxLat.toFixed(2)},{maxLng.toFixed(2)} -
-
- ); -} - -/* ─── CALENDAR VIEW ──────────────────────────────────────────────────── - * - * 6-week month grid anchored on `today`. Records with start/end (or - * any timestamp field) render as inline event chips. - */ -function CalendarView({ stream, selectedId, onSelect }) { - const fields = stream.schema.fields; - const startField = fields.find((f) => /^start/i.test(f.name))?.name ?? getTimeField(stream); - const titleField = fields.find((f) => /title|subject|name/i.test(f.name))?.name; - - const today = new Date(NOW); - const monthStart = new Date(today.getFullYear(), today.getMonth(), 1); - // Start the grid on the Sunday before (or equal to) the 1st. - const gridStart = new Date(monthStart); - gridStart.setDate(monthStart.getDate() - monthStart.getDay()); - - const cells = []; - for (let i = 0; i < 42; i++) { - const d = new Date(gridStart); - d.setDate(gridStart.getDate() + i); - cells.push(d); +
+ {["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"].map((d) => ( +
+ {d} +
+ ))} + {cells.map((d) => { + const isToday = dayKey(d) === dayKey(today); + const isOther = d.getMonth() !== today.getMonth(); + const evts = byDate.get(dayKey(d)) ?? []; + return ( +
+ {d.getDate()} + {evts.map((r) => ( + { + e.stopPropagation(); + onSelect(r); + }} + > + {r[titleField] ?? r.title ?? "·"} + + ))} +
+ ); + })} +
+ + ); } - const byDate = new Map(); - for (const r of stream.records) { - if (!r[startField]) continue; - const key = r[startField].slice(0, 10); - if (!byDate.has(key)) byDate.set(key, []); - byDate.get(key).push(r); - } + /* ─── CHART VIEW ─────────────────────────────────────────────────────── + * + * For each numeric measure, render a heatmap (day density) + sparkline. + * Generic: doesn't care what stream. + */ + function ChartView({ stream }) { + const fields = stream.schema.fields; + const timeField = getTimeField(stream); + const isMeasure = (f) => f.type === "number" && !/^(lat|lng|longitude|latitude|id|.*_id|.*_count)$/i.test(f.name); + const measures = fields.filter(isMeasure); - const dayKey = (d) => d.toISOString().slice(0, 10); + // Aggregate per-day count for the activity heatmap + const dayCounts = new Map(); + for (const r of stream.records) { + const d = r[timeField]?.slice(0, 10); + if (!d) continue; + dayCounts.set(d, (dayCounts.get(d) ?? 0) + 1); + } + const heatValues = [...dayCounts.entries()].map(([date, count]) => ({ date, count })); - return ( - <> -
- {today.toLocaleDateString("en-US", { month: "long", year: "numeric" })} -
-
- {["Sun","Mon","Tue","Wed","Thu","Fri","Sat"].map((d) => ( -
{d}
- ))} - {cells.map((d) => { - const isToday = dayKey(d) === dayKey(today); - const isOther = d.getMonth() !== today.getMonth(); - const evts = byDate.get(dayKey(d)) ?? []; + // Sparklines: for each measure, sort by time and take last 30 values + function valuesForMeasure(f) { + return [...stream.records] + .filter((r) => r[f.name] != null && r[timeField]) + .sort((a, b) => new Date(a[timeField]) - new Date(b[timeField])) + .map((r) => r[f.name]); + } + + return ( +
+
+
+ Activity · last 12 weeks + + {stream.records.length} records · {dayCounts.size} active days + +
+ +
+ {measures.map((f) => { + const values = valuesForMeasure(f); + if (!values.length) return null; + const avg = values.reduce((s, v) => s + v, 0) / values.length; + const latest = values[values.length - 1]; return ( -
- {d.getDate()} - {evts.map((r) => ( - { e.stopPropagation(); onSelect(r); }} - > - {r[titleField] ?? r.title ?? "·"} +
+
+ {f.name} + + latest {formatMeasure(f, latest)} + {" · "}avg {formatMeasure(f, avg)} - ))} +
+
); })}
- - ); -} - -/* ─── CHART VIEW ─────────────────────────────────────────────────────── - * - * For each numeric measure, render a heatmap (day density) + sparkline. - * Generic: doesn't care what stream. - */ -function ChartView({ stream }) { - const fields = stream.schema.fields; - const timeField = getTimeField(stream); - const isMeasure = (f) => - f.type === "number" && - !/^(lat|lng|longitude|latitude|id|.*_id|.*_count)$/i.test(f.name); - const measures = fields.filter(isMeasure); - - // Aggregate per-day count for the activity heatmap - const dayCounts = new Map(); - for (const r of stream.records) { - const d = r[timeField]?.slice(0, 10); - if (!d) continue; - dayCounts.set(d, (dayCounts.get(d) ?? 0) + 1); + ); } - const heatValues = [...dayCounts.entries()].map(([date, count]) => ({ date, count })); - // Sparklines: for each measure, sort by time and take last 30 values - function valuesForMeasure(f) { - return [...stream.records] - .filter((r) => r[f.name] != null && r[timeField]) - .sort((a, b) => new Date(a[timeField]) - new Date(b[timeField])) - .map((r) => r[f.name]); + function formatMeasure(f, v) { + if (f.unit === "meters") return fmtDistance(v); + if (f.unit === "seconds") return fmtDuration(v); + if (typeof v === "number") return v.toFixed(v % 1 === 0 ? 0 : 1); + return String(v); } - return ( -
-
-
- Activity · last 12 weeks - {stream.records.length} records · {dayCounts.size} active days -
- -
- {measures.map((f) => { - const values = valuesForMeasure(f); - if (!values.length) return null; - const avg = values.reduce((s, v) => s + v, 0) / values.length; - const latest = values[values.length - 1]; - return ( -
-
- {f.name} - - latest {formatMeasure(f, latest)} - {" · "}avg {formatMeasure(f, avg)} - -
- -
- ); - })} -
- ); -} - -function formatMeasure(f, v) { - if (f.unit === "meters") return fmtDistance(v); - if (f.unit === "seconds") return fmtDuration(v); - if (typeof v === "number") return v.toFixed(v % 1 === 0 ? 0 : 1); - return String(v); -} - -Object.assign(window, { LedgerView, GalleryView, MapView, CalendarView, ChartView }); - + Object.assign(window, { LedgerView, GalleryView, MapView, CalendarView, ChartView }); })(); diff --git a/docs/design-system/ink-carbon/project/labs/LivingGrant.jsx b/docs/design-system/ink-carbon/project/labs/LivingGrant.jsx index b2293bebd..43fdb7db1 100644 --- a/docs/design-system/ink-carbon/project/labs/LivingGrant.jsx +++ b/docs/design-system/ink-carbon/project/labs/LivingGrant.jsx @@ -8,14 +8,14 @@ const { useState, useEffect, useRef } = React; // A small deterministic fake record stream const RECORD_STREAM = [ - { e: 'Acme Co', p: '2025-09-16→30', g: '$4,812.50', n: '$3,622.18' }, - { e: 'Acme Co', p: '2025-09-01→15', g: '$4,812.50', n: '$3,622.18' }, - { e: 'Acme Co', p: '2025-08-16→31', g: '$4,812.50', n: '$3,624.42' }, - { e: 'Acme Co', p: '2025-08-01→15', g: '$4,812.50', n: '$3,624.42' }, - { e: 'Acme Co', p: '2025-07-16→31', g: '$4,812.50', n: '$3,624.42' }, - { e: 'Acme Co', p: '2025-07-01→15', g: '$4,756.00', n: '$3,580.12' }, - { e: 'Acme Co', p: '2025-06-16→30', g: '$4,756.00', n: '$3,580.12' }, - { e: 'Acme Co', p: '2025-06-01→15', g: '$4,756.00', n: '$3,580.12' }, + { e: "Acme Co", p: "2025-09-16→30", g: "$4,812.50", n: "$3,622.18" }, + { e: "Acme Co", p: "2025-09-01→15", g: "$4,812.50", n: "$3,622.18" }, + { e: "Acme Co", p: "2025-08-16→31", g: "$4,812.50", n: "$3,624.42" }, + { e: "Acme Co", p: "2025-08-01→15", g: "$4,812.50", n: "$3,624.42" }, + { e: "Acme Co", p: "2025-07-16→31", g: "$4,812.50", n: "$3,624.42" }, + { e: "Acme Co", p: "2025-07-01→15", g: "$4,756.00", n: "$3,580.12" }, + { e: "Acme Co", p: "2025-06-16→30", g: "$4,756.00", n: "$3,580.12" }, + { e: "Acme Co", p: "2025-06-01→15", g: "$4,756.00", n: "$3,580.12" }, ]; const LivingGrant = () => { @@ -25,128 +25,267 @@ const LivingGrant = () => { useEffect(() => { if (paused) return; - const t = setInterval(() => setCursor(c => (c + 1) % RECORD_STREAM.length), 1800); + const t = setInterval(() => setCursor((c) => (c + 1) % RECORD_STREAM.length), 1800); return () => clearInterval(t); }, [paused]); - const thermalColor = `color-mix(in oklch, var(--human) ${(1-thermal)*100}%, var(--protocol) ${thermal*100}%)`; + const thermalColor = `color-mix(in oklch, var(--human) ${(1 - thermal) * 100}%, var(--protocol) ${thermal * 100}%)`; return ( -
+
{/* The thermal rule across the top */} -
+
{/* LEFT: Human side — the holder's view */} -
-
- §2 · HOLDER +
+
+ + §2 · HOLDER + you
- Longview is reading your pay statements. + Longview is reading your pay statements.
- Every other Friday since Oct 14. They see the employer, period, and gross and net pay. They cannot see your bank, address, or anything else. + Every other Friday since{" "} + + Oct 14 + + . They see the employer, period, and gross and net pay. They cannot see your bank, address, or anything else.
-
- {['pay_statements.read', 'employment.read'].map(s => ( -
- - {s} - live +
+ {["pay_statements.read", "employment.read"].map((s) => ( +
+ + + {s} + + + live +
))} - {['tax_docs.read', 'identity.read', 'transactions.read'].map(s => ( -
- - {s} - + {["tax_docs.read", "identity.read", "transactions.read"].map((s) => ( +
+ + + {s} + + + — +
))}
-
- - +
+ +
{/* RIGHT: Protocol side — the machine's view */} -
-
+
+
grt_longview01 - ISSUER · §5 + + ISSUER · §5 +
-
- Longview holds a grant, not a key. +
+ Longview holds a grant, not a key.
{/* The live stream pane */} -
-
- {['employer', 'pay_period', 'gross', 'net'].map(h => ( - {h} +
+
+ {["employer", "pay_period", "gross", "net"].map((h) => ( + + {h} + ))}
-
+
{RECORD_STREAM.map((r, i) => { const offset = (i - cursor + RECORD_STREAM.length) % RECORD_STREAM.length; const y = offset * 20 - 10; const opacity = offset === 0 ? 1 : offset < 4 ? 0.8 - offset * 0.15 : 0; return ( -
- {r.e} - {r.p} - {r.g} - {r.n} +
+ + {r.e} + + + {r.p} + + + {r.g} + + + {r.n} +
); })} {/* Bottom fade */} -
+
-
- cursor: {String(cursor).padStart(3,'0')} / ∞ -
- The resource server drops any field not named in the grant. Purpose is declared, not enforced. Revocation is authoritative at the issuer. + The resource server drops any field not named in the grant. Purpose is declared, not enforced. Revocation is + authoritative at the issuer.
{/* The thermal slider across the bottom — the metaphor made interactive */} -
+
thermal → -
{ - const rect = e.currentTarget.getBoundingClientRect(); - setThermal(Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width))); - }}> -
+
{ + const rect = e.currentTarget.getBoundingClientRect(); + setThermal(Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width))); + }} + > +
- - {thermal < 0.35 ? 'HOLDER' : thermal > 0.65 ? 'ISSUER' : 'BOUNDARY'} · {Math.round(thermal * 100)}° + + {thermal < 0.35 ? "HOLDER" : thermal > 0.65 ? "ISSUER" : "BOUNDARY"} · {Math.round(thermal * 100)}°
diff --git a/docs/design-system/ink-carbon/project/labs/SpecElements.jsx b/docs/design-system/ink-carbon/project/labs/SpecElements.jsx index 6d5cb4b7f..db23c4008 100644 --- a/docs/design-system/ink-carbon/project/labs/SpecElements.jsx +++ b/docs/design-system/ink-carbon/project/labs/SpecElements.jsx @@ -4,79 +4,181 @@ // SpecPage — the whole thing as a reimagined document. const NightToggle = () => { - const [night, setNight] = useState(() => localStorage.getItem('pdpp-view') === 'night'); + const [night, setNight] = useState(() => localStorage.getItem("pdpp-view") === "night"); useEffect(() => { - document.documentElement.dataset.view = night ? 'night' : 'day'; - localStorage.setItem('pdpp-view', night ? 'night' : 'day'); + document.documentElement.dataset.view = night ? "night" : "day"; + localStorage.setItem("pdpp-view", night ? "night" : "day"); }, [night]); return ( - ); }; // Typographic mark — a 'P' with a serif terminal, drawn in paper over ink const MarkP = ({ size = 32 }) => ( -
+
P - +
); // The gutter-numbered spec row — like a printed RFC const SpecRow = ({ num, t, children, tone }) => ( -
+
-
§{num}
+
+ §{num} +
-
{t}
- {children &&
{children}
} +
+ {t} +
+ {children && ( +
+ {children} +
+ )}
); // The thermal legend — a compact key that shows what the two colors mean const ThermalLegend = () => ( -
-
-
HOLDER SIDE
-
warm · declarative · consent
+
+
+
+ HOLDER SIDE +
+
+ warm · declarative · consent +
-
-
-
ISSUER SIDE
-
cool · enforcing · precise
+
+
+
+ ISSUER SIDE +
+
+ cool · enforcing · precise +
); // Purpose taxonomy — a visual vocabulary for why data is requested const PURPOSES = [ - { c: 'planning', gloss: 'forecast futures', tone: 0.2 }, - { c: 'verification', gloss: 'prove a fact', tone: 0.4 }, - { c: 'underwriting', gloss: 'assess risk', tone: 0.55 }, - { c: 'research', gloss: 'learn in aggregate', tone: 0.75 }, - { c: 'fulfillment', gloss: 'complete a request', tone: 0.9 }, + { c: "planning", gloss: "forecast futures", tone: 0.2 }, + { c: "verification", gloss: "prove a fact", tone: 0.4 }, + { c: "underwriting", gloss: "assess risk", tone: 0.55 }, + { c: "research", gloss: "learn in aggregate", tone: 0.75 }, + { c: "fulfillment", gloss: "complete a request", tone: 0.9 }, ]; const PurposeTaxonomy = () => ( -
+
{PURPOSES.map((p, i) => { - const color = `color-mix(in oklch, var(--human) ${(1-p.tone)*100}%, var(--protocol) ${p.tone*100}%)`; + const color = `color-mix(in oklch, var(--human) ${(1 - p.tone) * 100}%, var(--protocol) ${p.tone * 100}%)`; return ( -
0 ? '1px solid var(--rule)' : 'none', gap: 16 }}> - {String(i+1).padStart(2,'0')} - {p.c} - "{p.gloss}" -
+
0 ? "1px solid var(--rule)" : "none", + gap: 16, + }} + > + + {String(i + 1).padStart(2, "0")} + + + {p.c} + + + "{p.gloss}" + +
); })} @@ -85,26 +187,53 @@ const PurposeTaxonomy = () => ( // Footer — a colophon in the RFC style const Colophon = () => ( -
+
DOCUMENT
-
PDPP-0.1.0 · draft 3
-
2026-04-19
+
+ PDPP-0.1.0 · draft 3 +
+
+ 2026-04-19 +
SET
-
Fraunces · Geist · JetBrains Mono
-
ligatures on · tabular figures
+
+ Fraunces · Geist · JetBrains Mono +
+
+ ligatures on · tabular figures +
PRINTED
-
paper oklch(0.985 0.005 85)
-
ink oklch(0.16 0.01 60)
+
+ paper oklch(0.985 0.005 85) +
+
+ ink oklch(0.16 0.01 60) +
-
+
COLOPHON
-
vana-com/pdpp
-
"the grant is the artifact"
+
+ vana-com/pdpp +
+
+ "the grant is the artifact" +
); diff --git a/docs/design-system/ink-carbon/project/labs/TheAtlas.jsx b/docs/design-system/ink-carbon/project/labs/TheAtlas.jsx index c2342b581..ae7de4921 100644 --- a/docs/design-system/ink-carbon/project/labs/TheAtlas.jsx +++ b/docs/design-system/ink-carbon/project/labs/TheAtlas.jsx @@ -4,67 +4,147 @@ // TheAtlas — the spec as a visual index. Purposes, scopes, and temperatures laid out as a map. const SCOPES_MAP = [ - { s: 'pay_statements', fields: 6, reads: 48, axis: 0.35 }, - { s: 'employment', fields: 4, reads: 12, axis: 0.45 }, - { s: 'tax_documents', fields: 5, reads: 6, axis: 0.4 }, - { s: 'identity', fields: 3, reads: 23, axis: 0.7 }, - { s: 'transactions', fields: 8, reads: 94, axis: 0.55 }, - { s: 'health_records', fields: 12, reads: 2, axis: 0.25 }, - { s: 'location', fields: 2, reads: 156, axis: 0.85 }, + { s: "pay_statements", fields: 6, reads: 48, axis: 0.35 }, + { s: "employment", fields: 4, reads: 12, axis: 0.45 }, + { s: "tax_documents", fields: 5, reads: 6, axis: 0.4 }, + { s: "identity", fields: 3, reads: 23, axis: 0.7 }, + { s: "transactions", fields: 8, reads: 94, axis: 0.55 }, + { s: "health_records", fields: 12, reads: 2, axis: 0.25 }, + { s: "location", fields: 2, reads: 156, axis: 0.85 }, ]; const TheAtlas = () => { - const maxReads = Math.max(...SCOPES_MAP.map(s => s.reads)); + const maxReads = Math.max(...SCOPES_MAP.map((s) => s.reads)); return ( -
-
-
+
+
+
§4 · THE ATLAS
-

+

Every stream is a temperature.

- Warmer streams are intimate — held close by the person who owns them. Cooler streams are transactional — issued and acknowledged by machines. The thermal axis runs under every design decision. + Warmer streams are intimate — held close by the person who owns them. Cooler streams are transactional — + issued and acknowledged by machines. The thermal axis runs under every design decision.

{/* The atlas itself — a thermal bar chart */} -
-
+
+
stream fields temperature · warmer to cooler - reads/24h + + reads/24h +
{SCOPES_MAP.map((s, i) => { - const color = `color-mix(in oklch, var(--human) ${(1-s.axis)*100}%, var(--protocol) ${s.axis*100}%)`; + const color = `color-mix(in oklch, var(--human) ${(1 - s.axis) * 100}%, var(--protocol) ${s.axis * 100}%)`; return ( -
0 ? '1px solid var(--rule)' : 'none' }}> +
0 ? "1px solid var(--rule)" : "none", + }} + >
- {s.s} + + {s.s} +
- {s.fields} -
-
-
+ + {s.fields} + +
+
+
{/* tick marks */} - {[0.25, 0.5, 0.75].map(t => ( -
+ {[0.25, 0.5, 0.75].map((t) => ( +
))}
-
-
-
+
+
+
- {s.reads} + + {s.reads} +
); @@ -72,17 +152,30 @@ const TheAtlas = () => {
{/* Footnote */} -
-
- "A location stream is colder than a pay stream, because its provenance has already been abstracted by the device." +
+
+ "A location stream is colder than a pay stream, because its provenance has already been abstracted by the + device."
-
+
— from the annotated spec, footnote 4.11
-
+
also see
-
§4.2 field projection
-
§4.3 stream modes
+
+ §4.2 field projection +
+
+ §4.3 stream modes +
diff --git a/docs/design-system/ink-carbon/project/labs/TheContract.jsx b/docs/design-system/ink-carbon/project/labs/TheContract.jsx index e8415138c..1b95bdf60 100644 --- a/docs/design-system/ink-carbon/project/labs/TheContract.jsx +++ b/docs/design-system/ink-carbon/project/labs/TheContract.jsx @@ -4,63 +4,181 @@ // TheContract — a typographic manifesto. Big serif, set like a declaration, with mono annotations in the margin. const TheContract = () => ( -
-
+
+
{/* Left gutter — the annotations */} {/* The contract */}
-
THE FOUR COMMITMENTS
-
+
+ THE FOUR COMMITMENTS +
+
- I. - The holder decides what may be read, for how long, and why. + + I. + + The holder decides what may be read, for how + long, and why.
- II. - The issuer drops every field not named in the grant. + + II. + + The issuer drops every field not named in + the grant.
- III. - The client states a purpose. The purpose becomes part of the record. + + III. + + The client states a purpose. The purpose becomes part of the + record.
- IV. - Revocation is a hard stop. Authoritative at the issuer. No appeals. + + IV. + + Revocation is a hard stop. Authoritative at + the issuer. No appeals.
-
-
+
+
so that
-
+
-
- Consent becomes{' '} - portable, - access becomes{' '} - granular, - and data stops being a key to steal. +
+ Consent becomes{" "} + + portable + + , access becomes{" "} + + granular + + , and data stops being a key to steal.
{/* Right gutter — the counterweight */} -
diff --git a/docs/design-system/ink-carbon/project/labs/ThePurposes.jsx b/docs/design-system/ink-carbon/project/labs/ThePurposes.jsx index 42c506918..22480d2ae 100644 --- a/docs/design-system/ink-carbon/project/labs/ThePurposes.jsx +++ b/docs/design-system/ink-carbon/project/labs/ThePurposes.jsx @@ -5,28 +5,31 @@ // This is the brand's most opinionated page: purpose as a taxonomy of intent. const ThePurposes = () => ( -
-
-
+
+
+
§3 · PURPOSES
-

+

Every grant states why.

- A purpose_code is a machine-readable commitment. The spec ships five canonical purposes; implementations may extend them, but the shape — verb, object, scope — never changes. + A purpose_code is a machine-readable commitment. The spec ships + five canonical purposes; implementations may extend them, but the shape — verb, object, scope — never changes.

-
+
- The protocol does not police downstream use — it records the commitment. Policing is the job of courts, auditors, and reputation markets. The record is what makes those possible. + The protocol does not police downstream use — it records the commitment. Policing is the job of courts, + auditors, and reputation markets. The record is what makes those possible. - Changing a purpose on an existing grant requires re-consent. The old grant revokes, a new grant issues, the new purpose appears in its own signed artifact. Purposes do not drift. + Changing a purpose on an existing grant requires re-consent. The old grant revokes, a new grant issues, the + new purpose appears in its own signed artifact. Purposes do not drift.
diff --git a/docs/design-system/ink-carbon/project/labs/TheSpecimen.jsx b/docs/design-system/ink-carbon/project/labs/TheSpecimen.jsx index f62edfec4..22736e830 100644 --- a/docs/design-system/ink-carbon/project/labs/TheSpecimen.jsx +++ b/docs/design-system/ink-carbon/project/labs/TheSpecimen.jsx @@ -4,78 +4,192 @@ // TheSpecimen — the type system shown as a specimen page, in the way a foundry would. const TheSpecimen = () => ( -
-
-
+
+
+
§6 · SPECIMEN
-

+

Three faces, one voice.

- A serif speaks for the protocol. A sans-serif speaks for the person. A monospace speaks for the machine. All three share a paper. + A serif speaks for the protocol. A sans-serif speaks for the person. A monospace speaks for the machine. All + three share a paper.

-
+
DISPLAY
-
Fraunces
-
opsz 144 · wght 300–500
+
+ Fraunces +
+
+ opsz 144 · wght 300–500 +
TEXT
-
Geist
-
wght 300–600
+
+ Geist +
+
+ wght 300–600 +
MACHINE
-
JetBrains Mono
-
tnum, cv02, ss01
+
+ JetBrains Mono +
+
+ tnum, cv02, ss01 +
{/* Big letter specimen */} -
- Aa - Aa - Aa - Aa +
+ + Aa + + + Aa + + + Aa + + + Aa +
{/* Pangram stack */} -
+
PANGRAM · serif / italic / sans / mono
-
+
The grant is the artifact, not the key.
-
+
The holder decides what may be read, and why.
-
+
Clients request named records and fields. Every response stays inside the grant.
-
+
GET /v1/streams/pay_statements/records  ·  grant_id=grt_longview01  ·  200 OK
{/* Numerals showcase — tabular figures for the ledger */} -
+
LEDGER · tabular figures · mono + serif
-
-
-
2025-10-14 09:22:07Z
-
2025-10-28 09:22:07Z
-
2025-11-11 09:22:07Z
-
2025-11-25 14:08:02Z ✕
+
+
+
2025-10-14 09:22:07Z
+
2025-10-28 09:22:07Z
+
2025-11-11 09:22:07Z
+
2025-11-25 14:08:02Z ✕
-
+
$4,812.50   gross
$3,622.18   net
$1,190.32   withheld
-
— every fortnight
+
— every fortnight
diff --git a/docs/design-system/ink-carbon/project/labs/ThermalField.jsx b/docs/design-system/ink-carbon/project/labs/ThermalField.jsx index 2f24d398f..5b5ce08d3 100644 --- a/docs/design-system/ink-carbon/project/labs/ThermalField.jsx +++ b/docs/design-system/ink-carbon/project/labs/ThermalField.jsx @@ -5,76 +5,129 @@ // ThermalField — an atmospheric opening. The thermal gradient as page-sized presence. const ThermalField = () => ( -
+
{/* The field itself — a painted atmosphere behind the type */} -
+ }} + /> {/* Top bar — the masthead */} -
-
+
+
-
+
Personal Data Portability Protocol
-
v0.1.0 · draft 3 · 2026-04-19
+
+ v0.1.0 · draft 3 · 2026-04-19 +
-
{/* The thesis block — masthead markers flank the display line, no stretched void */} -
-
-
HOLDER
-
you
-
your data, your terms
+
+
+
+ HOLDER +
+
+ you +
+
+ your data, your terms +
-

- The grant is the artifact,
- not the key. +

+ The grant is the artifact, +
+ not the key.

-
-
ISSUER
-
the server
-
boundary, not barrier
+
+
+ ISSUER +
+
+ the server +
+
+ boundary, not barrier +
{/* A thermal rule — the painted horizon */} -
+
{/* Bottom — the lede and affordances */} -
+

- An open specification for how personal user data flows through the digital economy - under authorization-first, purpose-bound access. - Clients request named records and fields. Every response stays inside the grant. + An open specification for how personal user data flows through the digital economy under{" "} + authorization-first, purpose-bound access. Clients request named + records and fields. Every response stays inside the grant.

-
+
diff --git a/docs/design-system/ink-carbon/project/labs/labs.css b/docs/design-system/ink-carbon/project/labs/labs.css index d1ee65e21..5502fcb81 100644 --- a/docs/design-system/ink-carbon/project/labs/labs.css +++ b/docs/design-system/ink-carbon/project/labs/labs.css @@ -8,9 +8,9 @@ "The spec as a living document" ═══════════════════════════════════════════════════════════════════════════ */ -@import url('https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,300;0,9..144,400;0,9..144,500;0,9..144,600;1,9..144,400&display=swap'); -@import url('https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600;700&display=swap'); -@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600&display=swap'); +@import url("https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,300;0,9..144,400;0,9..144,500;0,9..144,600;1,9..144,400&display=swap"); +@import url("https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600;700&display=swap"); +@import url("https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600&display=swap"); :root { /* Typography */ @@ -30,7 +30,12 @@ --protocol-glow: oklch(0.72 0.16 253); /* The thermal gradient — human to protocol across temperature */ - --thermal: linear-gradient(90deg, var(--human) 0%, oklch(0.55 0.1 180) 50%, var(--protocol) 100%); + --thermal: linear-gradient( + 90deg, + var(--human) 0%, + oklch(0.55 0.1 180) 50%, + var(--protocol) 100% + ); /* Paper / ink — the ledger aesthetic */ --paper: oklch(0.985 0.005 85); @@ -73,7 +78,9 @@ --live: oklch(0.72 0.15 145); } -* { box-sizing: border-box; } +* { + box-sizing: border-box; +} html { background: var(--paper); @@ -91,15 +98,26 @@ body { radial-gradient(ellipse at 85% 10%, var(--protocol-wash), transparent 50%), var(--paper); min-height: 100vh; - transition: background 400ms var(--ease-read), color 200ms; + transition: + background 400ms var(--ease-read), + color 200ms; } /* Scrollbar — like a page edge */ -::-webkit-scrollbar { width: 4px; height: 4px; } -::-webkit-scrollbar-thumb { background: var(--rule-deep); border-radius: 999px; } +::-webkit-scrollbar { + width: 4px; + height: 4px; +} +::-webkit-scrollbar-thumb { + background: var(--rule-deep); + border-radius: 999px; +} /* Selection — copper ink */ -::selection { background: var(--human-wash); color: var(--ink); } +::selection { + background: var(--human-wash); + color: var(--ink); +} /* ═══ Type — serif display, sans body, mono machine ═══ */ @@ -179,66 +197,156 @@ body { /* ═══ Structural elements ═══ */ -.hair { height: 1px; background: var(--rule); } -.hair-deep { height: 1px; background: var(--rule-deep); } +.hair { + height: 1px; + background: var(--rule); +} +.hair-deep { + height: 1px; + background: var(--rule-deep); +} /* The thermal rule — copper to blue, used for major section dividers */ -.hair-thermal { height: 1px; background: var(--thermal); opacity: 0.55; } +.hair-thermal { + height: 1px; + background: var(--thermal); + opacity: 0.55; +} /* Buttons — flat, inked, no shadows, press-feedback only */ .btn { - display: inline-flex; align-items: center; gap: 8px; - height: 40px; padding: 0 18px; - font-family: var(--font-sans); font-size: 14px; font-weight: 500; - border-radius: 2px; border: 1px solid transparent; cursor: pointer; + display: inline-flex; + align-items: center; + gap: 8px; + height: 40px; + padding: 0 18px; + font-family: var(--font-sans); + font-size: 14px; + font-weight: 500; + border-radius: 2px; + border: 1px solid transparent; + cursor: pointer; transition: all 150ms var(--ease-read); letter-spacing: 0.01em; } -.btn-ink { background: var(--ink); color: var(--paper); } -.btn-ink:hover { background: var(--ink-soft); } -.btn-ink:active { transform: translateY(1px); } -.btn-paper { background: transparent; color: var(--ink); border-color: var(--rule-deep); } -.btn-paper:hover { background: var(--paper-warm); border-color: var(--ink-faint); } -.btn-ghost { background: transparent; color: var(--ink-soft); } -.btn-ghost:hover { color: var(--ink); } +.btn-ink { + background: var(--ink); + color: var(--paper); +} +.btn-ink:hover { + background: var(--ink-soft); +} +.btn-ink:active { + transform: translateY(1px); +} +.btn-paper { + background: transparent; + color: var(--ink); + border-color: var(--rule-deep); +} +.btn-paper:hover { + background: var(--paper-warm); + border-color: var(--ink-faint); +} +.btn-ghost { + background: transparent; + color: var(--ink-soft); +} +.btn-ghost:hover { + color: var(--ink); +} /* Chips — the pill-in-prose */ .chip { - display: inline-flex; align-items: center; gap: 4px; - padding: 1px 7px; border-radius: 2px; - font-family: var(--font-mono); font-size: 11.5px; - background: var(--paper-warm); color: var(--ink-soft); + display: inline-flex; + align-items: center; + gap: 4px; + padding: 1px 7px; + border-radius: 2px; + font-family: var(--font-mono); + font-size: 11.5px; + background: var(--paper-warm); + color: var(--ink-soft); border: 1px solid var(--rule); } -.chip-human { color: var(--human); border-color: oklch(from var(--human) l c h / 0.25); background: var(--human-wash); } -.chip-protocol { color: var(--protocol); border-color: oklch(from var(--protocol) l c h / 0.25); background: var(--protocol-wash); } -.chip-live { color: var(--live); border-color: oklch(from var(--live) l c h / 0.3); background: oklch(from var(--live) l c h / 0.08); } +.chip-human { + color: var(--human); + border-color: oklch(from var(--human) l c h / 0.25); + background: var(--human-wash); +} +.chip-protocol { + color: var(--protocol); + border-color: oklch(from var(--protocol) l c h / 0.25); + background: var(--protocol-wash); +} +.chip-live { + color: var(--live); + border-color: oklch(from var(--live) l c h / 0.3); + background: oklch(from var(--live) l c h / 0.08); +} /* ═══ Animations ═══ */ @keyframes pulse-dot { - 0%, 100% { opacity: 0.5; transform: scale(0.85); } - 50% { opacity: 1; transform: scale(1); } + 0%, + 100% { + opacity: 0.5; + transform: scale(0.85); + } + 50% { + opacity: 1; + transform: scale(1); + } } @keyframes stream-flow { - 0% { transform: translateX(-100%); } - 100% { transform: translateX(100%); } + 0% { + transform: translateX(-100%); + } + 100% { + transform: translateX(100%); + } } @keyframes reveal-up { - from { opacity: 0; transform: translateY(12px); } - to { opacity: 1; transform: translateY(0); } + from { + opacity: 0; + transform: translateY(12px); + } + to { + opacity: 1; + transform: translateY(0); + } } @keyframes glyph-cycle { - 0%, 20% { opacity: 1; } - 25%, 45% { opacity: 0; } - 50%, 70% { opacity: 1; } - 75%, 95% { opacity: 0; } - 100% { opacity: 1; } + 0%, + 20% { + opacity: 1; + } + 25%, + 45% { + opacity: 0; + } + 50%, + 70% { + opacity: 1; + } + 75%, + 95% { + opacity: 0; + } + 100% { + opacity: 1; + } } @media (prefers-reduced-motion: reduce) { - *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; } + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } } diff --git a/docs/design-system/ink-carbon/project/preview/brand-logo.html b/docs/design-system/ink-carbon/project/preview/brand-logo.html index 0b4580e8a..c7a49aaaa 100644 --- a/docs/design-system/ink-carbon/project/preview/brand-logo.html +++ b/docs/design-system/ink-carbon/project/preview/brand-logo.html @@ -1,40 +1,97 @@ - - -PDPP · Brand - - - - - -
-
- - PDPP -
- The sheet casting its carbon — the mark is the doctrine -
-
-
-
-

The protocol that always leaves you your copy.

+ + + PDPP · Brand + + + + + +
+
+ + PDPP +
+ The sheet casting its carbon — the mark is the doctrine
-
-

The carbon — the brand's one signature move

-
- +
+
+
+

The protocol that always leaves you your copy.

+
+
+

The carbon — the brand's one signature move

+
+ diff --git a/docs/design-system/ink-carbon/project/preview/colors-primary.html b/docs/design-system/ink-carbon/project/preview/colors-primary.html index e261f00de..ba6050a04 100644 --- a/docs/design-system/ink-carbon/project/preview/colors-primary.html +++ b/docs/design-system/ink-carbon/project/preview/colors-primary.html @@ -1,26 +1,67 @@ - - -PDPP · Color foundations - - - - -
-
Paper--backgroundoklch(.985 .004 90)
-
Ink--foregroundoklch(.18 .005 270)
-
Protocol ink--primaryoklch(.46 .11 255)
-
Human--humanoklch(.55 .11 45)
-
Muted--mutedoklch(.955 .004 270)
-
- + + + PDPP · Color foundations + + + + +
+
+
+ Paper--backgroundoklch(.985 .004 90) +
+
+
+ Ink--foregroundoklch(.18 .005 270) +
+
+
+ Protocol ink--primaryoklch(.46 .11 255) +
+
+
+ Human--humanoklch(.55 .11 45) +
+
+
+ Muted--mutedoklch(.955 .004 270) +
+
+ diff --git a/docs/design-system/ink-carbon/project/preview/colors-semantic.html b/docs/design-system/ink-carbon/project/preview/colors-semantic.html index cdb2fc8ff..dc223e315 100644 --- a/docs/design-system/ink-carbon/project/preview/colors-semantic.html +++ b/docs/design-system/ink-carbon/project/preview/colors-semantic.html @@ -1,25 +1,65 @@ - - -PDPP · State colors - - - - -
-
Recorded / active--successoklch(.50 .11 158)
-
Expiring--warningoklch(.58 .13 70)
-
Destructive--destructiveoklch(.52 .16 27)
-
Carbon--carbon-fill / -outlineprimary @ 9% / 40%
-
- + + + PDPP · State colors + + + + +
+
+
+ Recorded / active--successoklch(.50 .11 158) +
+
+
+ Expiring--warningoklch(.58 .13 70) +
+
+
+ Destructive--destructiveoklch(.52 .16 27) +
+
+
+ Carbon--carbon-fill / -outlineprimary @ 9% / 40% +
+
+ diff --git a/docs/design-system/ink-carbon/project/preview/colors-washes.html b/docs/design-system/ink-carbon/project/preview/colors-washes.html index 429795b8b..f8864c8c9 100644 --- a/docs/design-system/ink-carbon/project/preview/colors-washes.html +++ b/docs/design-system/ink-carbon/project/preview/colors-washes.html @@ -1,24 +1,64 @@ - - -PDPP · Spent color - - - - -
Protocol ink on protocol-authored emphasis — --primary on eyebrows, links, the carbon.
-
Human warmth only where the owner acts — the approve button, the owner badge.
-
State colors only inside endorsements — active, expiring, destructive.
-
No gradients, no washes-as-decoration, no colored left borders, no hue on neutral chrome.
- + + + PDPP · Spent color + + + + +
+ Protocol ink + on protocol-authored emphasis — --primary on eyebrows, links, the carbon. +
+
+ Human warmth only where the owner acts — the approve button, the owner badge. +
+
+ State colors only inside endorsements — active, expiring, destructive. +
+
+ No gradients, no washes-as-decoration, no colored left borders, no hue on neutral chrome. +
+ diff --git a/docs/design-system/ink-carbon/project/preview/components-badges.html b/docs/design-system/ink-carbon/project/preview/components-badges.html index 7c5ada077..d58aedb3d 100644 --- a/docs/design-system/ink-carbon/project/preview/components-badges.html +++ b/docs/design-system/ink-carbon/project/preview/components-badges.html @@ -1,32 +1,46 @@ - - -PDPP · Endorsements - - - - - -
endorsements — state color is spent here and only here
-
- active - continuous - expiring 26h - revoked - denied -
-
taxonomy tags — typed, no color spent
-
- append only - owner device flow - v0.1.0 - draft -
- + + + PDPP · Endorsements + + + + + +
endorsements — state color is spent here and only here
+
+ active + continuous + expiring 26h + revoked + denied +
+
taxonomy tags — typed, no color spent
+
+ append only + owner device flow + v0.1.0 + draft +
+ diff --git a/docs/design-system/ink-carbon/project/preview/components-buttons.html b/docs/design-system/ink-carbon/project/preview/components-buttons.html index 0185cc7dc..dffd70219 100644 --- a/docs/design-system/ink-carbon/project/preview/components-buttons.html +++ b/docs/design-system/ink-carbon/project/preview/components-buttons.html @@ -1,25 +1,39 @@ - - -PDPP · Buttons - - - - - -
the human variant is reserved for the owner's act of consent
-
- - - - - -
- + + + PDPP · Buttons + + + + + +
the human variant is reserved for the owner's act of consent
+
+ + + + + +
+ diff --git a/docs/design-system/ink-carbon/project/preview/components-cards.html b/docs/design-system/ink-carbon/project/preview/components-cards.html index f153684b8..b15fb5236 100644 --- a/docs/design-system/ink-carbon/project/preview/components-cards.html +++ b/docs/design-system/ink-carbon/project/preview/components-cards.html @@ -1,43 +1,61 @@ - - -PDPP · Sheets - - - - - -
-
pdpp-sheet — plain
-
-
-

Grant record

- grt_lngvw_01 -
-
-

A sheet is square paper behind a hairline. No shadow, no rounding, no stripe.

-
-
-
-
-
pdpp-carbon — only when the server retains a copy
-
-
-
-

Access request

- staged · PAR + + + PDPP · Sheets + + + + + +
+
pdpp-sheet — plain
+
+
+

Grant record

+ grt_lngvw_01 +
+
+

A sheet is square paper behind a hairline. No shadow, no rounding, no stripe.

+
-
- Carbon — your copy stays here +
+
+
pdpp-carbon — only when the server retains a copy
+
+
+
+

Access request

+ staged · PAR +
+
+ Carbon — your copy stays here +
+
-
-
- + diff --git a/docs/design-system/ink-carbon/project/preview/components-code.html b/docs/design-system/ink-carbon/project/preview/components-code.html index b15c5072e..a2ce41af6 100644 --- a/docs/design-system/ink-carbon/project/preview/components-code.html +++ b/docs/design-system/ink-carbon/project/preview/components-code.html @@ -1,20 +1,38 @@ - - -PDPP · Code - - - - -
// every response carries the grant that shaped it
+  
+    
+    PDPP · Code
+    
+    
+  
+  
+    
// every response carries the grant that shaped it
 {
   "grant_id": "grt_lngvw_01",
   "stream": "pay_statements",
@@ -22,5 +40,5 @@
   "fields_total": 8,
   "trace_id": "trc_9f2k0419"
 }
- + diff --git a/docs/design-system/ink-carbon/project/preview/components-consent-row.html b/docs/design-system/ink-carbon/project/preview/components-consent-row.html index 479ae1882..83aeccad6 100644 --- a/docs/design-system/ink-carbon/project/preview/components-consent-row.html +++ b/docs/design-system/ink-carbon/project/preview/components-consent-row.html @@ -1,32 +1,36 @@ - - -PDPP · Scope rows - - - - - -
-
- pay_statements.read - append only · 2 yrs - Employer, pay period, gross and net pay — 5 of 8 fields -
-
- employment.read - current + 5 yrs - Employers and dates. No salary history. -
-
- tax_docs.read - declined by you - Struck, not hidden — the request is part of the record. -
-
- + + + PDPP · Scope rows + + + + + +
+
+ pay_statements.read + append only · 2 yrs + Employer, pay period, gross and net pay — 5 of 8 fields +
+
+ employment.read + current + 5 yrs + Employers and dates. No salary history. +
+
+ tax_docs.read + declined by you + Struck, not hidden — the request is part of the record. +
+
+ diff --git a/docs/design-system/ink-carbon/project/preview/components-dataflow.html b/docs/design-system/ink-carbon/project/preview/components-dataflow.html index d14421309..eb8c89d1d 100644 --- a/docs/design-system/ink-carbon/project/preview/components-dataflow.html +++ b/docs/design-system/ink-carbon/project/preview/components-dataflow.html @@ -1,50 +1,54 @@ - - -PDPP · Data list - - - - - -
-
- client - scopes - status - expires -
-
- - Longview Planning - grt_lngvw_01 - - pay_statements.read · employment.read - active - exp 12-14 -
-
- - Concert Recommendations - grt_cncrt_02 - - listening_history.read - continuous - renews -
-
- - Crosswise Ads - grt_xwise_09 - - browsing.read - revoked - 05-02 -
-
- + + + PDPP · Data list + + + + + +
+
+ client + scopes + status + expires +
+
+ + Longview Planning + grt_lngvw_01 + + pay_statements.read · employment.read + active + exp 12-14 +
+
+ + Concert Recommendations + grt_cncrt_02 + + listening_history.read + continuous + renews +
+
+ + Crosswise Ads + grt_xwise_09 + + browsing.read + revoked + 05-02 +
+
+ diff --git a/docs/design-system/ink-carbon/project/preview/components-grant-card.html b/docs/design-system/ink-carbon/project/preview/components-grant-card.html index a068ca3c6..446cd6521 100644 --- a/docs/design-system/ink-carbon/project/preview/components-grant-card.html +++ b/docs/design-system/ink-carbon/project/preview/components-grant-card.html @@ -1,39 +1,56 @@ - - -PDPP · Grant record - - - - - -
-
-
-

Longview Planning

- - active - grt_lngvw_01 - -
-
-
-
purposelong_term_financial_planning
-
scopespay_statements.read · employment.read
-
issued2025-10-14 09:22Z
-
expires2026-12-14 09:22Z
+ + + PDPP · Grant record + + + + + +
+
+
+

Longview Planning

+ + active + grt_lngvw_01 + +
+
+
+
+ purposelong_term_financial_planning +
+
+ scopespay_statements.read · employment.read +
+
+ issued2025-10-14 09:22Z +
+
+ expires2026-12-14 09:22Z +
+
+
+
+ Carbon — your copy stays here + +
-
- Carbon — your copy stays here - -
-
-
- + diff --git a/docs/design-system/ink-carbon/project/preview/components-inputs.html b/docs/design-system/ink-carbon/project/preview/components-inputs.html index 139587035..b7c707e99 100644 --- a/docs/design-system/ink-carbon/project/preview/components-inputs.html +++ b/docs/design-system/ink-carbon/project/preview/components-inputs.html @@ -1,25 +1,33 @@ - - -PDPP · Inputs - - - - - -
- - - Discovery runs against /.well-known/oauth-protected-resource -
-
- - - Gates consent, device approval, and the console -
- + + + PDPP · Inputs + + + + + +
+ + + Discovery runs against /.well-known/oauth-protected-resource +
+
+ + + Gates consent, device approval, and the console +
+ diff --git a/docs/design-system/ink-carbon/project/preview/iconography.html b/docs/design-system/ink-carbon/project/preview/iconography.html index 55b8c5bf5..63172261e 100644 --- a/docs/design-system/ink-carbon/project/preview/iconography.html +++ b/docs/design-system/ink-carbon/project/preview/iconography.html @@ -1,29 +1,63 @@ - - -PDPP · Glyphs - - - - -
-
active
-
pending
-
expiring
-
revoked
-
crosses
-
×declined
-
-

The rule: PDPP uses no pictographic icon set. Meaning is carried by words, glyphs from the typeface, and structure. If a label needs a picture to be understood, fix the label.

- + + + PDPP · Glyphs + + + + +
+
active
+
pending
+
expiring
+
revoked
+
crosses
+
×declined
+
+

+ The rule: + PDPP uses no pictographic icon set. Meaning is carried by words, glyphs from the typeface, and structure. If a + label needs a picture to be understood, fix the label. +

+ diff --git a/docs/design-system/ink-carbon/project/preview/layout-grid.html b/docs/design-system/ink-carbon/project/preview/layout-grid.html index e840c23ca..8cbdbaf7c 100644 --- a/docs/design-system/ink-carbon/project/preview/layout-grid.html +++ b/docs/design-system/ink-carbon/project/preview/layout-grid.html @@ -1,32 +1,67 @@ - - -PDPP · Layout - - - - - -console: sidebar 240/280px · main max 1400px — reading column 760px, wide 1200px -
-
-
-
-pdpp-band — the dataset summary -
-
2connectors
-
7streams
-
48,112records
-
1.21 GBretained
-
- + + + PDPP · Layout + + + + + + console: sidebar 240/280px · main max 1400px — reading column 760px, wide 1200px +
+
+
+
+
+
+
+
+ pdpp-band — the dataset summary +
+
2connectors
+
7streams
+
+ 48,112records +
+
+ 1.21 GBretained +
+
+ diff --git a/docs/design-system/ink-carbon/project/preview/radii.html b/docs/design-system/ink-carbon/project/preview/radii.html index a3091b69e..69483f112 100644 --- a/docs/design-system/ink-carbon/project/preview/radii.html +++ b/docs/design-system/ink-carbon/project/preview/radii.html @@ -1,29 +1,63 @@ - - -PDPP · Corners - - - - - -
-
- sheets, rows, bands — 0px -
-
- - buttons, inputs — var(--radius-control) -
-

Why: documents are square. A 2px ease on controls is the only concession — enough to read as pressable, never as a pill.

- + + + PDPP · Corners + + + + + +
+
+ sheets, rows, bands — 0px +
+
+ + buttons, inputs — var(--radius-control) +
+

+ Why: + documents are square. A 2px ease on controls is the only concession — enough to read as pressable, never as a + pill. +

+ diff --git a/docs/design-system/ink-carbon/project/preview/shadows.html b/docs/design-system/ink-carbon/project/preview/shadows.html index 8914b21c9..7b73a065c 100644 --- a/docs/design-system/ink-carbon/project/preview/shadows.html +++ b/docs/design-system/ink-carbon/project/preview/shadows.html @@ -1,28 +1,53 @@ - - -PDPP · Elevation - - - - - -
-
resting state — flat, always
-
No drop shadows. A sheet sits on the page like paper on a desk. Floating layers (menus, dialogs) are the single exception.
-
-
-
retained state — the carbon is the elevation
-
-
Depth means something here: a second copy exists, and it is yours.
-
-
- + + + PDPP · Elevation + + + + + +
+
resting state — flat, always
+
+ No drop shadows. + A sheet sits on the page like paper on a desk. Floating layers (menus, dialogs) are the single exception. +
+
+
+
retained state — the carbon is the elevation
+
+
Depth means something here: a second copy exists, and it is yours.
+
+
+ diff --git a/docs/design-system/ink-carbon/project/preview/spacing-scale.html b/docs/design-system/ink-carbon/project/preview/spacing-scale.html index b7d61325f..5671bca14 100644 --- a/docs/design-system/ink-carbon/project/preview/spacing-scale.html +++ b/docs/design-system/ink-carbon/project/preview/spacing-scale.html @@ -1,26 +1,73 @@ - - -PDPP · Spacing - - - - -
4
-
8
-
12
-
16
-
20
-
24
-
32
-
40
-
48
- + + + PDPP · Spacing + + + + +
+
+ 4 +
+
+
+ 8 +
+
+
+ 12 +
+
+
+ 16 +
+
+
+ 20 +
+
+
+ 24 +
+
+
+ 32 +
+
+
+ 40 +
+
+
+ 48 +
+ diff --git a/docs/design-system/ink-carbon/project/preview/type-body.html b/docs/design-system/ink-carbon/project/preview/type-body.html index 299dd023c..d0e467cec 100644 --- a/docs/design-system/ink-carbon/project/preview/type-body.html +++ b/docs/design-system/ink-carbon/project/preview/type-body.html @@ -1,22 +1,40 @@ - - -PDPP · Body type - - - - -pdpp-body-lg · 18/400 -

Only the fields below cross. Every response is projected to this grant.

-pdpp-body · 14/400 -

Revocation takes effect at the server, not the app. Your copy of the grant survives it.

-pdpp-title · 14/600    pdpp-label · 12/500    pdpp-caption · 12/400 -

Grant record   Expires Dec 14   revocable at any time

- + + + PDPP · Body type + + + + + pdpp-body-lg · 18/400 +

Only the fields below cross. Every response is projected to this grant.

+ pdpp-body · 14/400 +

Revocation takes effect at the server, not the app. Your copy of the grant survives it.

+ pdpp-title · 14/600    pdpp-label · 12/500    pdpp-caption · 12/400 +

+ Grant record +   Expires Dec 14   + revocable at any time +

+ diff --git a/docs/design-system/ink-carbon/project/preview/type-display.html b/docs/design-system/ink-carbon/project/preview/type-display.html index de732c034..0e4b9903c 100644 --- a/docs/design-system/ink-carbon/project/preview/type-display.html +++ b/docs/design-system/ink-carbon/project/preview/type-display.html @@ -1,21 +1,32 @@ - - -PDPP · Display type - - - - -pdpp-display-lg · 60/700 -
Your data. Your copy.
-pdpp-display · 40/700 -
Longview Planning asks to read 2 streams
-pdpp-heading · 20/700 -
Grants on file
- + + + PDPP · Display type + + + + + pdpp-display-lg · 60/700 +
Your data. Your copy.
+ pdpp-display · 40/700 +
Longview Planning asks to read 2 streams
+ pdpp-heading · 20/700 +
Grants on file
+ diff --git a/docs/design-system/ink-carbon/project/preview/type-mono.html b/docs/design-system/ink-carbon/project/preview/type-mono.html index eb4832cd5..48d6c58d4 100644 --- a/docs/design-system/ink-carbon/project/preview/type-mono.html +++ b/docs/design-system/ink-carbon/project/preview/type-mono.html @@ -1,24 +1,47 @@ - - -PDPP · Typed voice - - - - -pdpp-eyebrow -Access request · staged via PAR -pdpp-typed · 13/500 -pay_statements.read · append only · exp 2026-12-14 -pdpp-typed-sm · 11/400 -grant grt_lngvw_01 · client longview_planning_v1 -

The voice rule: mono is reserved for what the protocol itself writes — IDs, scopes, terms, timestamps. If a human wrote it or a human is being addressed, it is grotesk.

- + + + PDPP · Typed voice + + + + + pdpp-eyebrow + Access request · staged via PAR + pdpp-typed · 13/500 + pay_statements.read · append only · exp 2026-12-14 + pdpp-typed-sm · 11/400 + grant grt_lngvw_01 · client longview_planning_v1 +

+ The voice rule: + mono is reserved for what the protocol itself writes — IDs, scopes, terms, timestamps. If a human wrote it or a + human is being addressed, it is grotesk. +

+ diff --git a/docs/design-system/ink-carbon/project/recordroom/console.css b/docs/design-system/ink-carbon/project/recordroom/console.css index d1a2131d0..bcf9392cf 100644 --- a/docs/design-system/ink-carbon/project/recordroom/console.css +++ b/docs/design-system/ink-carbon/project/recordroom/console.css @@ -37,7 +37,8 @@ border-bottom: 1px solid var(--border); } .rr-side__mark { - width: 18px; height: 18px; + width: 18px; + height: 18px; position: relative; flex-shrink: 0; } @@ -56,8 +57,17 @@ inset: 0; background: var(--foreground); } -.rr-side__name { font-size: 15px; font-weight: 800; letter-spacing: -0.01em; } -.rr-side__nav { display: flex; flex-direction: column; padding: 12px 10px; gap: 2px; } +.rr-side__name { + font-size: 15px; + font-weight: 800; + letter-spacing: -0.01em; +} +.rr-side__nav { + display: flex; + flex-direction: column; + padding: 12px 10px; + gap: 2px; +} .rr-nav-item { display: grid; grid-template-columns: 1fr auto; @@ -71,17 +81,28 @@ border: 0; border-radius: var(--radius-control); text-align: left; - transition: color var(--motion-state), background var(--motion-state); + transition: + color var(--motion-state), + background var(--motion-state); +} +.rr-nav-item:hover { + color: var(--foreground); + background: var(--muted); +} +.rr-nav-item.is-active { + color: var(--foreground); + background: var(--muted); + font-weight: 700; } -.rr-nav-item:hover { color: var(--foreground); background: var(--muted); } -.rr-nav-item.is-active { color: var(--foreground); background: var(--muted); font-weight: 700; } .rr-nav-item__count { font-family: var(--font-mono); font-size: 10px; color: var(--muted-foreground); font-variant-numeric: tabular-nums; } -.rr-side__spacer { flex: 1; } +.rr-side__spacer { + flex: 1; +} .rr-side__foot { padding: 12px 18px 0; border-top: 1px solid var(--border); @@ -89,7 +110,10 @@ flex-direction: column; gap: 3px; } -.rr-side__owner { font-size: 12.5px; font-weight: 600; } +.rr-side__owner { + font-size: 12.5px; + font-weight: 600; +} .rr-side__host { font-family: var(--font-mono); font-size: 9.5px; @@ -107,7 +131,12 @@ } /* ─── Main column ─── */ -.rr-main { display: flex; flex-direction: column; min-width: 0; min-height: 0; } +.rr-main { + display: flex; + flex-direction: column; + min-width: 0; + min-height: 0; +} .rr-head { display: flex; justify-content: space-between; @@ -125,7 +154,12 @@ overflow: hidden; text-overflow: ellipsis; } -.rr-head__title { font-size: 17px; font-weight: 700; letter-spacing: -0.012em; margin: 0; } +.rr-head__title { + font-size: 17px; + font-weight: 700; + letter-spacing: -0.012em; + margin: 0; +} .rr-head__sub { font-family: var(--font-mono); font-size: 10px; @@ -138,7 +172,9 @@ padding: 28px 32px 56px; min-height: 0; } -.rr-page { max-width: 1200px; } +.rr-page { + max-width: 1200px; +} .rr-page--split { display: grid; grid-template-columns: minmax(0, 1fr) 360px; @@ -152,8 +188,16 @@ gap: 16px; margin: 0 0 24px; } -.rr-page--split > .rr-page-head { grid-column: 1 / -1; margin-bottom: 4px; } -.rr-page-head__t { font-size: 21px; font-weight: 800; letter-spacing: -0.02em; margin: 0; } +.rr-page--split > .rr-page-head { + grid-column: 1 / -1; + margin-bottom: 4px; +} +.rr-page-head__t { + font-size: 21px; + font-weight: 800; + letter-spacing: -0.02em; + margin: 0; +} .rr-page-head__s { font-family: var(--font-mono); font-size: 10.5px; @@ -168,7 +212,9 @@ } /* ─── Incoming request banner — staged = retained = carbon ─── */ -.rr-incoming { margin-bottom: 22px; } +.rr-incoming { + margin-bottom: 22px; +} .rr-incoming__sheet { display: flex; justify-content: space-between; @@ -178,8 +224,16 @@ background: var(--card); border: 1px solid var(--border-strong); } -.rr-incoming__text { display: flex; flex-direction: column; gap: 2px; min-width: 0; } -.rr-incoming__title { font-size: 14px; font-weight: 700; } +.rr-incoming__text { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} +.rr-incoming__title { + font-size: 14px; + font-weight: 700; +} .rr-incoming__meta { font-family: var(--font-mono); font-size: 10px; @@ -200,16 +254,25 @@ color: inherit; font: inherit; } -.rr-row-btn:hover .pdpp-data-row { background: var(--muted); } +.rr-row-btn:hover .pdpp-data-row { + background: var(--muted); +} .rr-row-btn.is-selected .pdpp-data-row { background: var(--primary-tint); box-shadow: inset 0 0 0 1px var(--carbon-outline); } -.rr-row-btn .pdpp-data-row { transition: background var(--motion-state); } +.rr-row-btn .pdpp-data-row { + transition: background var(--motion-state); +} /* ─── Inspector — YOUR copy of the grant: it carries the carbon ─── */ -.rr-inspector { position: sticky; top: 0; } -.rr-inspector .pdpp-sheet__title { font-size: 15px; } +.rr-inspector { + position: sticky; + top: 0; +} +.rr-inspector .pdpp-sheet__title { + font-size: 15px; +} .rr-inspector__empty { border: 1px dashed var(--border-strong); padding: 28px 22px; @@ -236,9 +299,21 @@ flex-direction: column; gap: 2px; } -.rr-insp-item__t { font-size: 0.875rem; font-weight: 600; } -.rr-insp-item__s { font-family: var(--font-mono); font-size: 0.66rem; color: var(--muted-foreground); } -.rr-insp-item__f { font-family: var(--font-mono); font-size: 0.66rem; color: var(--foreground); overflow-wrap: anywhere; } +.rr-insp-item__t { + font-size: 0.875rem; + font-weight: 600; +} +.rr-insp-item__s { + font-family: var(--font-mono); + font-size: 0.66rem; + color: var(--muted-foreground); +} +.rr-insp-item__f { + font-family: var(--font-mono); + font-size: 0.66rem; + color: var(--foreground); + overflow-wrap: anywhere; +} .rr-insp-keep { padding: 8px 0; border-bottom: 1px solid var(--border); @@ -246,10 +321,26 @@ flex-direction: column; gap: 2px; } -.rr-insp-keep__what { font-family: var(--font-mono); font-size: 0.75rem; color: var(--muted-foreground); overflow-wrap: anywhere; } -.rr-insp-keep__what--declined { text-decoration: line-through; text-decoration-thickness: 1px; } -.rr-insp-keep__why { font-size: 0.69rem; color: var(--muted-foreground); } -.rr-insp-pulse { font-family: var(--font-mono); font-size: 0.69rem; color: var(--muted-foreground); margin-top: 12px; } +.rr-insp-keep__what { + font-family: var(--font-mono); + font-size: 0.75rem; + color: var(--muted-foreground); + overflow-wrap: anywhere; +} +.rr-insp-keep__what--declined { + text-decoration: line-through; + text-decoration-thickness: 1px; +} +.rr-insp-keep__why { + font-size: 0.69rem; + color: var(--muted-foreground); +} +.rr-insp-pulse { + font-family: var(--font-mono); + font-size: 0.69rem; + color: var(--muted-foreground); + margin-top: 12px; +} /* Revocation strike — the line draws, the record stays */ .rr-strikeable { @@ -263,7 +354,8 @@ } .rr-strikeable__line { position: absolute; - left: 0; top: 56%; + left: 0; + top: 56%; height: 1.5px; width: 0; background: currentColor; @@ -275,7 +367,9 @@ } /* ─── Streams view ─── */ -.rr-stream { margin-bottom: 18px; } +.rr-stream { + margin-bottom: 18px; +} .rr-stream__fields { display: flex; flex-wrap: wrap; @@ -287,7 +381,10 @@ font-size: 10.5px; padding: 4px 9px; box-shadow: inset 0 0 0 1px var(--border-strong); - transition: opacity var(--motion-state), color var(--motion-state), box-shadow var(--motion-state); + transition: + opacity var(--motion-state), + color var(--motion-state), + box-shadow var(--motion-state); white-space: nowrap; } .rr-field-chip--dropped { @@ -306,10 +403,16 @@ color: var(--muted-foreground); border: 1px solid var(--input); border-radius: var(--radius-control); - transition: color var(--motion-state), border-color var(--motion-state), background var(--motion-state); + transition: + color var(--motion-state), + border-color var(--motion-state), + background var(--motion-state); white-space: nowrap; } -.rr-lens:hover { color: var(--foreground); border-color: var(--border-strong); } +.rr-lens:hover { + color: var(--foreground); + border-color: var(--border-strong); +} .rr-lens.is-on { color: var(--primary); border-color: var(--primary); @@ -317,7 +420,9 @@ } /* ─── Activity view ─── */ -.rr-log { border-top: 1px solid var(--border-strong); } +.rr-log { + border-top: 1px solid var(--border-strong); +} .rr-log__row { display: grid; grid-template-columns: 150px 70px minmax(0, 1fr) auto; @@ -328,14 +433,35 @@ font-size: 11px; align-items: baseline; } -.rr-log__t { color: var(--muted-foreground); white-space: nowrap; font-variant-numeric: tabular-nums; } -.rr-log__verb { font-weight: 600; } -.rr-log__verb--consent { color: var(--success); } -.rr-log__verb--revoke { color: var(--destructive); } -.rr-log__verb--deny { color: var(--warning); } -.rr-log__what { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.rr-log__ref { color: var(--muted-foreground); white-space: nowrap; } -.rr-log__row--new { animation: slide-up var(--motion-enter) both; } +.rr-log__t { + color: var(--muted-foreground); + white-space: nowrap; + font-variant-numeric: tabular-nums; +} +.rr-log__verb { + font-weight: 600; +} +.rr-log__verb--consent { + color: var(--success); +} +.rr-log__verb--revoke { + color: var(--destructive); +} +.rr-log__verb--deny { + color: var(--warning); +} +.rr-log__what { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.rr-log__ref { + color: var(--muted-foreground); + white-space: nowrap; +} +.rr-log__row--new { + animation: slide-up var(--motion-enter) both; +} /* ─── The consent ceremony — light paper in a dark room ─── */ .rr-overlay { @@ -360,7 +486,7 @@ --primary-tint: oklch(0.46 0.11 255 / 0.08); --human: oklch(0.55 0.11 45); --human-foreground: oklch(0.99 0.005 90); - --carbon-outline: oklch(0.46 0.11 255 / 0.40); + --carbon-outline: oklch(0.46 0.11 255 / 0.4); --carbon-fill: oklch(0.46 0.11 255 / 0.09); --muted: oklch(0.955 0.004 270); --muted-foreground: oklch(0.47 0.008 270); @@ -369,18 +495,28 @@ --input: oklch(0.79 0.006 270); --destructive: oklch(0.52 0.16 27); --destructive-tint: oklch(0.52 0.16 27 / 0.08); - --success: oklch(0.50 0.11 158); + --success: oklch(0.5 0.11 158); --warning: oklch(0.58 0.13 70); color: var(--foreground); } -.rr-ceremony { width: min(600px, 92vw); } +.rr-ceremony { + width: min(600px, 92vw); +} @media (prefers-reduced-motion: no-preference) { - .rr-ceremony { animation: rr-paper-in 380ms var(--ease-enter) both; } + .rr-ceremony { + animation: rr-paper-in 380ms var(--ease-enter) both; + } } @keyframes rr-paper-in { - from { opacity: 0; transform: translateY(16px) scale(0.985); } - to { opacity: 1; transform: translateY(0) scale(1); } + from { + opacity: 0; + transform: translateY(16px) scale(0.985); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } } .rr-ceremony__kicker-row { display: flex; @@ -390,14 +526,33 @@ padding: 12px 22px; border-bottom: 1px solid var(--border); } -.rr-ceremony__kicker-row .pdpp-eyebrow { white-space: nowrap; } +.rr-ceremony__kicker-row .pdpp-eyebrow { + white-space: nowrap; +} /* The duplicate does not exist until consent is given */ -.rr-ceremony:not(.is-pressing) .pdpp-carbon::before { opacity: 0; } -.rr-ceremony__title { font-size: 19px; font-weight: 700; letter-spacing: -0.015em; margin: 0; } -.rr-ceremony__sub { font-size: 13px; color: var(--muted-foreground); margin: 6px 0 0; line-height: 1.55; } -.rr-ceremony__body { padding: 16px 22px 6px; } -.rr-ceremony__scopes { border-top: 1px solid var(--border-strong); margin-top: 14px; } +.rr-ceremony:not(.is-pressing) .pdpp-carbon::before { + opacity: 0; +} +.rr-ceremony__title { + font-size: 19px; + font-weight: 700; + letter-spacing: -0.015em; + margin: 0; +} +.rr-ceremony__sub { + font-size: 13px; + color: var(--muted-foreground); + margin: 6px 0 0; + line-height: 1.55; +} +.rr-ceremony__body { + padding: 16px 22px 6px; +} +.rr-ceremony__scopes { + border-top: 1px solid var(--border-strong); + margin-top: 14px; +} .rr-scope-decide { display: grid; @@ -407,8 +562,16 @@ padding: 11px 0; border-bottom: 1px solid var(--border); } -.rr-scope-decide__name { font-family: var(--font-mono); font-size: 13px; font-weight: 500; } -.rr-scope-decide__desc { font-size: 11.5px; color: var(--muted-foreground); grid-column: 1; } +.rr-scope-decide__name { + font-family: var(--font-mono); + font-size: 13px; + font-weight: 500; +} +.rr-scope-decide__desc { + font-size: 11.5px; + color: var(--muted-foreground); + grid-column: 1; +} .rr-scope-decide__terms { font-family: var(--font-mono); font-size: 10px; @@ -435,10 +598,17 @@ border-radius: var(--radius-control); grid-row: 1 / span 2; grid-column: 3; - transition: color var(--motion-state), border-color var(--motion-state), background var(--motion-state); + transition: + color var(--motion-state), + border-color var(--motion-state), + background var(--motion-state); white-space: nowrap; } -.rr-allow.is-on { color: var(--success); border-color: var(--success); background: var(--success-tint); } +.rr-allow.is-on { + color: var(--success); + border-color: var(--success); + background: var(--success-tint); +} .rr-ceremony__foot { display: flex; justify-content: space-between; @@ -450,9 +620,17 @@ /* The press: the carbon emerges at the moment of approval */ @keyframes rr-press { - 0% { transform: translate(0, 0); opacity: 0; } - 35% { opacity: 1; } - 100% { transform: translate(var(--carbon-offset), var(--carbon-offset)); opacity: 1; } + 0% { + transform: translate(0, 0); + opacity: 0; + } + 35% { + opacity: 1; + } + 100% { + transform: translate(var(--carbon-offset), var(--carbon-offset)); + opacity: 1; + } } .rr-ceremony.is-pressing .pdpp-carbon::before { animation: rr-press 620ms var(--ease-standard) both; @@ -461,17 +639,36 @@ animation: fade-in var(--duration-moderate) var(--ease-enter) 380ms both; } @media (prefers-reduced-motion: reduce) { - .rr-ceremony.is-pressing .pdpp-carbon::before { animation: none; opacity: 1; transform: translate(var(--carbon-offset), var(--carbon-offset)); } - .rr-ceremony.is-pressing .rr-press-reveal { animation: none; } + .rr-ceremony.is-pressing .pdpp-carbon::before { + animation: none; + opacity: 1; + transform: translate(var(--carbon-offset), var(--carbon-offset)); + } + .rr-ceremony.is-pressing .rr-press-reveal { + animation: none; + } } /* ─── Density ─── */ -.rr-app[data-density="compact"] .pdpp-data-row { padding-top: 6px; padding-bottom: 6px; } -.rr-app[data-density="compact"] .rr-log__row { padding-top: 5px; padding-bottom: 5px; } -.rr-app[data-density="compact"] .rr-content { padding-top: 16px; } +.rr-app[data-density="compact"] .pdpp-data-row { + padding-top: 6px; + padding-bottom: 6px; +} +.rr-app[data-density="compact"] .rr-log__row { + padding-top: 5px; + padding-bottom: 5px; +} +.rr-app[data-density="compact"] .rr-content { + padding-top: 16px; +} /* ─── Small screens — split views stack; phone grammar lives in mobile.css ─── */ @media (max-width: 980px) { - .rr-content--split, .rr-page--split { grid-template-columns: minmax(0, 1fr); } - .rr-inspector { position: static; } + .rr-content--split, + .rr-page--split { + grid-template-columns: minmax(0, 1fr); + } + .rr-inspector { + position: static; + } } diff --git a/docs/design-system/ink-carbon/project/recordroom/console2.css b/docs/design-system/ink-carbon/project/recordroom/console2.css index bf39aef95..6a68311e4 100644 --- a/docs/design-system/ink-carbon/project/recordroom/console2.css +++ b/docs/design-system/ink-carbon/project/recordroom/console2.css @@ -7,34 +7,74 @@ /* ─── Entrance choreography — rows cascade, views breathe ─── */ @keyframes rr-row-in { - from { opacity: 0; transform: translateY(5px); } - to { opacity: 1; transform: translateY(0); } + from { + opacity: 0; + transform: translateY(5px); + } + to { + opacity: 1; + transform: translateY(0); + } } @media (prefers-reduced-motion: no-preference) { - .rr-content { animation: fade-in var(--duration-base) var(--ease-enter) both; } - :is(.pdpp-table, .rr-cells, .rr-ov__grid > div) > :is(.pdpp-table__row, .rr-row-btn, .rr-trace-row, .rr-feed-row, .rr-cell, .rr-check) { + .rr-content { + animation: fade-in var(--duration-base) var(--ease-enter) both; + } + :is(.pdpp-table, .rr-cells, .rr-ov__grid > div) + > :is( + .pdpp-table__row, + .rr-row-btn, + .rr-trace-row, + .rr-feed-row, + .rr-cell, + .rr-check + ) { animation: rr-row-in var(--duration-moderate) var(--ease-enter) both; animation-delay: var(--rr-d, 0s); } - :is(.pdpp-table, .rr-cells) > :nth-child(2) { --rr-d: 25ms; } - :is(.pdpp-table, .rr-cells) > :nth-child(3) { --rr-d: 50ms; } - :is(.pdpp-table, .rr-cells) > :nth-child(4) { --rr-d: 75ms; } - :is(.pdpp-table, .rr-cells) > :nth-child(5) { --rr-d: 100ms; } - :is(.pdpp-table, .rr-cells) > :nth-child(6) { --rr-d: 125ms; } - :is(.pdpp-table, .rr-cells) > :nth-child(7) { --rr-d: 150ms; } - :is(.pdpp-table, .rr-cells) > :nth-child(8) { --rr-d: 175ms; } - .rr-anim-swap { animation: rr-row-in var(--duration-base) var(--ease-enter) both; } + :is(.pdpp-table, .rr-cells) > :nth-child(2) { + --rr-d: 25ms; + } + :is(.pdpp-table, .rr-cells) > :nth-child(3) { + --rr-d: 50ms; + } + :is(.pdpp-table, .rr-cells) > :nth-child(4) { + --rr-d: 75ms; + } + :is(.pdpp-table, .rr-cells) > :nth-child(5) { + --rr-d: 100ms; + } + :is(.pdpp-table, .rr-cells) > :nth-child(6) { + --rr-d: 125ms; + } + :is(.pdpp-table, .rr-cells) > :nth-child(7) { + --rr-d: 150ms; + } + :is(.pdpp-table, .rr-cells) > :nth-child(8) { + --rr-d: 175ms; + } + .rr-anim-swap { + animation: rr-row-in var(--duration-base) var(--ease-enter) both; + } } /* The landing — a just-approved grant settles into the ledger */ @keyframes rr-land { - 0% { background: var(--primary-tint); } - 100% { background: transparent; } + 0% { + background: var(--primary-tint); + } + 100% { + background: transparent; + } +} +.pdpp-data-row--landed { + animation: rr-land 1.6s var(--ease-exit) 250ms both; } -.pdpp-data-row--landed { animation: rr-land 1.6s var(--ease-exit) 250ms both; } /* ─── Overview hero — a staged request leads the page ─── */ -.rr-hero { margin-bottom: 4px; } +.rr-hero { + margin-bottom: 4px; +} .rr-hero__sheet { background: var(--card); border: 1px solid var(--border-strong); @@ -44,7 +84,12 @@ align-items: flex-end; gap: 24px; } -.rr-hero__text { display: flex; flex-direction: column; gap: 6px; min-width: 0; } +.rr-hero__text { + display: flex; + flex-direction: column; + gap: 6px; + min-width: 0; +} .rr-hero__title { font-size: 22px; font-weight: 700; @@ -62,14 +107,28 @@ } /* ─── Shared column templates — one axis per view ─── */ -.rr-cols-grants { --cols: 26px minmax(150px, 1.05fr) minmax(0, 1.5fr) 110px 118px; } -.rr-cols-runs { --cols: minmax(150px, 0.8fr) minmax(0, 1.6fr) 64px 52px 104px; } -.rr-cols-schedules { --cols: minmax(170px, 1fr) minmax(0, 1.2fr) 84px 150px; } -.rr-cols-traces { --cols: 82px minmax(120px, 0.9fr) minmax(0, 1.1fr) 44px 44px 50px; } -.rr-cols-feed { --cols: 86px 148px minmax(0, 1fr) 104px; } +.rr-cols-grants { + --cols: 26px minmax(150px, 1.05fr) minmax(0, 1.5fr) 110px 118px; +} +.rr-cols-runs { + --cols: minmax(150px, 0.8fr) minmax(0, 1.6fr) 64px 52px 104px; +} +.rr-cols-schedules { + --cols: minmax(170px, 1fr) minmax(0, 1.2fr) 84px 150px; +} +.rr-cols-traces { + --cols: 82px minmax(120px, 0.9fr) minmax(0, 1.1fr) 44px 44px 50px; +} +.rr-cols-feed { + --cols: 86px 148px minmax(0, 1fr) 104px; +} /* ─── Topbar actions ─── */ -.rr-head__actions { display: flex; align-items: center; gap: 8px; } +.rr-head__actions { + display: flex; + align-items: center; + gap: 8px; +} .rr-kbd { font-family: var(--font-mono); font-size: 9.5px; @@ -89,10 +148,15 @@ border: 1px solid var(--input); border-radius: var(--radius-control); padding: 5px 10px; - transition: color var(--motion-state), border-color var(--motion-state); + transition: + color var(--motion-state), + border-color var(--motion-state); white-space: nowrap; } -.rr-chrome-btn:hover { color: var(--foreground); border-color: var(--border-strong); } +.rr-chrome-btn:hover { + color: var(--foreground); + border-color: var(--border-strong); +} /* ─── Sidebar groups + env footer ─── */ .rr-side__group { @@ -104,7 +168,12 @@ color: var(--muted-foreground); padding: 14px 10px 4px; } -.rr-env { display: flex; flex-direction: column; gap: 5px; padding-top: 10px; } +.rr-env { + display: flex; + flex-direction: column; + gap: 5px; + padding-top: 10px; +} .rr-env__row { display: flex; align-items: center; @@ -115,10 +184,25 @@ white-space: nowrap; overflow: hidden; } -.rr-env__dot { width: 6px; height: 6px; border-radius: 50%; background: var(--success); flex-shrink: 0; } -.rr-env__dot--down { background: var(--destructive); } -.rr-env__label { width: 18px; flex-shrink: 0; color: var(--foreground); } -.rr-env__url { overflow: hidden; text-overflow: ellipsis; } +.rr-env__dot { + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--success); + flex-shrink: 0; +} +.rr-env__dot--down { + background: var(--destructive); +} +.rr-env__label { + width: 18px; + flex-shrink: 0; + color: var(--foreground); +} +.rr-env__url { + overflow: hidden; + text-overflow: ellipsis; +} /* ─── Empty states — the record speaks even when blank ─── */ .rr-empty { @@ -128,8 +212,15 @@ gap: 6px; border-bottom: 1px solid var(--border); } -.rr-empty__line { font-size: 14px; font-weight: 600; } -.rr-empty__sub { font-family: var(--font-mono); font-size: 11px; color: var(--muted-foreground); } +.rr-empty__line { + font-size: 14px; + font-weight: 600; +} +.rr-empty__sub { + font-family: var(--font-mono); + font-size: 11px; + color: var(--muted-foreground); +} /* ─── Terminal affordance — every record ends with a quiet act ─── */ .rr-end { @@ -139,7 +230,11 @@ gap: 16px; padding: 14px 0 0; } -.rr-end__note { font-family: var(--font-mono); font-size: 10.5px; color: var(--muted-foreground); } +.rr-end__note { + font-family: var(--font-mono); + font-size: 10.5px; + color: var(--muted-foreground); +} /* ─── Command palette ─── */ .rr-palette-overlay { @@ -170,8 +265,15 @@ border-bottom: 1px solid var(--border); outline: none; } -.rr-palette__input::placeholder { color: var(--muted-foreground); font-family: var(--font-sans); } -.rr-palette__list { max-height: 320px; overflow-y: auto; padding: 6px 0; } +.rr-palette__input::placeholder { + color: var(--muted-foreground); + font-family: var(--font-sans); +} +.rr-palette__list { + max-height: 320px; + overflow-y: auto; + padding: 6px 0; +} .rr-palette__item { display: grid; grid-template-columns: 1fr auto; @@ -186,7 +288,9 @@ font-size: 13px; padding: 8px 16px; } -.rr-palette__item.is-hl { background: var(--primary-tint); } +.rr-palette__item.is-hl { + background: var(--primary-tint); +} .rr-palette__kind { font-family: var(--font-mono); font-size: 9.5px; @@ -194,7 +298,11 @@ letter-spacing: 0.06em; white-space: nowrap; } -.rr-palette__empty { padding: 18px 16px; font-size: 12.5px; color: var(--muted-foreground); } +.rr-palette__empty { + padding: 18px 16px; + font-size: 12.5px; + color: var(--muted-foreground); +} /* ─── Traces ─── */ .rr-trace-row { @@ -207,25 +315,60 @@ width: 100%; text-align: left; background: transparent; - border-left: 0; border-right: 0; border-top: 0; + border-left: 0; + border-right: 0; + border-top: 0; color: inherit; font-family: var(--font-mono); font-size: 11.5px; transition: background var(--motion-state); } -.rr-trace-row:focus { outline: none; } -.rr-trace-row:hover { background: var(--muted); } -.rr-trace-row.is-selected { background: var(--primary-tint); } -.rr-trace-row__t { color: var(--muted-foreground); white-space: nowrap; } -.rr-trace-row__who { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: var(--font-sans); font-size: 0.875rem; font-weight: 600; } -.rr-trace-row__what { color: var(--muted-foreground); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.rr-trace-row__n { text-align: right; font-variant-numeric: tabular-nums; } -.rr-decide { font-weight: 600; } -.rr-decide--allow { color: var(--success); } -.rr-decide--deny { color: var(--destructive); } +.rr-trace-row:focus { + outline: none; +} +.rr-trace-row:hover { + background: var(--muted); +} +.rr-trace-row.is-selected { + background: var(--primary-tint); +} +.rr-trace-row__t { + color: var(--muted-foreground); + white-space: nowrap; +} +.rr-trace-row__who { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-family: var(--font-sans); + font-size: 0.875rem; + font-weight: 600; +} +.rr-trace-row__what { + color: var(--muted-foreground); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.rr-trace-row__n { + text-align: right; + font-variant-numeric: tabular-nums; +} +.rr-decide { + font-weight: 600; +} +.rr-decide--allow { + color: var(--success); +} +.rr-decide--deny { + color: var(--destructive); +} /* Trace detail: the enforcement path */ -.rr-steps { display: flex; flex-direction: column; } +.rr-steps { + display: flex; + flex-direction: column; +} .rr-step { display: grid; grid-template-columns: 86px 1fr; @@ -236,19 +379,27 @@ .rr-step::before { content: ""; position: absolute; - left: 0; top: 16px; - width: 7px; height: 7px; + left: 0; + top: 16px; + width: 7px; + height: 7px; background: var(--primary); } .rr-step::after { content: ""; position: absolute; - left: 3px; top: 25px; bottom: -7px; + left: 3px; + top: 25px; + bottom: -7px; width: 1px; background: var(--border-strong); } -.rr-step:last-child::after { display: none; } -.rr-step--deny::before { background: var(--destructive); } +.rr-step:last-child::after { + display: none; +} +.rr-step--deny::before { + background: var(--destructive); +} .rr-step__k { font-family: var(--font-mono); font-size: 10px; @@ -257,7 +408,12 @@ color: var(--muted-foreground); padding-top: 1px; } -.rr-step__v { font-family: var(--font-mono); font-size: 11.5px; line-height: 1.5; overflow-wrap: anywhere; } +.rr-step__v { + font-family: var(--font-mono); + font-size: 11.5px; + line-height: 1.5; + overflow-wrap: anywhere; +} .rr-copyid { background: none; @@ -266,7 +422,9 @@ cursor: pointer; transition: color var(--motion-state); } -.rr-copyid:hover { color: var(--primary); } +.rr-copyid:hover { + color: var(--primary); +} /* ─── Deployment checks ─── */ .rr-check { @@ -277,11 +435,29 @@ padding: 10px 2px; border-bottom: 1px solid var(--border); } -.rr-check__glyph { font-family: var(--font-mono); font-size: 9px; font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase; } -.rr-check--ok .rr-check__glyph { color: var(--success); } -.rr-check--warn .rr-check__glyph { color: var(--warning); } -.rr-check__name { font-size: 13px; font-weight: 600; } -.rr-check__detail { font-family: var(--font-mono); font-size: 10.5px; color: var(--muted-foreground); overflow-wrap: anywhere; } +.rr-check__glyph { + font-family: var(--font-mono); + font-size: 9px; + font-weight: 600; + letter-spacing: 0.1em; + text-transform: uppercase; +} +.rr-check--ok .rr-check__glyph { + color: var(--success); +} +.rr-check--warn .rr-check__glyph { + color: var(--warning); +} +.rr-check__name { + font-size: 13px; + font-weight: 600; +} +.rr-check__detail { + font-family: var(--font-mono); + font-size: 10.5px; + color: var(--muted-foreground); + overflow-wrap: anywhere; +} /* ─── Feed ─── */ .rr-feed-row { @@ -292,13 +468,40 @@ padding: 9px 0; border-bottom: 1px solid var(--border); } -.rr-feed-row__t { font-family: var(--font-mono); font-size: 10.5px; color: var(--muted-foreground); white-space: nowrap; } -.rr-feed-row__stream { font-family: var(--font-mono); font-size: 12px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.rr-feed-row__body { font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.rr-feed-row__id { font-family: var(--font-mono); font-size: 10px; color: var(--muted-foreground); white-space: nowrap; text-align: right; } +.rr-feed-row__t { + font-family: var(--font-mono); + font-size: 10.5px; + color: var(--muted-foreground); + white-space: nowrap; +} +.rr-feed-row__stream { + font-family: var(--font-mono); + font-size: 12px; + font-weight: 600; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.rr-feed-row__body { + font-size: 13px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.rr-feed-row__id { + font-family: var(--font-mono); + font-size: 10px; + color: var(--muted-foreground); + white-space: nowrap; + text-align: right; +} /* ─── Overview composition ─── */ -.rr-ov { display: flex; flex-direction: column; gap: 24px; } +.rr-ov { + display: flex; + flex-direction: column; + gap: 24px; +} .rr-ov__grid { display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); @@ -307,7 +510,9 @@ } /* Attention list — a record, not a stack of cards */ -.rr-attn { border-bottom: 0; } +.rr-attn { + border-bottom: 0; +} .rr-attn__row { display: grid; grid-template-columns: minmax(0, 1fr) auto; @@ -315,12 +520,40 @@ padding: 11px 0; border-bottom: 1px solid var(--border); } -.rr-attn__row:first-child { padding-top: 4px; } -.rr-attn__name { font-size: 0.875rem; font-weight: 600; } -.rr-attn__name--mono { font-family: var(--font-mono); font-size: 0.8125rem; font-weight: 500; } -.rr-attn__detail { font-family: var(--font-mono); font-size: 0.72rem; color: var(--muted-foreground); grid-column: 1; overflow-wrap: anywhere; } -.rr-attn__side { grid-row: 1 / span 2; grid-column: 2; display: flex; flex-direction: column; align-items: flex-end; gap: 4px; } -.rr-attn__meta { font-family: var(--font-mono); font-size: 0.66rem; color: var(--muted-foreground); white-space: nowrap; font-variant-numeric: tabular-nums; } +.rr-attn__row:first-child { + padding-top: 4px; +} +.rr-attn__name { + font-size: 0.875rem; + font-weight: 600; +} +.rr-attn__name--mono { + font-family: var(--font-mono); + font-size: 0.8125rem; + font-weight: 500; +} +.rr-attn__detail { + font-family: var(--font-mono); + font-size: 0.72rem; + color: var(--muted-foreground); + grid-column: 1; + overflow-wrap: anywhere; +} +.rr-attn__side { + grid-row: 1 / span 2; + grid-column: 2; + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 4px; +} +.rr-attn__meta { + font-family: var(--font-mono); + font-size: 0.66rem; + color: var(--muted-foreground); + white-space: nowrap; + font-variant-numeric: tabular-nums; +} .rr-mini-head { display: flex; justify-content: space-between; @@ -328,18 +561,34 @@ gap: 12px; margin-bottom: 8px; } -.rr-mini-head__t { font-size: 13px; font-weight: 700; margin: 0; white-space: nowrap; } -.rr-mini-head a, .rr-link { +.rr-mini-head__t { + font-size: 13px; + font-weight: 700; + margin: 0; + white-space: nowrap; +} +.rr-mini-head a, +.rr-link { font-family: var(--font-mono); font-size: 10px; color: var(--primary); - background: none; border: 0; padding: 0; + background: none; + border: 0; + padding: 0; letter-spacing: 0.04em; } -.rr-link:hover { text-decoration: underline; text-underline-offset: 3px; } +.rr-link:hover { + text-decoration: underline; + text-underline-offset: 3px; +} /* ─── Generic two-col cell lists (apps, tokens, exporters, subs) ─── */ -.rr-cells { display: flex; flex-direction: column; gap: 14px; max-width: 880px; } +.rr-cells { + display: flex; + flex-direction: column; + gap: 14px; + max-width: 880px; +} .rr-cell { display: grid; grid-template-columns: minmax(0, 1fr) auto; @@ -348,7 +597,28 @@ background: var(--card); border: 1px solid var(--border-strong); } -.rr-cell__name { font-size: 14px; font-weight: 700; } -.rr-cell__detail { font-family: var(--font-mono); font-size: 10.5px; color: var(--muted-foreground); grid-column: 1; overflow-wrap: anywhere; } -.rr-cell__side { grid-row: 1 / span 2; grid-column: 2; display: flex; flex-direction: column; align-items: flex-end; gap: 5px; } -.rr-cell__meta { font-family: var(--font-mono); font-size: 9.5px; color: var(--muted-foreground); white-space: nowrap; } +.rr-cell__name { + font-size: 14px; + font-weight: 700; +} +.rr-cell__detail { + font-family: var(--font-mono); + font-size: 10.5px; + color: var(--muted-foreground); + grid-column: 1; + overflow-wrap: anywhere; +} +.rr-cell__side { + grid-row: 1 / span 2; + grid-column: 2; + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 5px; +} +.rr-cell__meta { + font-family: var(--font-mono); + font-size: 9.5px; + color: var(--muted-foreground); + white-space: nowrap; +} diff --git a/docs/design-system/ink-carbon/project/recordroom/explore.css b/docs/design-system/ink-carbon/project/recordroom/explore.css index 9f103ca04..35e5185d8 100644 --- a/docs/design-system/ink-carbon/project/recordroom/explore.css +++ b/docs/design-system/ink-carbon/project/recordroom/explore.css @@ -13,8 +13,16 @@ } /* ─── Facet rail ─── */ -.rr-x-rail { display: flex; flex-direction: column; gap: 22px; min-width: 0; } -.rr-x-facets { display: flex; flex-direction: column; } +.rr-x-rail { + display: flex; + flex-direction: column; + gap: 22px; + min-width: 0; +} +.rr-x-facets { + display: flex; + flex-direction: column; +} .rr-x-facets__label { font-family: var(--font-mono); font-size: 10px; @@ -38,18 +46,37 @@ cursor: pointer; transition: color var(--motion-state); } -.rr-x-facet:hover { color: var(--foreground); } -.rr-x-facet.is-on { color: var(--foreground); font-weight: 600; } +.rr-x-facet:hover { + color: var(--foreground); +} +.rr-x-facet.is-on { + color: var(--foreground); + font-weight: 600; +} .rr-x-facet__name { font-size: 12.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.rr-x-facet__name--mono { font-family: var(--font-mono); font-size: 11.5px; } -.rr-x-facet.is-revoked .rr-x-facet__name { text-decoration: line-through; text-decoration-thickness: 1px; } -.rr-x-facet__flag { font-family: var(--font-mono); font-size: 9px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted-foreground); } -.rr-x-facet__flag.is-warn { color: var(--warning); } +.rr-x-facet__name--mono { + font-family: var(--font-mono); + font-size: 11.5px; +} +.rr-x-facet.is-revoked .rr-x-facet__name { + text-decoration: line-through; + text-decoration-thickness: 1px; +} +.rr-x-facet__flag { + font-family: var(--font-mono); + font-size: 9px; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--muted-foreground); +} +.rr-x-facet__flag.is-warn { + color: var(--warning); +} .rr-x-mark { font-family: var(--font-mono); font-size: 0.6rem; @@ -61,37 +88,146 @@ margin-right: 7px; vertical-align: 1px; } -.rr-x-facet__n { font-family: var(--font-mono); font-size: 10px; font-variant-numeric: tabular-nums; } +.rr-x-facet__n { + font-family: var(--font-mono); + font-size: 10px; + font-variant-numeric: tabular-nums; +} /* ─── Controls ─── */ -.rr-x-main { min-width: 0; display: flex; flex-direction: column; gap: 16px; } -.rr-x-controls { display: flex; flex-direction: column; gap: 10px; } -.rr-x-search { width: 100%; } -.rr-x-ranges { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; } -.rr-x-jump { margin-left: auto; } +.rr-x-main { + min-width: 0; + display: flex; + flex-direction: column; + gap: 16px; +} +.rr-x-controls { + display: flex; + flex-direction: column; + gap: 10px; +} +.rr-x-search { + width: 100%; +} +.rr-x-ranges { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} +.rr-x-jump { + margin-left: auto; +} /* ─── Search & filter controls ─── */ -.rr-x-searchrow { display: flex; gap: 12px; align-items: stretch; } -.rr-x-search { flex: 1 1 auto; min-width: 0; width: auto; } -.rr-x-sort { display: flex; align-items: center; gap: 6px; flex-shrink: 0; } -.rr-x-sort__label { font-family: var(--font-mono); font-size: 9px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted-foreground); } -.rr-x-help { position: relative; } -.rr-x-help > summary { font-family: var(--font-mono); font-size: 10px; color: var(--primary); cursor: pointer; list-style: none; } -.rr-x-help > summary::-webkit-details-marker { display: none; } -.rr-x-help__body { position: absolute; z-index: 5; top: 20px; left: 0; width: 330px; padding: 10px 12px; background: var(--card); border: 1px solid var(--border-strong); font-size: 11px; line-height: 1.9; color: var(--muted-foreground); } -.rr-x-help__body code { font-family: var(--font-mono); font-size: 10px; color: var(--foreground); background: var(--muted); padding: 1px 5px; } -.rr-x-active { display: flex; flex-wrap: wrap; gap: 7px; align-items: center; } -.rr-x-chip { display: inline-flex; align-items: center; gap: 7px; font-family: var(--font-mono); font-size: 10.5px; padding: 4px 6px 4px 9px; background: var(--primary-tint); color: var(--primary); border: 1px solid var(--primary); border-radius: var(--radius-control); cursor: pointer; } -.rr-x-chip__x { font-size: 13px; line-height: 1; opacity: 0.7; } -.rr-x-chip:hover .rr-x-chip__x { opacity: 1; } -.rr-x-clearall { font-family: var(--font-mono); font-size: 10px; color: var(--muted-foreground); background: none; border: 0; cursor: pointer; padding: 4px; } -.rr-x-clearall:hover { color: var(--foreground); } +.rr-x-searchrow { + display: flex; + gap: 12px; + align-items: stretch; +} +.rr-x-search { + flex: 1 1 auto; + min-width: 0; + width: auto; +} +.rr-x-sort { + display: flex; + align-items: center; + gap: 6px; + flex-shrink: 0; +} +.rr-x-sort__label { + font-family: var(--font-mono); + font-size: 9px; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--muted-foreground); +} +.rr-x-help { + position: relative; +} +.rr-x-help > summary { + font-family: var(--font-mono); + font-size: 10px; + color: var(--primary); + cursor: pointer; + list-style: none; +} +.rr-x-help > summary::-webkit-details-marker { + display: none; +} +.rr-x-help__body { + position: absolute; + z-index: 5; + top: 20px; + left: 0; + width: 330px; + padding: 10px 12px; + background: var(--card); + border: 1px solid var(--border-strong); + font-size: 11px; + line-height: 1.9; + color: var(--muted-foreground); +} +.rr-x-help__body code { + font-family: var(--font-mono); + font-size: 10px; + color: var(--foreground); + background: var(--muted); + padding: 1px 5px; +} +.rr-x-active { + display: flex; + flex-wrap: wrap; + gap: 7px; + align-items: center; +} +.rr-x-chip { + display: inline-flex; + align-items: center; + gap: 7px; + font-family: var(--font-mono); + font-size: 10.5px; + padding: 4px 6px 4px 9px; + background: var(--primary-tint); + color: var(--primary); + border: 1px solid var(--primary); + border-radius: var(--radius-control); + cursor: pointer; +} +.rr-x-chip__x { + font-size: 13px; + line-height: 1; + opacity: 0.7; +} +.rr-x-chip:hover .rr-x-chip__x { + opacity: 1; +} +.rr-x-clearall { + font-family: var(--font-mono); + font-size: 10px; + color: var(--muted-foreground); + background: none; + border: 0; + cursor: pointer; + padding: 4px; +} +.rr-x-clearall:hover { + color: var(--foreground); +} /* ─── Count line ─── */ -.rr-x-pulse__note { font-family: var(--font-mono); font-size: 10.5px; color: var(--muted-foreground); } +.rr-x-pulse__note { + font-family: var(--font-mono); + font-size: 10.5px; + color: var(--muted-foreground); +} /* ─── Partial fan-in, in the system's voice ─── */ -.rr-x-partial { border: 1px solid var(--border-strong); background: var(--card); } +.rr-x-partial { + border: 1px solid var(--border-strong); + background: var(--card); +} .rr-x-partial__head { display: flex; align-items: baseline; @@ -104,10 +240,32 @@ padding: 10px 14px; cursor: pointer; } -.rr-x-partial__line { font-size: 12.5px; font-weight: 600; flex: 1; min-width: 0; } -.rr-x-partial__toggle { font-family: var(--font-mono); font-size: 10px; color: var(--primary); white-space: nowrap; } -.rr-x-partial__body { padding: 0 14px 12px 36px; display: flex; flex-direction: column; gap: 8px; align-items: flex-start; } -.rr-x-partial__expl { margin: 0; font-size: 12.5px; line-height: 1.55; color: var(--muted-foreground); max-width: 60ch; } +.rr-x-partial__line { + font-size: 12.5px; + font-weight: 600; + flex: 1; + min-width: 0; +} +.rr-x-partial__toggle { + font-family: var(--font-mono); + font-size: 10px; + color: var(--primary); + white-space: nowrap; +} +.rr-x-partial__body { + padding: 0 14px 12px 36px; + display: flex; + flex-direction: column; + gap: 8px; + align-items: flex-start; +} +.rr-x-partial__expl { + margin: 0; + font-size: 12.5px; + line-height: 1.55; + color: var(--muted-foreground); + max-width: 60ch; +} .rr-x-partial__raw { font-family: var(--font-mono); font-size: 10px; @@ -118,7 +276,10 @@ } /* ─── Day groups ─── */ -.rr-x-day { display: flex; flex-direction: column; } +.rr-x-day { + display: flex; + flex-direction: column; +} .rr-x-day__head { display: flex; justify-content: space-between; @@ -127,8 +288,17 @@ padding-bottom: 6px; border-bottom: 1px solid var(--border-strong); } -.rr-x-day__label { font-size: 12.5px; font-weight: 700; white-space: nowrap; } -.rr-x-day__n { font-family: var(--font-mono); font-size: 10.5px; color: var(--muted-foreground); font-variant-numeric: tabular-nums; } +.rr-x-day__label { + font-size: 12.5px; + font-weight: 700; + white-space: nowrap; +} +.rr-x-day__n { + font-family: var(--font-mono); + font-size: 10.5px; + color: var(--muted-foreground); + font-variant-numeric: tabular-nums; +} /* ─── Record rows ─── */ .rr-x-row { @@ -146,15 +316,25 @@ transition: background var(--motion-state); min-width: 0; } -.rr-x-row:hover { background: var(--muted); } -.rr-x-row.is-selected { background: var(--primary-tint); box-shadow: inset 0 0 0 1px var(--carbon-outline); } +.rr-x-row:hover { + background: var(--muted); +} +.rr-x-row.is-selected { + background: var(--primary-tint); + box-shadow: inset 0 0 0 1px var(--carbon-outline); +} .rr-x-row__attr { display: flex; align-items: baseline; gap: 10px; min-width: 0; } -.rr-x-row__stream { font-family: var(--font-mono); font-size: 10.5px; color: var(--primary); white-space: nowrap; } +.rr-x-row__stream { + font-family: var(--font-mono); + font-size: 10.5px; + color: var(--primary); + white-space: nowrap; +} .rr-x-row__con { font-family: var(--font-mono); font-size: 10px; @@ -165,7 +345,12 @@ flex: 1; min-width: 0; } -.rr-x-row__rel { font-family: var(--font-mono); font-size: 10px; color: var(--muted-foreground); white-space: nowrap; } +.rr-x-row__rel { + font-family: var(--font-mono); + font-size: 10px; + color: var(--muted-foreground); + white-space: nowrap; +} .rr-x-row__title { font-size: 0.875rem; font-weight: 600; @@ -179,7 +364,10 @@ font-weight: 400; color: var(--muted-foreground); } -.rr-x-row.is-fold .rr-x-row__title { color: var(--muted-foreground); font-weight: 500; } +.rr-x-row.is-fold .rr-x-row__title { + color: var(--muted-foreground); + font-weight: 500; +} .rr-x-row__snippet { font-size: 12px; color: var(--muted-foreground); @@ -211,9 +399,22 @@ align-items: flex-start; border-bottom: 1px solid var(--border); } -.rr-x-empty__line { font-size: 14px; font-weight: 600; } -.rr-x-foldnote { margin: 10px 0 0; font-size: 11.5px; line-height: 1.5; color: var(--muted-foreground); } -.rr-x-sheet-title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; } +.rr-x-empty__line { + font-size: 14px; + font-weight: 600; +} +.rr-x-foldnote { + margin: 10px 0 0; + font-size: 11.5px; + line-height: 1.5; + color: var(--muted-foreground); +} +.rr-x-sheet-title { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + min-width: 0; +} /* ─── The record type system ─── */ @@ -227,10 +428,17 @@ margin-right: 8px; font-weight: 400; } -.rr-x-row__title.is-derived .rr-x-kicker { margin-right: 6px; } +.rr-x-row__title.is-derived .rr-x-kicker { + margin-right: 6px; +} /* Money hero — the amount is the record */ -.rr-hero { display: flex; flex-direction: column; gap: 3px; padding: 4px 0 14px; } +.rr-hero { + display: flex; + flex-direction: column; + gap: 3px; + padding: 4px 0 14px; +} .rr-hero--money .rr-hero__amount { font-family: var(--font-mono); font-size: 2rem; @@ -239,12 +447,26 @@ letter-spacing: -0.02em; line-height: 1; } -.rr-hero__amount.is-neg { color: var(--foreground); } -.rr-hero__cap { font-size: 0.8rem; color: var(--foreground); font-weight: 500; } -.rr-hero__wire { font-size: 0.66rem; color: var(--muted-foreground); } +.rr-hero__amount.is-neg { + color: var(--foreground); +} +.rr-hero__cap { + font-size: 0.8rem; + color: var(--foreground); + font-weight: 500; +} +.rr-hero__wire { + font-size: 0.66rem; + color: var(--muted-foreground); +} /* Inline image field */ -.rr-rec-image { width: 100%; height: 150px; display: block; margin-bottom: 14px; } +.rr-rec-image { + width: 100%; + height: 150px; + display: block; + margin-bottom: 14px; +} /* Body reading region — long text gets room, not a kv cell */ .rr-bodytext { @@ -271,7 +493,10 @@ } /* Dual-key fields — human label + the wire key a client receives */ -.rr-flds { display: flex; flex-direction: column; } +.rr-flds { + display: flex; + flex-direction: column; +} .rr-fld { display: grid; grid-template-columns: minmax(0, 1fr) auto; @@ -280,10 +505,24 @@ padding: 8px 0; border-bottom: 1px solid var(--border); } -.rr-fld:last-child { border-bottom: 0; } -.rr-fld__id { display: flex; flex-direction: column; gap: 1px; min-width: 0; } -.rr-fld__label { font-size: 0.8rem; font-weight: 500; } -.rr-fld__wire { font-family: var(--font-mono); font-size: 0.62rem; color: var(--muted-foreground); } +.rr-fld:last-child { + border-bottom: 0; +} +.rr-fld__id { + display: flex; + flex-direction: column; + gap: 1px; + min-width: 0; +} +.rr-fld__label { + font-size: 0.8rem; + font-weight: 500; +} +.rr-fld__wire { + font-family: var(--font-mono); + font-size: 0.62rem; + color: var(--muted-foreground); +} .rr-fld__val { font-family: var(--font-mono); font-size: 0.8rem; @@ -291,30 +530,92 @@ overflow-wrap: anywhere; min-width: 0; } -.rr-fld__val.is-num { font-variant-numeric: tabular-nums; font-weight: 600; white-space: nowrap; } +.rr-fld__val.is-num { + font-variant-numeric: tabular-nums; + font-weight: 600; + white-space: nowrap; +} /* ─── Lens & keep (carried from vault view) ─── */ -.rr-ex-lens { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; padding: 12px 18px 0; } -.rr-ex-lens__label { font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted-foreground); } -.rr-ex-keep { margin-top: 10px; border-top: 1px solid var(--border); padding-top: 10px; display: flex; flex-direction: column; gap: 3px; } -.rr-ex-keep__label { font-size: 11.5px; font-weight: 600; } -.rr-ex-keep__fields { font-family: var(--font-mono); font-size: 11px; color: var(--muted-foreground); overflow-wrap: anywhere; } -.rr-ex-keep__note { font-size: 11.5px; color: var(--muted-foreground); } -.rr-ex-alone { margin: 0; padding: 12px 18px 0; font-size: 12.5px; line-height: 1.55; color: var(--muted-foreground); } -.rr-ex-alone b { color: var(--foreground); font-weight: 600; } +.rr-ex-lens { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 8px; + padding: 12px 18px 0; +} +.rr-ex-lens__label { + font-family: var(--font-mono); + font-size: 10px; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--muted-foreground); +} +.rr-ex-keep { + margin-top: 10px; + border-top: 1px solid var(--border); + padding-top: 10px; + display: flex; + flex-direction: column; + gap: 3px; +} +.rr-ex-keep__label { + font-size: 11.5px; + font-weight: 600; +} +.rr-ex-keep__fields { + font-family: var(--font-mono); + font-size: 11px; + color: var(--muted-foreground); + overflow-wrap: anywhere; +} +.rr-ex-keep__note { + font-size: 11.5px; + color: var(--muted-foreground); +} +.rr-ex-alone { + margin: 0; + padding: 12px 18px 0; + font-size: 12.5px; + line-height: 1.55; + color: var(--muted-foreground); +} +.rr-ex-alone b { + color: var(--foreground); + font-weight: 600; +} /* ─── Responsive ─── */ @media (max-width: 1160px) { - .rr-x { grid-template-columns: 200px minmax(0, 1fr); } - .rr-x .rr-inspector { grid-column: 2; } + .rr-x { + grid-template-columns: 200px minmax(0, 1fr); + } + .rr-x .rr-inspector { + grid-column: 2; + } } @media (max-width: 980px) { - .rr-x { grid-template-columns: minmax(0, 1fr); } - .rr-x .rr-inspector { grid-column: 1; } - .rr-x-rail { flex-direction: row; gap: 28px; } - .rr-x-facets { flex: 1; min-width: 0; } + .rr-x { + grid-template-columns: minmax(0, 1fr); + } + .rr-x .rr-inspector { + grid-column: 1; + } + .rr-x-rail { + flex-direction: row; + gap: 28px; + } + .rr-x-facets { + flex: 1; + min-width: 0; + } } @media (max-width: 860px) { - .rr-x-rail { flex-direction: column; gap: 18px; } - .rr-x-jump { margin-left: 0; } + .rr-x-rail { + flex-direction: column; + gap: 18px; + } + .rr-x-jump { + margin-left: 0; + } } diff --git a/docs/design-system/ink-carbon/project/recordroom/image-slot.js b/docs/design-system/ink-carbon/project/recordroom/image-slot.js index c08100946..e87f23e35 100644 --- a/docs/design-system/ink-carbon/project/recordroom/image-slot.js +++ b/docs/design-system/ink-carbon/project/recordroom/image-slot.js @@ -52,7 +52,7 @@ /* END USAGE */ (() => { - const STATE_FILE = '.image-slots.state.json'; + const STATE_FILE = ".image-slots.state.json"; // 2× a ~600px slot in a 1920-wide deck — retina-sharp without making the // sidecar enormous. A 1200px WebP at q=0.85 is ~150-300KB. const MAX_DIM = 1200; @@ -60,7 +60,7 @@ // on SVG blobs is inconsistent). GIF is excluded because the canvas // re-encode keeps only the first frame, so an animated GIF would silently // go still — better to reject than surprise. - const ACCEPT = ['image/png', 'image/jpeg', 'image/webp', 'image/avif']; + const ACCEPT = ["image/png", "image/jpeg", "image/webp", "image/avif"]; // ── Shared sidecar store ──────────────────────────────────────────────── // One fetch + immediate write-on-change for every on the @@ -83,14 +83,14 @@ .then((j) => { // Merge: sidecar loses to any in-memory change that raced ahead of // the fetch (drop or clear) so neither is clobbered by hydration. - if (j && typeof j === 'object') { + if (j && typeof j === "object") { const merged = Object.assign({}, j, slots); // A framing-only write that raced ahead of hydration must not // drop a user image that's only on disk — inherit u from the // sidecar for any in-memory entry that lacks one. for (const k in slots) { if (merged[k] && !merged[k].u && j[k]) { - merged[k].u = typeof j[k] === 'string' ? j[k] : j[k].u; + merged[k].u = typeof j[k] === "string" ? j[k] : j[k].u; } } for (const id of tombstones) delete merged[id]; @@ -99,7 +99,10 @@ tombstones.clear(); }) .catch(() => {}) - .then(() => { loaded = true; subs.forEach((fn) => fn()); }); + .then(() => { + loaded = true; + subs.forEach((fn) => fn()); + }); return loadP; } @@ -110,13 +113,22 @@ let saving = false; let saveDirty = false; function save() { - if (saving) { saveDirty = true; return; } + if (saving) { + saveDirty = true; + return; + } const w = window.omelette && window.omelette.writeFile; if (!w) return; saving = true; Promise.resolve(w(STATE_FILE, JSON.stringify(slots))) .catch(() => {}) - .then(() => { saving = false; if (saveDirty) { saveDirty = false; save(); } }); + .then(() => { + saving = false; + if (saveDirty) { + saveDirty = false; + save(); + } + }); } const S_MAX = 5; @@ -127,18 +139,24 @@ function getSlot(id) { const v = slots[id]; if (!v) return null; - return typeof v === 'string' ? { u: v, s: 1, x: 0, y: 0 } : v; + return typeof v === "string" ? { u: v, s: 1, x: 0, y: 0 } : v; } function setSlot(id, val) { if (!id) return; - if (val) { slots[id] = val; tombstones.delete(id); } - else { delete slots[id]; if (!loaded) tombstones.add(id); } + if (val) { + slots[id] = val; + tombstones.delete(id); + } else { + delete slots[id]; + if (!loaded) tombstones.add(id); + } subs.forEach((fn) => fn()); // A drop is rare + high-value — write immediately so nav-away can't lose // it. Gate on the initial read so we don't overwrite a sidecar we haven't // merged yet; the merge in load() keeps this change once the read lands. - if (loaded) save(); else load().then(save); + if (loaded) save(); + else load().then(save); } // ── Image downscale ───────────────────────────────────────────────────── @@ -153,10 +171,11 @@ const scale = Math.min(1, cap / Math.max(bitmap.width, bitmap.height)); const w = Math.max(1, Math.round(bitmap.width * scale)); const h = Math.max(1, Math.round(bitmap.height * scale)); - const canvas = document.createElement('canvas'); - canvas.width = w; canvas.height = h; - canvas.getContext('2d').drawImage(bitmap, 0, 0, w, h); - return canvas.toDataURL('image/webp', 0.85); + const canvas = document.createElement("canvas"); + canvas.width = w; + canvas.height = h; + canvas.getContext("2d").drawImage(bitmap, 0, 0, w, h); + return canvas.toDataURL("image/webp", 0.85); } finally { bitmap.close && bitmap.close(); } @@ -164,62 +183,62 @@ // ── Custom element ────────────────────────────────────────────────────── const stylesheet = - ':host{display:inline-block;position:relative;vertical-align:top;' + - ' font:13px/1.3 system-ui,-apple-system,sans-serif;color:rgba(0,0,0,.55);width:240px;height:160px}' + - '.frame{position:absolute;inset:0;overflow:hidden;background:rgba(0,0,0,.04)}' + + ":host{display:inline-block;position:relative;vertical-align:top;" + + " font:13px/1.3 system-ui,-apple-system,sans-serif;color:rgba(0,0,0,.55);width:240px;height:160px}" + + ".frame{position:absolute;inset:0;overflow:hidden;background:rgba(0,0,0,.04)}" + // .frame img (clipped) and .spill (unclipped ghost + handles) share the // same left/top/width/height in frame-%, computed by _applyView(), so the // inside-mask crop and the outside-mask spill stay pixel-aligned. - '.frame img{position:absolute;max-width:none;transform:translate(-50%,-50%);' + - ' -webkit-user-drag:none;user-select:none;touch-action:none}' + + ".frame img{position:absolute;max-width:none;transform:translate(-50%,-50%);" + + " -webkit-user-drag:none;user-select:none;touch-action:none}" + // Reframe mode (double-click): the full image spills past the mask. The // spill layer is sized to the IMAGE bounds so its corners are where the // resize handles belong. The ghost inside is translucent; the real // clipped underneath shows the opaque in-mask crop. - '.spill{position:absolute;transform:translate(-50%,-50%);display:none;z-index:1;' + - ' cursor:grab;touch-action:none}' + - ':host([data-panning]) .spill{cursor:grabbing}' + - '.spill .ghost{position:absolute;inset:0;width:100%;height:100%;opacity:.35;' + - ' pointer-events:none;-webkit-user-drag:none;user-select:none;' + - ' box-shadow:0 0 0 1px rgba(0,0,0,.2),0 12px 32px rgba(0,0,0,.2)}' + - '.spill .handle{position:absolute;width:12px;height:12px;border-radius:50%;' + - ' background:#fff;box-shadow:0 0 0 1.5px #c96442,0 1px 3px rgba(0,0,0,.3);' + - ' transform:translate(-50%,-50%)}' + - '.spill .handle[data-c=nw]{left:0;top:0;cursor:nwse-resize}' + - '.spill .handle[data-c=ne]{left:100%;top:0;cursor:nesw-resize}' + - '.spill .handle[data-c=sw]{left:0;top:100%;cursor:nesw-resize}' + - '.spill .handle[data-c=se]{left:100%;top:100%;cursor:nwse-resize}' + - ':host([data-reframe]){z-index:10}' + - ':host([data-reframe]) .spill{display:block}' + - ':host([data-reframe]) .frame{box-shadow:0 0 0 2px #c96442}' + - '.empty{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;' + - ' justify-content:center;gap:6px;text-align:center;padding:12px;box-sizing:border-box;' + - ' cursor:pointer;user-select:none}' + - '.empty svg{opacity:.45}' + - '.empty .cap{max-width:90%;font-weight:500;letter-spacing:.01em}' + - '.empty .sub{font-size:11px}' + - '.empty .sub u{text-underline-offset:2px;text-decoration-color:rgba(0,0,0,.25)}' + - '.empty:hover .sub u{color:rgba(0,0,0,.75);text-decoration-color:currentColor}' + - ':host([data-over]) .frame{outline:2px solid #c96442;outline-offset:-2px;' + - ' background:rgba(201,100,66,.10)}' + - '.ring{position:absolute;inset:0;pointer-events:none;border:1.5px dashed rgba(0,0,0,.25);' + - ' transition:border-color .12s}' + - ':host([data-over]) .ring{border-color:#c96442}' + - ':host([data-filled]) .ring{display:none}' + + ".spill{position:absolute;transform:translate(-50%,-50%);display:none;z-index:1;" + + " cursor:grab;touch-action:none}" + + ":host([data-panning]) .spill{cursor:grabbing}" + + ".spill .ghost{position:absolute;inset:0;width:100%;height:100%;opacity:.35;" + + " pointer-events:none;-webkit-user-drag:none;user-select:none;" + + " box-shadow:0 0 0 1px rgba(0,0,0,.2),0 12px 32px rgba(0,0,0,.2)}" + + ".spill .handle{position:absolute;width:12px;height:12px;border-radius:50%;" + + " background:#fff;box-shadow:0 0 0 1.5px #c96442,0 1px 3px rgba(0,0,0,.3);" + + " transform:translate(-50%,-50%)}" + + ".spill .handle[data-c=nw]{left:0;top:0;cursor:nwse-resize}" + + ".spill .handle[data-c=ne]{left:100%;top:0;cursor:nesw-resize}" + + ".spill .handle[data-c=sw]{left:0;top:100%;cursor:nesw-resize}" + + ".spill .handle[data-c=se]{left:100%;top:100%;cursor:nwse-resize}" + + ":host([data-reframe]){z-index:10}" + + ":host([data-reframe]) .spill{display:block}" + + ":host([data-reframe]) .frame{box-shadow:0 0 0 2px #c96442}" + + ".empty{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;" + + " justify-content:center;gap:6px;text-align:center;padding:12px;box-sizing:border-box;" + + " cursor:pointer;user-select:none}" + + ".empty svg{opacity:.45}" + + ".empty .cap{max-width:90%;font-weight:500;letter-spacing:.01em}" + + ".empty .sub{font-size:11px}" + + ".empty .sub u{text-underline-offset:2px;text-decoration-color:rgba(0,0,0,.25)}" + + ".empty:hover .sub u{color:rgba(0,0,0,.75);text-decoration-color:currentColor}" + + ":host([data-over]) .frame{outline:2px solid #c96442;outline-offset:-2px;" + + " background:rgba(201,100,66,.10)}" + + ".ring{position:absolute;inset:0;pointer-events:none;border:1.5px dashed rgba(0,0,0,.25);" + + " transition:border-color .12s}" + + ":host([data-over]) .ring{border-color:#c96442}" + + ":host([data-filled]) .ring{display:none}" + // Controls sit BELOW the mask (top:100%), absolutely positioned so the // author-declared slot height is unaffected. The gap is padding, not a // top offset, so the hover target stays contiguous with the frame. - '.ctl{position:absolute;top:100%;left:50%;transform:translateX(-50%);padding-top:8px;' + - ' display:flex;gap:6px;opacity:0;pointer-events:none;transition:opacity .12s;z-index:2;' + - ' white-space:nowrap}' + - ':host([data-filled][data-editable]:hover) .ctl,:host([data-reframe]) .ctl' + - ' {opacity:1;pointer-events:auto}' + - '.ctl button{appearance:none;border:0;border-radius:6px;padding:5px 10px;cursor:pointer;' + - ' background:rgba(0,0,0,.65);color:#fff;font:11px/1 system-ui,-apple-system,sans-serif;' + - ' backdrop-filter:blur(6px)}' + - '.ctl button:hover{background:rgba(0,0,0,.8)}' + - '.err{position:absolute;left:8px;bottom:8px;right:8px;color:#b3261e;font-size:11px;' + - ' background:rgba(255,255,255,.85);padding:4px 6px;border-radius:5px;pointer-events:none}'; + ".ctl{position:absolute;top:100%;left:50%;transform:translateX(-50%);padding-top:8px;" + + " display:flex;gap:6px;opacity:0;pointer-events:none;transition:opacity .12s;z-index:2;" + + " white-space:nowrap}" + + ":host([data-filled][data-editable]:hover) .ctl,:host([data-reframe]) .ctl" + + " {opacity:1;pointer-events:auto}" + + ".ctl button{appearance:none;border:0;border-radius:6px;padding:5px 10px;cursor:pointer;" + + " background:rgba(0,0,0,.65);color:#fff;font:11px/1 system-ui,-apple-system,sans-serif;" + + " backdrop-filter:blur(6px)}" + + ".ctl button:hover{background:rgba(0,0,0,.8)}" + + ".err{position:absolute;left:8px;bottom:8px;right:8px;color:#b3261e;font-size:11px;" + + " background:rgba(255,255,255,.85);padding:4px 6px;border-radius:5px;pointer-events:none}"; const icon = '' + stylesheet + '' + + "" + '
' + ' ' + - '
' + icon + + '
' + + icon + '
' + '
or browse files
' + '
' + - '
' + + "
" + '
' + ' ' + '
' + '
' + - '
' + + "
" + '
' + '
' + - ''; - this._frame = root.querySelector('.frame'); - this._ring = root.querySelector('.ring'); - this._img = root.querySelector('.frame img'); - this._empty = root.querySelector('.empty'); - this._cap = root.querySelector('.cap'); - this._sub = root.querySelector('.sub'); - this._spill = root.querySelector('.spill'); - this._ghost = root.querySelector('.ghost'); + ''; + this._frame = root.querySelector(".frame"); + this._ring = root.querySelector(".ring"); + this._img = root.querySelector(".frame img"); + this._empty = root.querySelector(".empty"); + this._cap = root.querySelector(".cap"); + this._sub = root.querySelector(".sub"); + this._spill = root.querySelector(".spill"); + this._ghost = root.querySelector(".ghost"); this._err = null; - this._input = root.querySelector('input'); + this._input = root.querySelector("input"); this._depth = 0; this._gen = 0; this._view = { s: 1, x: 0, y: 0 }; this._subFn = () => this._render(); // Shadow-DOM listeners live with the shadow DOM — bound once here so // disconnect/reconnect (e.g. React remount) doesn't stack handlers. - this._empty.addEventListener('click', () => this._input.click()); - root.addEventListener('click', (e) => { - const act = e.target && e.target.getAttribute && e.target.getAttribute('data-act'); - if (act === 'replace') { this._exitReframe(true); this._input.click(); } - if (act === 'clear') { + this._empty.addEventListener("click", () => this._input.click()); + root.addEventListener("click", (e) => { + const act = e.target && e.target.getAttribute && e.target.getAttribute("data-act"); + if (act === "replace") { + this._exitReframe(true); + this._input.click(); + } + if (act === "clear") { this._exitReframe(false); this._gen++; this._local = null; - if (this.id) setSlot(this.id, null); else this._render(); + if (this.id) setSlot(this.id, null); + else this._render(); } }); - this._input.addEventListener('change', () => { + this._input.addEventListener("change", () => { const f = this._input.files && this._input.files[0]; if (f) this._ingest(f); - this._input.value = ''; + this._input.value = ""; }); // naturalWidth/Height aren't known until load — re-apply so the cover // baseline is computed from real dimensions, not the 100%×100% fallback. - this._img.addEventListener('load', () => this._applyView()); + this._img.addEventListener("load", () => this._applyView()); // Gated on editable + fit=cover so share links and contain/fill slots // stay static. - this.addEventListener('dblclick', (e) => { - if (!this.hasAttribute('data-editable') || !this._reframes()) return; + this.addEventListener("dblclick", (e) => { + if (!this.hasAttribute("data-editable") || !this._reframes()) return; e.preventDefault(); - if (this.hasAttribute('data-reframe')) this._exitReframe(true); + if (this.hasAttribute("data-reframe")) this._exitReframe(true); else this._enterReframe(); }); // Pan + resize both originate on the spill layer. A handle pointerdown // drives an aspect-locked resize anchored at the opposite corner; any // other pointerdown on the spill pans. Offsets are frame-% so a // reframed slot survives responsive resize / PPTX export. - this._spill.addEventListener('pointerdown', (e) => { - if (e.button !== 0 || !this.hasAttribute('data-reframe')) return; + this._spill.addEventListener("pointerdown", (e) => { + if (e.button !== 0 || !this.hasAttribute("data-reframe")) return; e.preventDefault(); e.stopPropagation(); this._spill.setPointerCapture(e.pointerId); const rect = this.getBoundingClientRect(); - const fw = rect.width || 1, fh = rect.height || 1; - const corner = e.target.getAttribute && e.target.getAttribute('data-c'); + const fw = rect.width || 1, + fh = rect.height || 1; + const corner = e.target.getAttribute && e.target.getAttribute("data-c"); let move; if (corner) { // Resize about the OPPOSITE corner. Viewport-px throughout (rect // fw/fh, not clientWidth) so the math survives a transform:scale() // ancestor — deck_stage renders slides scaled-to-fit. - const iw = this._img.naturalWidth || 1, ih = this._img.naturalHeight || 1; + const iw = this._img.naturalWidth || 1, + ih = this._img.naturalHeight || 1; const base = Math.max(fw / iw, fh / ih); - const sx = corner.includes('e') ? 1 : -1; - const sy = corner.includes('s') ? 1 : -1; + const sx = corner.includes("e") ? 1 : -1; + const sy = corner.includes("s") ? 1 : -1; const s0 = this._view.s; - const w0 = iw * base * s0, h0 = ih * base * s0; - const cx0 = (50 + this._view.x) / 100 * fw; - const cy0 = (50 + this._view.y) / 100 * fh; - const ox = cx0 - sx * w0 / 2, oy = cy0 - sy * h0 / 2; + const w0 = iw * base * s0, + h0 = ih * base * s0; + const cx0 = ((50 + this._view.x) / 100) * fw; + const cy0 = ((50 + this._view.y) / 100) * fh; + const ox = cx0 - (sx * w0) / 2, + oy = cy0 - (sy * h0) / 2; const diag0 = Math.hypot(w0, h0); - const ux = sx * w0 / diag0, uy = sy * h0 / diag0; + const ux = (sx * w0) / diag0, + uy = (sy * h0) / diag0; move = (ev) => { - const proj = (ev.clientX - rect.left - ox) * ux + - (ev.clientY - rect.top - oy) * uy; - const s = clampS(s0 * proj / diag0); - const d = diag0 * s / s0; + const proj = (ev.clientX - rect.left - ox) * ux + (ev.clientY - rect.top - oy) * uy; + const s = clampS((s0 * proj) / diag0); + const d = (diag0 * s) / s0; this._view.s = s; - this._view.x = (ox + ux * d / 2) / fw * 100 - 50; - this._view.y = (oy + uy * d / 2) / fh * 100 - 50; + this._view.x = ((ox + (ux * d) / 2) / fw) * 100 - 50; + this._view.y = ((oy + (uy * d) / 2) / fh) * 100 - 50; this._clampView(); this._applyView(); }; } else { - this.setAttribute('data-panning', ''); + this.setAttribute("data-panning", ""); const start = { px: e.clientX, py: e.clientY, x: this._view.x, y: this._view.y }; move = (ev) => { - this._view.x = start.x + (ev.clientX - start.px) / fw * 100; - this._view.y = start.y + (ev.clientY - start.py) / fh * 100; + this._view.x = start.x + ((ev.clientX - start.px) / fw) * 100; + this._view.y = start.y + ((ev.clientY - start.py) / fh) * 100; this._clampView(); this._applyView(); }; } const up = () => { - try { this._spill.releasePointerCapture(e.pointerId); } catch {} - this._spill.removeEventListener('pointermove', move); - this._spill.removeEventListener('pointerup', up); - this._spill.removeEventListener('pointercancel', up); - this.removeAttribute('data-panning'); + try { + this._spill.releasePointerCapture(e.pointerId); + } catch {} + this._spill.removeEventListener("pointermove", move); + this._spill.removeEventListener("pointerup", up); + this._spill.removeEventListener("pointercancel", up); + this.removeAttribute("data-panning"); this._dragUp = null; }; // Stashed so _exitReframe (Escape / outside-click mid-drag) can // tear the capture + listeners down synchronously. this._dragUp = up; - this._spill.addEventListener('pointermove', move); - this._spill.addEventListener('pointerup', up); - this._spill.addEventListener('pointercancel', up); + this._spill.addEventListener("pointermove", move); + this._spill.addEventListener("pointerup", up); + this._spill.addEventListener("pointercancel", up); }); // Wheel zoom stays available inside reframe mode as a trackpad nicety — // zooms toward the cursor (offset' = cursor·(1-k) + offset·k). - this.addEventListener('wheel', (e) => { - if (!this.hasAttribute('data-reframe')) return; - e.preventDefault(); - const r = this.getBoundingClientRect(); - const cx = (e.clientX - r.left) / r.width * 100 - 50; - const cy = (e.clientY - r.top) / r.height * 100 - 50; - const prev = this._view.s; - const next = clampS(prev * Math.pow(1.0015, -e.deltaY)); - if (next === prev) return; - const k = next / prev; - this._view.s = next; - this._view.x = cx * (1 - k) + this._view.x * k; - this._view.y = cy * (1 - k) + this._view.y * k; - this._clampView(); - this._applyView(); - }, { passive: false }); + this.addEventListener( + "wheel", + (e) => { + if (!this.hasAttribute("data-reframe")) return; + e.preventDefault(); + const r = this.getBoundingClientRect(); + const cx = ((e.clientX - r.left) / r.width) * 100 - 50; + const cy = ((e.clientY - r.top) / r.height) * 100 - 50; + const prev = this._view.s; + const next = clampS(prev * Math.pow(1.0015, -e.deltaY)); + if (next === prev) return; + const k = next / prev; + this._view.s = next; + this._view.x = cx * (1 - k) + this._view.x * k; + this._view.y = cy * (1 - k) + this._view.y * k; + this._clampView(); + this._applyView(); + }, + { passive: false } + ); } connectedCallback() { @@ -386,12 +424,12 @@ // cannot persist, and two id-less slots would share nothing. if (!this.id && !ImageSlot._warned) { ImageSlot._warned = true; - console.warn(' without an id will not persist its dropped image.'); + console.warn(" without an id will not persist its dropped image."); } - this.addEventListener('dragenter', this); - this.addEventListener('dragover', this); - this.addEventListener('dragleave', this); - this.addEventListener('drop', this); + this.addEventListener("dragenter", this); + this.addEventListener("dragover", this); + this.addEventListener("dragleave", this); + this.addEventListener("drop", this); subs.add(this._subFn); // width%/height% in _applyView encode the frame aspect at call time — // a host resize (responsive grid, pane divider) would stretch the @@ -407,17 +445,20 @@ disconnectedCallback() { subs.delete(this._subFn); - this.removeEventListener('dragenter', this); - this.removeEventListener('dragover', this); - this.removeEventListener('dragleave', this); - this.removeEventListener('drop', this); - if (this._ro) { this._ro.disconnect(); this._ro = null; } + this.removeEventListener("dragenter", this); + this.removeEventListener("dragover", this); + this.removeEventListener("dragleave", this); + this.removeEventListener("drop", this); + if (this._ro) { + this._ro.disconnect(); + this._ro = null; + } this._exitReframe(false); } _enterReframe() { - if (this.hasAttribute('data-reframe')) return; - this.setAttribute('data-reframe', ''); + if (this.hasAttribute("data-reframe")) return; + this.setAttribute("data-reframe", ""); this._applyView(); // Close on click outside (the spill handler stopPropagation()s so // in-image drags don't reach this) and on Escape. Listeners are held @@ -427,43 +468,50 @@ if (e.composedPath && e.composedPath().includes(this)) return; this._exitReframe(true); }; - this._esc = (e) => { if (e.key === 'Escape') this._exitReframe(true); }; - document.addEventListener('pointerdown', this._outside, true); - document.addEventListener('keydown', this._esc, true); + this._esc = (e) => { + if (e.key === "Escape") this._exitReframe(true); + }; + document.addEventListener("pointerdown", this._outside, true); + document.addEventListener("keydown", this._esc, true); } _exitReframe(commit) { - if (!this.hasAttribute('data-reframe')) return; + if (!this.hasAttribute("data-reframe")) return; if (this._dragUp) this._dragUp(); - this.removeAttribute('data-reframe'); - this.removeAttribute('data-panning'); - if (this._outside) document.removeEventListener('pointerdown', this._outside, true); - if (this._esc) document.removeEventListener('keydown', this._esc, true); + this.removeAttribute("data-reframe"); + this.removeAttribute("data-panning"); + if (this._outside) document.removeEventListener("pointerdown", this._outside, true); + if (this._esc) document.removeEventListener("keydown", this._esc, true); this._outside = this._esc = null; if (commit) this._commitView(); } - attributeChangedCallback() { if (this.shadowRoot) this._render(); } + attributeChangedCallback() { + if (this.shadowRoot) this._render(); + } // handleEvent — one listener object for all four drag events keeps the // add/remove symmetric and the depth counter correct. handleEvent(e) { - if (e.type === 'dragenter' || e.type === 'dragover') { + if (e.type === "dragenter" || e.type === "dragover") { // Without preventDefault the browser never fires 'drop'. e.preventDefault(); e.stopPropagation(); - if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy'; - if (e.type === 'dragenter') this._depth++; - this.setAttribute('data-over', ''); - } else if (e.type === 'dragleave') { + if (e.dataTransfer) e.dataTransfer.dropEffect = "copy"; + if (e.type === "dragenter") this._depth++; + this.setAttribute("data-over", ""); + } else if (e.type === "dragleave") { // dragenter/leave fire for every descendant crossing — count depth // so hovering the icon inside the empty state doesn't flicker. - if (--this._depth <= 0) { this._depth = 0; this.removeAttribute('data-over'); } - } else if (e.type === 'drop') { + if (--this._depth <= 0) { + this._depth = 0; + this.removeAttribute("data-over"); + } + } else if (e.type === "drop") { e.preventDefault(); e.stopPropagation(); this._depth = 0; - this.removeAttribute('data-over'); + this.removeAttribute("data-over"); const f = e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0]; if (f) this._ingest(f); } @@ -472,7 +520,7 @@ async _ingest(file) { this._setError(null); if (!file || ACCEPT.indexOf(file.type) < 0) { - this._setError('Drop a PNG, JPEG, WebP, or AVIF image.'); + this._setError("Drop a PNG, JPEG, WebP, or AVIF image."); return; } // toDataUrl can take hundreds of ms on a large photo. A Clear or a @@ -487,32 +535,43 @@ // or decode failure leaves the in-progress crop untouched. this._exitReframe(false); const val = { u: url, s: 1, x: 0, y: 0 }; - setSlot(this.id || '', val); + setSlot(this.id || "", val); // Keep a session-local copy for id-less slots so the drop still // shows, even though it cannot persist. - if (!this.id) { this._local = val; this._render(); } + if (!this.id) { + this._local = val; + this._render(); + } } catch (err) { if (gen !== this._gen) return; - this._setError('Could not read that image.'); - console.warn(' ingest failed:', err); + this._setError("Could not read that image."); + console.warn(" ingest failed:", err); } } _setError(msg) { - if (this._err) { this._err.remove(); this._err = null; } + if (this._err) { + this._err.remove(); + this._err = null; + } if (!msg) return; - const d = document.createElement('div'); - d.className = 'err'; d.textContent = msg; + const d = document.createElement("div"); + d.className = "err"; + d.textContent = msg; this.shadowRoot.appendChild(d); this._err = d; - setTimeout(() => { if (this._err === d) { d.remove(); this._err = null; } }, 3000); + setTimeout(() => { + if (this._err === d) { + d.remove(); + this._err = null; + } + }, 3000); } // Reframing (pan/resize) is only meaningful for fit=cover — contain/fill // keep the old object-fit path and double-click is a no-op. _reframes() { - return this.hasAttribute('data-filled') && - (this.getAttribute('fit') || 'cover') === 'cover'; + return this.hasAttribute("data-filled") && (this.getAttribute("fit") || "cover") === "cover"; } // Cover-baseline geometry, shared by clamp/apply/resize. Null until the @@ -521,8 +580,10 @@ // and clamping against a degenerate 1×1 frame would silently pull the // stored pan toward zero. _geom() { - const iw = this._img.naturalWidth, ih = this._img.naturalHeight; - const fw = this.clientWidth, fh = this.clientHeight; + const iw = this._img.naturalWidth, + ih = this._img.naturalHeight; + const fw = this.clientWidth, + fh = this.clientHeight; if (!iw || !ih || !fw || !fh) return null; return { iw, ih, fw, fh, base: Math.max(fw / iw, fh / ih) }; } @@ -531,23 +592,23 @@ // Pan range on each axis is half the overflow past the frame edge. const g = this._geom(); if (!g) return; - const mx = Math.max(0, (g.iw * g.base * this._view.s / g.fw - 1) * 50); - const my = Math.max(0, (g.ih * g.base * this._view.s / g.fh - 1) * 50); + const mx = Math.max(0, ((g.iw * g.base * this._view.s) / g.fw - 1) * 50); + const my = Math.max(0, ((g.ih * g.base * this._view.s) / g.fh - 1) * 50); this._view.x = Math.max(-mx, Math.min(mx, this._view.x)); this._view.y = Math.max(-my, Math.min(my, this._view.y)); } _applyView() { const g = this._geom(); - const fit = this.getAttribute('fit') || 'cover'; - if (fit !== 'cover' || !g) { + const fit = this.getAttribute("fit") || "cover"; + if (fit !== "cover" || !g) { // Non-cover, or dimensions not known yet (before img load). - this._img.style.width = '100%'; - this._img.style.height = '100%'; - this._img.style.left = '50%'; - this._img.style.top = '50%'; + this._img.style.width = "100%"; + this._img.style.height = "100%"; + this._img.style.left = "50%"; + this._img.style.top = "50%"; this._img.style.objectFit = fit; - this._img.style.objectPosition = this.getAttribute('position') || '50% 50%'; + this._img.style.objectPosition = this.getAttribute("position") || "50% 50%"; return; } // Cover baseline: img fills the frame on its tighter axis at s=1, so @@ -556,15 +617,19 @@ // frame aspect ratio, so a responsive resize keeps the same crop. The // spill layer mirrors the same box so its corners = image corners. const k = g.base * this._view.s; - const w = (g.iw * k / g.fw * 100) + '%'; - const h = (g.ih * k / g.fh * 100) + '%'; - const l = (50 + this._view.x) + '%'; - const t = (50 + this._view.y) + '%'; - this._img.style.width = w; this._img.style.height = h; - this._img.style.left = l; this._img.style.top = t; - this._img.style.objectFit = ''; - this._spill.style.width = w; this._spill.style.height = h; - this._spill.style.left = l; this._spill.style.top = t; + const w = ((g.iw * k) / g.fw) * 100 + "%"; + const h = ((g.ih * k) / g.fh) * 100 + "%"; + const l = 50 + this._view.x + "%"; + const t = 50 + this._view.y + "%"; + this._img.style.width = w; + this._img.style.height = h; + this._img.style.left = l; + this._img.style.top = t; + this._img.style.objectFit = ""; + this._spill.style.width = w; + this._spill.style.height = h; + this._spill.style.left = l; + this._spill.style.top = t; } _commitView() { @@ -573,7 +638,9 @@ // Framing-only (no u) persists too so an author-src slot remembers its // crop; clearing the sidecar still falls through to src=. if (this.id) setSlot(this.id, v); - else { this._local = v; } + else { + this._local = v; + } } _render() { @@ -581,24 +648,24 @@ // follow the rounded outline; clip-path is only applied for an // explicit `mask` (the ring is hidden there since a rectangle // dashed border chopped by an arbitrary polygon looks broken). - const mask = this.getAttribute('mask'); - const shape = (this.getAttribute('shape') || 'rounded').toLowerCase(); - let radius = ''; - if (shape === 'circle') radius = '50%'; - else if (shape === 'pill') radius = '9999px'; - else if (shape === 'rounded') { - const n = parseFloat(this.getAttribute('radius')); - radius = (Number.isFinite(n) ? n : 12) + 'px'; + const mask = this.getAttribute("mask"); + const shape = (this.getAttribute("shape") || "rounded").toLowerCase(); + let radius = ""; + if (shape === "circle") radius = "50%"; + else if (shape === "pill") radius = "9999px"; + else if (shape === "rounded") { + const n = parseFloat(this.getAttribute("radius")); + radius = (Number.isFinite(n) ? n : 12) + "px"; } - this._frame.style.borderRadius = mask ? '' : radius; - this._frame.style.clipPath = mask || ''; - this._ring.style.borderRadius = mask ? '' : radius; - this._ring.style.display = mask ? 'none' : ''; + this._frame.style.borderRadius = mask ? "" : radius; + this._frame.style.clipPath = mask || ""; + this._ring.style.borderRadius = mask ? "" : radius; + this._ring.style.display = mask ? "none" : ""; // Controls and reframe entry gate on this so share links stay read-only. const editable = !!(window.omelette && window.omelette.writeFile); - this.toggleAttribute('data-editable', editable); - this._sub.style.display = editable ? '' : 'none'; + this.toggleAttribute("data-editable", editable); + this._sub.style.display = editable ? "" : "none"; // Content. The sidecar is also writable by the agent's write_file // tool, so its value isn't guaranteed canvas-originated — only accept @@ -606,41 +673,41 @@ // (the author wrote it into the HTML) so it passes through unchanged. let stored = this.id ? getSlot(this.id) : this._local; if (stored && stored.u && !/^data:image\//i.test(stored.u)) stored = null; - const srcAttr = this.getAttribute('src') || ''; + const srcAttr = this.getAttribute("src") || ""; this._userUrl = (stored && stored.u) || null; const url = this._userUrl || srcAttr; // Don't clobber an in-flight reframe with a store-triggered re-render. - if (!this.hasAttribute('data-reframe')) { + if (!this.hasAttribute("data-reframe")) { this._view = { s: stored && Number.isFinite(stored.s) ? clampS(stored.s) : 1, x: stored && Number.isFinite(stored.x) ? stored.x : 0, y: stored && Number.isFinite(stored.y) ? stored.y : 0, }; } - this._cap.textContent = this.getAttribute('placeholder') || 'Drop an image'; + this._cap.textContent = this.getAttribute("placeholder") || "Drop an image"; // Toggle via style.display — the [hidden] attribute alone loses to // the display:flex / display:block rules in the stylesheet above. if (url) { - if (this._img.getAttribute('src') !== url) { + if (this._img.getAttribute("src") !== url) { this._img.src = url; this._ghost.src = url; } - this._img.style.display = 'block'; - this._empty.style.display = 'none'; - this.setAttribute('data-filled', ''); + this._img.style.display = "block"; + this._empty.style.display = "none"; + this.setAttribute("data-filled", ""); this._clampView(); this._applyView(); } else { - this._img.style.display = 'none'; - this._img.removeAttribute('src'); - this._ghost.removeAttribute('src'); - this._empty.style.display = 'flex'; - this.removeAttribute('data-filled'); + this._img.style.display = "none"; + this._img.removeAttribute("src"); + this._ghost.removeAttribute("src"); + this._empty.style.display = "flex"; + this.removeAttribute("data-filled"); } } } - if (!customElements.get('image-slot')) { - customElements.define('image-slot', ImageSlot); + if (!customElements.get("image-slot")) { + customElements.define("image-slot", ImageSlot); } })(); diff --git a/docs/design-system/ink-carbon/project/recordroom/mobile.css b/docs/design-system/ink-carbon/project/recordroom/mobile.css index 1672a2b26..eaf257e97 100644 --- a/docs/design-system/ink-carbon/project/recordroom/mobile.css +++ b/docs/design-system/ink-carbon/project/recordroom/mobile.css @@ -11,7 +11,10 @@ ═══════════════════════════════════════════════════════════════════ */ /* Mobile-only chrome, hidden on desktop */ -.rr-head__brand, .rr-menu-btn { display: none; } +.rr-head__brand, +.rr-menu-btn { + display: none; +} .rr-head__brand { align-items: center; gap: 10px; @@ -31,7 +34,9 @@ } .rr-drawer { position: absolute; - left: 0; top: 0; bottom: 0; + left: 0; + top: 0; + bottom: 0; width: min(300px, 84vw); background: color-mix(in oklab, var(--background) 88%, oklch(0 0 0)); border-right: 1px solid var(--border-strong); @@ -41,40 +46,95 @@ overflow-y: auto; } @media (prefers-reduced-motion: no-preference) { - .rr-drawer { animation: rr-drawer-in var(--duration-base) var(--ease-enter) both; } + .rr-drawer { + animation: rr-drawer-in var(--duration-base) var(--ease-enter) both; + } } @keyframes rr-drawer-in { - from { transform: translateX(-14px); opacity: 0; } - to { transform: translateX(0); opacity: 1; } + from { + transform: translateX(-14px); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} +.rr-drawer__nav { + display: flex; + flex-direction: column; + padding: 10px; + gap: 2px; +} +.rr-drawer .rr-nav-item { + padding: 12px 12px; + font-size: 15px; +} +.rr-drawer .rr-side__group { + padding: 16px 12px 5px; +} +.rr-drawer .rr-side__foot { + margin-top: auto; } -.rr-drawer__nav { display: flex; flex-direction: column; padding: 10px; gap: 2px; } -.rr-drawer .rr-nav-item { padding: 12px 12px; font-size: 15px; } -.rr-drawer .rr-side__group { padding: 16px 12px 5px; } -.rr-drawer .rr-side__foot { margin-top: auto; } /* ─── ≤860px — the phone grammar ─── */ @media (max-width: 860px) { - .rr-app { grid-template-columns: 1fr; height: 100dvh; } - .rr-side { display: none; } + .rr-app { + grid-template-columns: 1fr; + height: 100dvh; + } + .rr-side { + display: none; + } /* No sideways worlds: anything wider than the phone wraps or hides */ - .rr-content { overflow-x: hidden; } - .rr-content :is(.pdpp-sheet__serial, .rr-attn__meta, .pdpp-typed-sm) { white-space: normal; overflow-wrap: anywhere; } - .rr-hero__meta { white-space: normal; } + .rr-content { + overflow-x: hidden; + } + .rr-content :is(.pdpp-sheet__serial, .rr-attn__meta, .pdpp-typed-sm) { + white-space: normal; + overflow-wrap: anywhere; + } + .rr-hero__meta { + white-space: normal; + } - .rr-head { padding: 10px 16px; } - .rr-head__crumb { display: none; } - .rr-head__brand { display: flex; } - .rr-menu-btn { display: inline-flex; } - .rr-kbd { display: none; } + .rr-head { + padding: 10px 16px; + } + .rr-head__crumb { + display: none; + } + .rr-head__brand { + display: flex; + } + .rr-menu-btn { + display: inline-flex; + } + .rr-kbd { + display: none; + } - .rr-content { padding: 20px 16px 56px; } - .rr-page-head { flex-direction: column; align-items: flex-start; gap: 4px; margin-bottom: 18px; } - .rr-page-head__s { white-space: normal; } + .rr-content { + padding: 20px 16px 56px; + } + .rr-page-head { + flex-direction: column; + align-items: flex-start; + gap: 4px; + margin-bottom: 18px; + } + .rr-page-head__s { + white-space: normal; + } /* ── Tables restack: headers off, two-line records on the one axis ── */ - .pdpp-table__hrow { display: none; } - .pdpp-table { border-top: 1px solid var(--border-strong); } + .pdpp-table__hrow { + display: none; + } + .pdpp-table { + border-top: 1px solid var(--border-strong); + } /* Grants: monogram · client+endorse / scopes */ .rr-cols-grants .pdpp-data-row { @@ -82,8 +142,13 @@ gap: 3px 12px; padding: 12px 0; } - .rr-cols-grants .pdpp-data-row__detail { grid-column: 2 / -1; white-space: normal; } - .rr-cols-grants .pdpp-data-row__meta { display: none; } + .rr-cols-grants .pdpp-data-row__detail { + grid-column: 2 / -1; + white-space: normal; + } + .rr-cols-grants .pdpp-data-row__meta { + display: none; + } /* Runs & Schedules: connector+status / result */ .rr-cols-runs .pdpp-data-row, @@ -98,7 +163,9 @@ grid-row: 2; white-space: normal; } - .rr-cols-runs .pdpp-data-row__meta { display: none; } + .rr-cols-runs .pdpp-data-row__meta { + display: none; + } .rr-cols-schedules .pdpp-data-row__meta { grid-column: 1 / -1; grid-row: 3; @@ -106,79 +173,214 @@ } /* Traces: client+ruling / request+time */ - .rr-trace-row { grid-template-columns: minmax(0, 1fr) auto; gap: 2px 12px; padding: 11px 0; } - .rr-trace-row__who { grid-row: 1; grid-column: 1; } - .rr-decide { grid-row: 1; grid-column: 2; text-align: right; } - .rr-trace-row__what { grid-row: 2; grid-column: 1; } - .rr-trace-row__t { grid-row: 2; grid-column: 2; text-align: right; } - .rr-trace-row__n { display: none; } + .rr-trace-row { + grid-template-columns: minmax(0, 1fr) auto; + gap: 2px 12px; + padding: 11px 0; + } + .rr-trace-row__who { + grid-row: 1; + grid-column: 1; + } + .rr-decide { + grid-row: 1; + grid-column: 2; + text-align: right; + } + .rr-trace-row__what { + grid-row: 2; + grid-column: 1; + } + .rr-trace-row__t { + grid-row: 2; + grid-column: 2; + text-align: right; + } + .rr-trace-row__n { + display: none; + } /* Feed: stream+time / record */ - .rr-cols-feed .rr-feed-row { grid-template-columns: minmax(0, 1fr) auto; gap: 2px 12px; padding: 11px 0; } - .rr-feed-row__stream { grid-row: 1; grid-column: 1; } - .rr-feed-row__t { grid-row: 1; grid-column: 2; text-align: right; } - .rr-feed-row__body { grid-row: 2; grid-column: 1 / -1; white-space: normal; } - .rr-feed-row__id { display: none; } + .rr-cols-feed .rr-feed-row { + grid-template-columns: minmax(0, 1fr) auto; + gap: 2px 12px; + padding: 11px 0; + } + .rr-feed-row__stream { + grid-row: 1; + grid-column: 1; + } + .rr-feed-row__t { + grid-row: 1; + grid-column: 2; + text-align: right; + } + .rr-feed-row__body { + grid-row: 2; + grid-column: 1 / -1; + white-space: normal; + } + .rr-feed-row__id { + display: none; + } /* Readiness checks: glyph · name / detail */ - .rr-check { grid-template-columns: 20px minmax(0, 1fr); } - .rr-check__detail { grid-column: 2; } + .rr-check { + grid-template-columns: 20px minmax(0, 1fr); + } + .rr-check__detail { + grid-column: 2; + } /* Enforcement path: key above value, rail preserved */ - .rr-step { grid-template-columns: 1fr; gap: 2px; } + .rr-step { + grid-template-columns: 1fr; + gap: 2px; + } /* Attention rows: side column folds under on narrow screens */ - .rr-attn__row { grid-template-columns: minmax(0, 1fr); } - .rr-attn__side { grid-row: auto; grid-column: 1; flex-direction: row; align-items: center; justify-content: flex-start; } + .rr-attn__row { + grid-template-columns: minmax(0, 1fr); + } + .rr-attn__side { + grid-row: auto; + grid-column: 1; + flex-direction: row; + align-items: center; + justify-content: flex-start; + } /* kv rows: label above value */ - .pdpp-kv__row { grid-template-columns: 1fr; gap: 1px; padding: 7px 0; } + .pdpp-kv__row { + grid-template-columns: 1fr; + gap: 1px; + padding: 7px 0; + } /* ── Stat band: 2-up grid ── */ - .pdpp-band { grid-auto-flow: row; grid-template-columns: 1fr 1fr; } - .pdpp-band__cell { border-left: 0; } - .pdpp-band__cell:nth-child(even) { border-left: 1px solid var(--border); } - .pdpp-band__cell:nth-child(n+3) { border-top: 1px solid var(--border); } + .pdpp-band { + grid-auto-flow: row; + grid-template-columns: 1fr 1fr; + } + .pdpp-band__cell { + border-left: 0; + } + .pdpp-band__cell:nth-child(even) { + border-left: 1px solid var(--border); + } + .pdpp-band__cell:nth-child(n + 3) { + border-top: 1px solid var(--border); + } /* ── Overview ── */ - .rr-ov__grid { grid-template-columns: 1fr; gap: 24px; } - .rr-hero__sheet { flex-direction: column; align-items: stretch; gap: 14px; padding: 16px 18px; } - .rr-hero__sheet .pdpp-btn { align-self: flex-start; } - .rr-hero__title { font-size: 19px; } - .rr-hero__meta { white-space: normal; } + .rr-ov__grid { + grid-template-columns: 1fr; + gap: 24px; + } + .rr-hero__sheet { + flex-direction: column; + align-items: stretch; + gap: 14px; + padding: 16px 18px; + } + .rr-hero__sheet .pdpp-btn { + align-self: flex-start; + } + .rr-hero__title { + font-size: 19px; + } + .rr-hero__meta { + white-space: normal; + } /* ── Sheets: heads wrap (stream lenses), inspector flows below ── */ - .pdpp-sheet__head { flex-wrap: wrap; } - .rr-inspector { margin-top: 8px; } + .pdpp-sheet__head { + flex-wrap: wrap; + } + .rr-inspector { + margin-top: 8px; + } /* ── The ceremony becomes a bottom sheet ── */ - .rr-overlay { padding: 10px; align-items: flex-end; } - .rr-ceremony { width: 100%; } - .rr-ceremony .pdpp-sheet { max-height: 86dvh; overflow-y: auto; } - .rr-ceremony__kicker-row { flex-wrap: wrap; } - .rr-scope-decide { grid-template-columns: minmax(0, 1fr) auto; } - .rr-scope-decide__terms { grid-row: 2; grid-column: 1; } - .rr-scope-decide__desc { grid-row: 3; grid-column: 1; } - .rr-allow { grid-row: 1 / span 3; grid-column: 2; padding: 9px 13px; } - .rr-ceremony__foot { flex-wrap: wrap; gap: 10px; } + .rr-overlay { + padding: 10px; + align-items: flex-end; + } + .rr-ceremony { + width: 100%; + } + .rr-ceremony .pdpp-sheet { + max-height: 86dvh; + overflow-y: auto; + } + .rr-ceremony__kicker-row { + flex-wrap: wrap; + } + .rr-scope-decide { + grid-template-columns: minmax(0, 1fr) auto; + } + .rr-scope-decide__terms { + grid-row: 2; + grid-column: 1; + } + .rr-scope-decide__desc { + grid-row: 3; + grid-column: 1; + } + .rr-allow { + grid-row: 1 / span 3; + grid-column: 2; + padding: 9px 13px; + } + .rr-ceremony__foot { + flex-wrap: wrap; + gap: 10px; + } /* ── Palette ── */ - .rr-palette-overlay { padding-top: 8dvh; } - .rr-palette { width: min(540px, 94vw); } + .rr-palette-overlay { + padding-top: 8dvh; + } + .rr-palette { + width: min(540px, 94vw); + } /* Incoming banner stacks like the hero */ - .rr-incoming__sheet { flex-direction: column; align-items: stretch; gap: 10px; } - .rr-incoming__sheet .pdpp-btn--sm { align-self: flex-start; } - .rr-incoming__meta { white-space: normal; } + .rr-incoming__sheet { + flex-direction: column; + align-items: stretch; + gap: 10px; + } + .rr-incoming__sheet .pdpp-btn--sm { + align-self: flex-start; + } + .rr-incoming__meta { + white-space: normal; + } /* ── Touch targets ── */ - .pdpp-btn--sm { padding: 9px 14px; } - .rr-lens { padding: 8px 12px; } - .rr-chrome-btn { padding: 8px 12px; } + .pdpp-btn--sm { + padding: 9px 14px; + } + .rr-lens { + padding: 8px 12px; + } + .rr-chrome-btn { + padding: 8px 12px; + } /* ── Stream sheets: field chips wrap inside the box ── */ - .rr-stream__fields { padding: 12px 16px 14px; } - .pdpp-sheet__head { padding: 13px 16px; } - .pdpp-sheet__body { padding: 14px 16px; } - .pdpp-sheet__foot { padding: 11px 16px; flex-wrap: wrap; } + .rr-stream__fields { + padding: 12px 16px 14px; + } + .pdpp-sheet__head { + padding: 13px 16px; + } + .pdpp-sheet__body { + padding: 14px 16px; + } + .pdpp-sheet__foot { + padding: 11px 16px; + flex-wrap: wrap; + } } diff --git a/docs/design-system/ink-carbon/project/recordroom/overview.css b/docs/design-system/ink-carbon/project/recordroom/overview.css index e109e8b70..abb408ffb 100644 --- a/docs/design-system/ink-carbon/project/recordroom/overview.css +++ b/docs/design-system/ink-carbon/project/recordroom/overview.css @@ -5,7 +5,12 @@ /* RECORDROOM — "Standing" home. Strong hierarchy, one warm moment. */ -.rr-stand { display: flex; flex-direction: column; gap: 34px; max-width: 1080px; } +.rr-stand { + display: flex; + flex-direction: column; + gap: 34px; + max-width: 1080px; +} /* ─── The hero: the one truth, set large ─── */ .rr-stand-hero { @@ -33,7 +38,9 @@ max-width: 22ch; text-wrap: balance; } -.rr-stand-hero__line em { font-style: normal; } +.rr-stand-hero__line em { + font-style: normal; +} .rr-stand-hero__sub { font-size: 0.95rem; line-height: 1.55; @@ -41,14 +48,22 @@ margin: 2px 0 0; max-width: 60ch; } -.rr-stand-hero__foot { display: flex; align-items: center; gap: 16px; margin-top: 8px; flex-wrap: wrap; } +.rr-stand-hero__foot { + display: flex; + align-items: center; + gap: 16px; + margin-top: 8px; + flex-wrap: wrap; +} /* Tone: calm — the one place warmth is spent */ .rr-stand-hero.is-calm { border-color: var(--border-strong); box-shadow: inset 3px 0 0 var(--human); } -.rr-stand-hero.is-calm .rr-stand-hero__line em { color: var(--human); } +.rr-stand-hero.is-calm .rr-stand-hero__line em { + color: var(--human); +} .rr-stand-hero__seal { font-family: var(--font-sans); font-size: 0.95rem; @@ -60,14 +75,20 @@ } .rr-stand-hero__seal::before { content: ""; - width: 9px; height: 9px; + width: 9px; + height: 9px; background: var(--human); border-radius: 50%; } /* Tone: decide — a request awaits */ -.rr-stand-hero.is-decide { box-shadow: inset 3px 0 0 var(--human); } -.rr-stand-hero.is-decide .rr-stand-hero__line em { color: var(--human); font-weight: 700; } +.rr-stand-hero.is-decide { + box-shadow: inset 3px 0 0 var(--human); +} +.rr-stand-hero.is-decide .rr-stand-hero__line em { + color: var(--human); + font-weight: 700; +} /* Tone: alarm — something needs you */ .rr-stand-hero.is-alarm { @@ -75,7 +96,10 @@ box-shadow: inset 3px 0 0 var(--warning); background: var(--warning-tint); } -.rr-stand-hero.is-alarm .rr-stand-hero__line em { color: var(--warning); font-weight: 700; } +.rr-stand-hero.is-alarm .rr-stand-hero__line em { + color: var(--warning); + font-weight: 700; +} /* ─── What can act as you — the bearer tier, weightier on purpose ─── */ .rr-bearer { @@ -90,7 +114,10 @@ padding: 13px 16px; border-bottom: 1px solid var(--border); } -.rr-bearer__who { font-size: 0.95rem; font-weight: 700; } +.rr-bearer__who { + font-size: 0.95rem; + font-weight: 700; +} .rr-bearer__tag { font-family: var(--font-mono); font-size: 0.6rem; @@ -109,7 +136,12 @@ grid-column: 1 / 4; grid-row: 2; } -.rr-bearer__row .rr-rel__revoke { grid-row: 1 / span 2; grid-column: 4; opacity: 1; align-self: center; } +.rr-bearer__row .rr-rel__revoke { + grid-row: 1 / span 2; + grid-column: 4; + opacity: 1; + align-self: center; +} .rr-bearer__note { margin: 0; padding: 11px 16px 13px; @@ -124,7 +156,11 @@ grid-template-columns: 1fr 1fr; gap: 28px; } -.rr-stand-block { display: flex; flex-direction: column; gap: 12px; } +.rr-stand-block { + display: flex; + flex-direction: column; + gap: 12px; +} .rr-stand-block__head { display: flex; justify-content: space-between; @@ -142,7 +178,10 @@ } /* Who can read you — relationships, not scope IDs */ -.rr-rel-list { display: flex; flex-direction: column; } +.rr-rel-list { + display: flex; + flex-direction: column; +} .rr-rel { display: grid; grid-template-columns: minmax(0, 1fr) auto; @@ -151,8 +190,13 @@ border-bottom: 1px solid var(--border); align-items: baseline; } -.rr-rel:last-child { border-bottom: 0; } -.rr-rel__who { font-size: 0.95rem; font-weight: 600; } +.rr-rel:last-child { + border-bottom: 0; +} +.rr-rel__who { + font-size: 0.95rem; + font-weight: 600; +} .rr-rel__reads { grid-column: 1; font-size: 0.84rem; @@ -174,11 +218,20 @@ opacity: 0; transition: opacity var(--motion-state); } -.rr-rel:hover .rr-rel__revoke, .rr-rel:focus-within .rr-rel__revoke { opacity: 1; } -.rr-rel__revoke:hover { text-decoration: underline; text-underline-offset: 3px; } +.rr-rel:hover .rr-rel__revoke, +.rr-rel:focus-within .rr-rel__revoke { + opacity: 1; +} +.rr-rel__revoke:hover { + text-decoration: underline; + text-underline-offset: 3px; +} /* What's crossed lately */ -.rr-lately { display: flex; flex-direction: column; } +.rr-lately { + display: flex; + flex-direction: column; +} .rr-lately__row { display: grid; grid-template-columns: minmax(0, 1fr) auto; @@ -187,9 +240,17 @@ border-bottom: 1px solid var(--border); align-items: baseline; } -.rr-lately__row:last-child { border-bottom: 0; } -.rr-lately__row.is-deny { box-shadow: inset 2px 0 0 var(--warning); padding-left: 10px; } -.rr-lately__text { font-size: 0.84rem; line-height: 1.45; } +.rr-lately__row:last-child { + border-bottom: 0; +} +.rr-lately__row.is-deny { + box-shadow: inset 2px 0 0 var(--warning); + padding-left: 10px; +} +.rr-lately__text { + font-size: 0.84rem; + line-height: 1.45; +} .rr-lately__when { font-family: var(--font-mono); font-size: 0.66rem; @@ -206,15 +267,31 @@ border: 1px solid var(--border); box-shadow: inset 3px 0 0 var(--human); } -.rr-allclear__text { font-size: 0.88rem; color: var(--foreground); } -.rr-stand-empty { font-size: 0.84rem; color: var(--muted-foreground); margin: 8px 0 0; } +.rr-allclear__text { + font-size: 0.88rem; + color: var(--foreground); +} +.rr-stand-empty { + font-size: 0.84rem; + color: var(--muted-foreground); + margin: 8px 0 0; +} /* ─── Responsive ─── */ @media (max-width: 900px) { - .rr-stand-grid { grid-template-columns: 1fr; gap: 26px; } - .rr-stand-hero__line { font-size: 2rem; } + .rr-stand-grid { + grid-template-columns: 1fr; + gap: 26px; + } + .rr-stand-hero__line { + font-size: 2rem; + } } @media (max-width: 560px) { - .rr-stand-hero { padding: 22px 18px; } - .rr-stand-hero__line { font-size: 1.6rem; } + .rr-stand-hero { + padding: 22px 18px; + } + .rr-stand-hero__line { + font-size: 1.6rem; + } } diff --git a/docs/design-system/ink-carbon/project/recordroom/rr-app.jsx b/docs/design-system/ink-carbon/project/recordroom/rr-app.jsx index b239b14c3..a5d276706 100644 --- a/docs/design-system/ink-carbon/project/recordroom/rr-app.jsx +++ b/docs/design-system/ink-carbon/project/recordroom/rr-app.jsx @@ -2,454 +2,686 @@ // SPDX-License-Identifier: Apache-2.0 /* RECORDROOM — data + state + app shell (full console surface). */ -;(() => { - -const { useState, useEffect, useMemo } = React; -const { - RRSidebarFull, RRGrantRow, RRInspector, RRStreamSheet, RRActivityLog, RRCeremony, - RRTracesView, RROverviewView, RRExploreView, RRSourcesView, RRSourcesView2, RRRunsView, - RRSchedulesView, RRConnectView, RRDeploymentView, RRExportersView, - RRSubscriptionsView, RRCommandPalette, - RRSyncsView, RRConnectView2, RRExportersView2, RRSubscriptionsView2, RRAttentionList, - RROverview2, - useTweaks, TweaksPanel, TweakSection, TweakSlider, TweakRadio, TweakButton, -} = window; - -/* ─── Fixture data ─── */ - -const STREAMS = [ - { id: "pay_statements", connector: "Northstar HR", records: "312", - fields: ["employer", "period_start", "period_end", "gross_pay", "net_pay", "taxes_withheld", "benefits_detail", "bank_routing"] }, - { id: "employment", connector: "Northstar HR", records: "4", - fields: ["employer", "title", "start_date", "end_date", "manager_contact"] }, - { id: "transactions", connector: "First Meridian", records: "41,203", - fields: ["date", "amount", "merchant", "category", "account_ref", "memo"] }, - { id: "listening_history", connector: "Tonal", records: "6,597", - fields: ["track", "artist", "played_at", "device", "playlist_ref"] }, - { id: "tax_docs", connector: "Northstar HR", records: "12", - fields: ["doc_type", "tax_year", "employer", "document_blob"] }, -]; - -const BASE_GRANTS = [ - { - id: "grt_lngvw_01", client: "Longview Planning", - purpose: "long_term_financial_planning", - scopes: [ - { name: "pay_statements.read", terms: "append only · 2 yrs" }, - { name: "employment.read", terms: "current + 5 yrs" }, - ], - declined: ["tax_docs.read"], - status: "active", issued: "2025-10-14 09:22Z", - expiry: "exp 2026-12-14", expiresFull: "2026-12-14 09:22Z", - projections: { - pay_statements: ["employer", "period_start", "period_end", "gross_pay", "net_pay"], - employment: ["employer", "start_date", "end_date"], +(() => { + const { useState, useEffect, useMemo } = React; + const { + RRSidebarFull, + RRGrantRow, + RRInspector, + RRStreamSheet, + RRActivityLog, + RRCeremony, + RRTracesView, + RROverviewView, + RRExploreView, + RRSourcesView, + RRSourcesView2, + RRRunsView, + RRSchedulesView, + RRConnectView, + RRDeploymentView, + RRExportersView, + RRSubscriptionsView, + RRCommandPalette, + RRSyncsView, + RRConnectView2, + RRExportersView2, + RRSubscriptionsView2, + RRAttentionList, + RROverview2, + useTweaks, + TweaksPanel, + TweakSection, + TweakSlider, + TweakRadio, + TweakButton, + } = window; + + /* ─── Fixture data ─── */ + + const STREAMS = [ + { + id: "pay_statements", + connector: "Northstar HR", + records: "312", + fields: [ + "employer", + "period_start", + "period_end", + "gross_pay", + "net_pay", + "taxes_withheld", + "benefits_detail", + "bank_routing", + ], }, - }, - { - id: "grt_cncrt_02", client: "Concert Recommendations", - purpose: "live_event_suggestions", - scopes: [{ name: "listening_history.read", terms: "rolling 12 mo" }], - declined: [], - status: "continuous", issued: "2026-01-30 18:04Z", - expiry: "continuous", expiresFull: "renews monthly · next 2026-07-01", - projections: { listening_history: ["track", "artist", "played_at"] }, - }, - { - id: "grt_taxpr_03", client: "TaxPrep Co", - purpose: "annual_filing_2025", - scopes: [{ name: "tax_docs.read", terms: "single use" }], - declined: [], - status: "expiring", hoursLeft: 26, issued: "2026-06-09 11:40Z", - expiry: "exp 2026-06-12", expiresFull: "2026-06-12 11:40Z", - projections: { tax_docs: ["doc_type", "tax_year", "employer"] }, - }, - { - id: "grt_xwise_09", client: "Crosswise Ads", - purpose: "ad_personalization", - scopes: [{ name: "transactions.read", terms: "90 d window" }], - declined: [], - status: "revoked", issued: "2026-02-11 08:15Z", - revokedOn: "2026-05-02", revokedFull: "2026-05-02 14:40Z · by owner", - expiry: "—", expiresFull: "—", - projections: { transactions: ["date", "amount", "merchant"] }, - }, -]; - -const BASE_LOG = [ - { t: "2026-06-11 07:58Z", kind: "read", verb: "read", what: "pay_statements · 12 records · 5/8 fields", ref: "grt_lngvw_01" }, - { t: "2026-06-11 06:02Z", kind: "read", verb: "read", what: "listening_history · 214 records · 3/5 fields", ref: "grt_cncrt_02" }, - { t: "2026-06-10 22:17Z", kind: "read", verb: "read", what: "employment · 4 records · 3/5 fields", ref: "grt_lngvw_01" }, - { t: "2026-06-10 22:17Z", kind: "deny", verb: "deny", what: "tax_docs read attempt · scope not granted", ref: "grt_lngvw_01" }, - { t: "2026-06-09 11:40Z", kind: "consent", verb: "grant", what: "tax_docs.read · single use · TaxPrep Co", ref: "grt_taxpr_03" }, - { t: "2026-05-02 14:40Z", kind: "revoke", verb: "revoke", what: "transactions.read · Crosswise Ads · by owner", ref: "grt_xwise_09" }, - { t: "2026-05-02 14:39Z", kind: "deny", verb: "deny", what: "transactions read attempt · grant suspended", ref: "grt_xwise_09" }, -]; - -const INCOMING = { - id: "req_atlas_7f2k", - client: "Atlas Mortgage", - purposeHuman: "mortgage pre-approval", - scopes: [ - { name: "pay_statements.read", terms: "append only · 90 d", desc: "Employer, pay period, gross and net pay", allowed: true }, - { name: "employment.read", terms: "current + 5 yrs", desc: "Employers and dates — no salary history", allowed: true }, - { name: "transactions.read", terms: "90 d window", desc: "Spending detail from First Meridian", allowed: false }, - ], -}; - -/* ─── Persistence (additive; never clears foreign keys) ─── */ - -const LS_KEY = "recordroom_state_v2"; -function loadState() { - try { return JSON.parse(localStorage.getItem(LS_KEY)) || {}; } catch { return {}; } -} -function saveState(patch) { - const cur = loadState(); - localStorage.setItem(LS_KEY, JSON.stringify({ ...cur, ...patch })); -} - -function nowStamp() { - const d = new Date(); - const p = (n) => String(n).padStart(2, "0"); - return `${d.getUTCFullYear()}-${p(d.getUTCMonth() + 1)}-${p(d.getUTCDate())} ${p(d.getUTCHours())}:${p(d.getUTCMinutes())}Z`; -} - -const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ - "theme": "dark", - "density": "comfortable", - "carbonOffset": 9 -}/*EDITMODE-END*/; - -const NAV = [ - { id: "explore", label: "Explore" }, - { group: "Collection" }, - { id: "syncs", label: "Syncs" }, - { id: "sources", label: "Sources" }, - { group: "Sharing" }, - { id: "grants", label: "Grants" }, - { id: "traces", label: "Traces" }, - { group: "Server" }, - { id: "connect", label: "Connect AI apps" }, - { id: "deployment", label: "Deployment" }, - { id: "exporters", label: "Device exporters" }, - { id: "events", label: "Event subscriptions" }, - { group: "Glance" }, - { id: "overview", label: "Standing" }, -]; - -const HEADS = { - overview: ["Overview", "where you stand"], - explore: ["Explore", "the reading room · 10 connections · only you see this"], - sources: ["Sources", "the loading dock · 10 instances · what arrives, from where, configured how"], - traces: ["Traces", "every request, accounted for"], - grants: ["Grants", ""], - syncs: ["Syncs", "is your data arriving · schedule + result, per stream"], - connect: ["Connect AI apps", "MCP · reads flow through grants"], - deployment: ["Deployment", "readiness · endpoints · owner tokens"], - exporters: ["Device exporters", "your devices, pushing home"], - events: ["Event subscriptions", "webhooks on protocol events"], -}; - -/* ─── App ─── */ - -function App() { - const persisted = useMemo(loadState, []); - const [t, setTweak] = useTweaks(TWEAK_DEFAULTS); - const [view, setView] = useState("explore"); - const [selected, setSelected] = useState("grt_lngvw_01"); - const [revoking, setRevoking] = useState(false); - const [striking, setStriking] = useState(false); - const [requestState, setRequestState] = useState(persisted.requestState || "pending"); - const [reqScopes, setReqScopes] = useState(INCOMING.scopes); - const [pressing, setPressing] = useState(false); - const [paletteOpen, setPaletteOpen] = useState(false); - const [exploreSeed, setExploreSeed] = useState(null); - const [navOpen, setNavOpen] = useState(false); - const [extraGrants, setExtraGrants] = useState(persisted.extraGrants || []); - const [revokedIds, setRevokedIds] = useState(persisted.revokedIds || []); - const [extraLog, setExtraLog] = useState(persisted.extraLog || []); - const [recents, setRecents] = useState(persisted.paletteRecents || []); - - useEffect(() => { - document.documentElement.classList.toggle("dark", t.theme === "dark"); - document.documentElement.style.setProperty("--carbon-offset", t.carbonOffset + "px"); - }, [t.theme, t.carbonOffset]); - - useEffect(() => { - function onKey(e) { - if ((e.metaKey || e.ctrlKey) && e.key === "k") { e.preventDefault(); setPaletteOpen((o) => !o); } - } - window.addEventListener("keydown", onKey); - return () => window.removeEventListener("keydown", onKey); - }, []); - - useEffect(() => { - if (!navOpen) return undefined; - function onKey(e) { if (e.key === "Escape") setNavOpen(false); } - window.addEventListener("keydown", onKey); - return () => window.removeEventListener("keydown", onKey); - }, [navOpen]); - - // Keyboard-first ledger: ↑↓ move selection, Escape backs out of a revoke. - useEffect(() => { - if (view !== "grants" || paletteOpen || requestState === "open") return undefined; - function onKey(e) { - if (e.target && /^(INPUT|TEXTAREA|SELECT)$/.test(e.target.tagName)) return; - if (e.key === "ArrowDown" || e.key === "ArrowUp") { - e.preventDefault(); - setSelected((cur) => { - const i = grants.findIndex((g) => g.id === cur); - const next = e.key === "ArrowDown" ? Math.min(i + 1, grants.length - 1) : Math.max(i - 1, 0); - return grants[next].id; - }); - setRevoking(false); - setStriking(false); - } - if (e.key === "Escape") { setRevoking(false); } - } - window.addEventListener("keydown", onKey); - return () => window.removeEventListener("keydown", onKey); - }, [view, paletteOpen, requestState, grants]); - - const grants = useMemo(() => { - const all = [...extraGrants, ...BASE_GRANTS]; - return all.map((g) => - revokedIds.includes(g.id) && g.status !== "revoked" - ? { ...g, status: "revoked", revokedOn: g.revokedOn || "2026-06-11", revokedFull: g.revokedFull || nowStamp() + " · by owner" } - : g - ); - }, [extraGrants, revokedIds]); - - const log = useMemo(() => [...extraLog, ...BASE_LOG], [extraLog]); - const grant = grants.find((g) => g.id === selected) || null; - const activeCount = grants.filter((g) => g.status !== "revoked").length; - - function addLog(entry) { - setExtraLog((cur) => { - const next = [{ ...entry, fresh: true }, ...cur]; - saveState({ extraLog: next.map(({ fresh, ...e }) => e) }); - return next; - }); - } + { + id: "employment", + connector: "Northstar HR", + records: "4", + fields: ["employer", "title", "start_date", "end_date", "manager_contact"], + }, + { + id: "transactions", + connector: "First Meridian", + records: "41,203", + fields: ["date", "amount", "merchant", "category", "account_ref", "memo"], + }, + { + id: "listening_history", + connector: "Tonal", + records: "6,597", + fields: ["track", "artist", "played_at", "device", "playlist_ref"], + }, + { + id: "tax_docs", + connector: "Northstar HR", + records: "12", + fields: ["doc_type", "tax_year", "employer", "document_blob"], + }, + ]; - function approve() { - setPressing(true); - const allowed = reqScopes.filter((s) => s.allowed); - const declined = reqScopes.filter((s) => !s.allowed).map((s) => s.name); - const newGrant = { - id: "grt_atlas_05", client: INCOMING.client, - purpose: "mortgage_preapproval", - scopes: allowed.map(({ name, terms }) => ({ name, terms })), - declined, - status: "active", issued: nowStamp(), - expiry: "exp 2026-09-09", expiresFull: "2026-09-09 · 90 d term", - justAdded: true, + const BASE_GRANTS = [ + { + id: "grt_lngvw_01", + client: "Longview Planning", + purpose: "long_term_financial_planning", + scopes: [ + { name: "pay_statements.read", terms: "append only · 2 yrs" }, + { name: "employment.read", terms: "current + 5 yrs" }, + ], + declined: ["tax_docs.read"], + status: "active", + issued: "2025-10-14 09:22Z", + expiry: "exp 2026-12-14", + expiresFull: "2026-12-14 09:22Z", projections: { pay_statements: ["employer", "period_start", "period_end", "gross_pay", "net_pay"], employment: ["employer", "start_date", "end_date"], }, - }; - setTimeout(() => { - setExtraGrants((cur) => { - const next = [newGrant, ...cur]; - saveState({ extraGrants: next.map(({ justAdded, ...g }) => g), requestState: "approved" }); - return next; - }); - addLog({ t: nowStamp(), kind: "consent", verb: "grant", what: `${allowed.length} scopes · ${declined.length} declined · ${INCOMING.client}`, ref: newGrant.id }); - setRequestState("approved"); - setPressing(false); - setSelected(newGrant.id); - setView("grants"); - }, 1900); - } + }, + { + id: "grt_cncrt_02", + client: "Concert Recommendations", + purpose: "live_event_suggestions", + scopes: [{ name: "listening_history.read", terms: "rolling 12 mo" }], + declined: [], + status: "continuous", + issued: "2026-01-30 18:04Z", + expiry: "continuous", + expiresFull: "renews monthly · next 2026-07-01", + projections: { listening_history: ["track", "artist", "played_at"] }, + }, + { + id: "grt_taxpr_03", + client: "TaxPrep Co", + purpose: "annual_filing_2025", + scopes: [{ name: "tax_docs.read", terms: "single use" }], + declined: [], + status: "expiring", + hoursLeft: 26, + issued: "2026-06-09 11:40Z", + expiry: "exp 2026-06-12", + expiresFull: "2026-06-12 11:40Z", + projections: { tax_docs: ["doc_type", "tax_year", "employer"] }, + }, + { + id: "grt_xwise_09", + client: "Crosswise Ads", + purpose: "ad_personalization", + scopes: [{ name: "transactions.read", terms: "90 d window" }], + declined: [], + status: "revoked", + issued: "2026-02-11 08:15Z", + revokedOn: "2026-05-02", + revokedFull: "2026-05-02 14:40Z · by owner", + expiry: "—", + expiresFull: "—", + projections: { transactions: ["date", "amount", "merchant"] }, + }, + ]; - function refuse() { - setRequestState("refused"); - saveState({ requestState: "refused" }); - addLog({ t: nowStamp(), kind: "deny", verb: "refuse", what: `access request refused · ${INCOMING.client}`, ref: INCOMING.id }); - } + const BASE_LOG = [ + { + t: "2026-06-11 07:58Z", + kind: "read", + verb: "read", + what: "pay_statements · 12 records · 5/8 fields", + ref: "grt_lngvw_01", + }, + { + t: "2026-06-11 06:02Z", + kind: "read", + verb: "read", + what: "listening_history · 214 records · 3/5 fields", + ref: "grt_cncrt_02", + }, + { + t: "2026-06-10 22:17Z", + kind: "read", + verb: "read", + what: "employment · 4 records · 3/5 fields", + ref: "grt_lngvw_01", + }, + { + t: "2026-06-10 22:17Z", + kind: "deny", + verb: "deny", + what: "tax_docs read attempt · scope not granted", + ref: "grt_lngvw_01", + }, + { + t: "2026-06-09 11:40Z", + kind: "consent", + verb: "grant", + what: "tax_docs.read · single use · TaxPrep Co", + ref: "grt_taxpr_03", + }, + { + t: "2026-05-02 14:40Z", + kind: "revoke", + verb: "revoke", + what: "transactions.read · Crosswise Ads · by owner", + ref: "grt_xwise_09", + }, + { + t: "2026-05-02 14:39Z", + kind: "deny", + verb: "deny", + what: "transactions read attempt · grant suspended", + ref: "grt_xwise_09", + }, + ]; - function confirmRevoke() { - // Optimistic: the record flips NOW; the strike draws as confirmation, not as a wait. - const g = grant; - setStriking(true); - setRevoking(false); - setRevokedIds((cur) => { - const next = [...cur, selected]; - saveState({ revokedIds: next }); - return next; - }); - addLog({ t: nowStamp(), kind: "revoke", verb: "revoke", what: `${g.scopes.map((s) => s.name).join(" · ")} · ${g.client} · by owner`, ref: g.id }); - setTimeout(() => setStriking(false), 520); - } + const INCOMING = { + id: "req_atlas_7f2k", + client: "Atlas Mortgage", + purposeHuman: "mortgage pre-approval", + scopes: [ + { + name: "pay_statements.read", + terms: "append only · 90 d", + desc: "Employer, pay period, gross and net pay", + allowed: true, + }, + { + name: "employment.read", + terms: "current + 5 yrs", + desc: "Employers and dates — no salary history", + allowed: true, + }, + { name: "transactions.read", terms: "90 d window", desc: "Spending detail from First Meridian", allowed: false }, + ], + }; + + /* ─── Persistence (additive; never clears foreign keys) ─── */ - function recordRecent(label) { - setRecents((prev) => { - const next = [label, ...prev.filter((l) => l !== label)].slice(0, 5); - saveState({ paletteRecents: next }); - return next; - }); + const LS_KEY = "recordroom_state_v2"; + function loadState() { + try { + return JSON.parse(localStorage.getItem(LS_KEY)) || {}; + } catch { + return {}; + } + } + function saveState(patch) { + const cur = loadState(); + localStorage.setItem(LS_KEY, JSON.stringify({ ...cur, ...patch })); } - function browseInExplore(conId, streamName) { - setExploreSeed({ con: conId, stream: streamName, n: Date.now() }); - setView("explore"); + function nowStamp() { + const d = new Date(); + const p = (n) => String(n).padStart(2, "0"); + return `${d.getUTCFullYear()}-${p(d.getUTCMonth() + 1)}-${p(d.getUTCDate())} ${p(d.getUTCHours())}:${p(d.getUTCMinutes())}Z`; } - const paletteItems = [ - ...NAV.filter((n) => n.id).map((n) => ({ label: n.label, kind: "view", run: () => setView(n.id) })), - ...grants.map((g) => ({ label: g.client + " — " + g.id, kind: "grant", run: () => { setView("grants"); setSelected(g.id); } })), - ...STREAMS.map((s) => ({ label: s.id, kind: "stream", run: () => setView("sources") })), - { label: "Reauthorize First Meridian", kind: "action", run: () => setView("syncs") }, - ...(requestState === "pending" ? [{ label: "Review Atlas Mortgage request", kind: "action", run: () => setRequestState("open") }] : []), - { label: "Toggle theme", kind: "action", run: () => setTweak("theme", t.theme === "dark" ? "light" : "dark") }, + const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/ { + theme: "dark", + density: "comfortable", + carbonOffset: 9, + } /*EDITMODE-END*/; + + const NAV = [ + { id: "explore", label: "Explore" }, + { group: "Collection" }, + { id: "syncs", label: "Syncs" }, + { id: "sources", label: "Sources" }, + { group: "Sharing" }, + { id: "grants", label: "Grants" }, + { id: "traces", label: "Traces" }, + { group: "Server" }, + { id: "connect", label: "Connect AI apps" }, + { id: "deployment", label: "Deployment" }, + { id: "exporters", label: "Device exporters" }, + { id: "events", label: "Event subscriptions" }, + { group: "Glance" }, + { id: "overview", label: "Standing" }, ]; - const heads = { ...HEADS, grants: ["Grants", `${activeCount} in effect · ${grants.length - activeCount} struck · ↑↓ select`] }; - - return ( -
- -
-
- Recordroom - rs.okafor.recordroom.net · pdpp 0.1.0 -
- - - -
-
-
-
-
-

{heads[view][0]}

- {heads[view][1]} -
- {view === "overview" && ( - { setView("grants"); setSelected(id); }} - onReview={() => setRequestState("open")} - requestState={requestState} - /> - )} - {view === "explore" && setPaletteOpen(true)} seed={exploreSeed} />} - {view === "sources" && } - {view === "traces" && } - {view === "grants" && ( -
- {requestState === "pending" && ( -
-
- - Atlas Mortgage asks to read 3 streams - staged · req_atlas_7f2k · purpose: mortgage_preapproval - - -
-
- )} -
-
- - client - scopes - status - expires -
- {grants.map((g) => ( - { setSelected(id); setRevoking(false); setStriking(false); }} selected={selected === g.id} /> - ))} -
-
- )} - {view === "grants" && ( - setRevoking(false)} - onRevokeConfirm={confirmRevoke} - onRevokeStart={() => setRevoking(true)} - revoking={revoking} - streams={STREAMS} - striking={striking} - /> - )} - {view === "syncs" && } - {view === "connect" && } - {view === "deployment" && } - {view === "exporters" && } - {view === "events" && } - {view === "activity" && } -
-
-
+ const HEADS = { + overview: ["Overview", "where you stand"], + explore: ["Explore", "the reading room · 10 connections · only you see this"], + sources: ["Sources", "the loading dock · 10 instances · what arrives, from where, configured how"], + traces: ["Traces", "every request, accounted for"], + grants: ["Grants", ""], + syncs: ["Syncs", "is your data arriving · schedule + result, per stream"], + connect: ["Connect AI apps", "MCP · reads flow through grants"], + deployment: ["Deployment", "readiness · endpoints · owner tokens"], + exporters: ["Device exporters", "your devices, pushing home"], + events: ["Event subscriptions", "webhooks on protocol events"], + }; + + /* ─── App ─── */ + + function App() { + const persisted = useMemo(loadState, []); + const [t, setTweak] = useTweaks(TWEAK_DEFAULTS); + const [view, setView] = useState("explore"); + const [selected, setSelected] = useState("grt_lngvw_01"); + const [revoking, setRevoking] = useState(false); + const [striking, setStriking] = useState(false); + const [requestState, setRequestState] = useState(persisted.requestState || "pending"); + const [reqScopes, setReqScopes] = useState(INCOMING.scopes); + const [pressing, setPressing] = useState(false); + const [paletteOpen, setPaletteOpen] = useState(false); + const [exploreSeed, setExploreSeed] = useState(null); + const [navOpen, setNavOpen] = useState(false); + const [extraGrants, setExtraGrants] = useState(persisted.extraGrants || []); + const [revokedIds, setRevokedIds] = useState(persisted.revokedIds || []); + const [extraLog, setExtraLog] = useState(persisted.extraLog || []); + const [recents, setRecents] = useState(persisted.paletteRecents || []); + + useEffect(() => { + document.documentElement.classList.toggle("dark", t.theme === "dark"); + document.documentElement.style.setProperty("--carbon-offset", t.carbonOffset + "px"); + }, [t.theme, t.carbonOffset]); + + useEffect(() => { + function onKey(e) { + if ((e.metaKey || e.ctrlKey) && e.key === "k") { + e.preventDefault(); + setPaletteOpen((o) => !o); + } + } + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }, []); + + useEffect(() => { + if (!navOpen) return undefined; + function onKey(e) { + if (e.key === "Escape") setNavOpen(false); + } + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }, [navOpen]); + + // Keyboard-first ledger: ↑↓ move selection, Escape backs out of a revoke. + useEffect(() => { + if (view !== "grants" || paletteOpen || requestState === "open") return undefined; + function onKey(e) { + if (e.target && /^(INPUT|TEXTAREA|SELECT)$/.test(e.target.tagName)) return; + if (e.key === "ArrowDown" || e.key === "ArrowUp") { + e.preventDefault(); + setSelected((cur) => { + const i = grants.findIndex((g) => g.id === cur); + const next = e.key === "ArrowDown" ? Math.min(i + 1, grants.length - 1) : Math.max(i - 1, 0); + return grants[next].id; + }); + setRevoking(false); + setStriking(false); + } + if (e.key === "Escape") { + setRevoking(false); + } + } + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }, [view, paletteOpen, requestState, grants]); + + const grants = useMemo(() => { + const all = [...extraGrants, ...BASE_GRANTS]; + return all.map((g) => + revokedIds.includes(g.id) && g.status !== "revoked" + ? { + ...g, + status: "revoked", + revokedOn: g.revokedOn || "2026-06-11", + revokedFull: g.revokedFull || nowStamp() + " · by owner", + } + : g + ); + }, [extraGrants, revokedIds]); + + const log = useMemo(() => [...extraLog, ...BASE_LOG], [extraLog]); + const grant = grants.find((g) => g.id === selected) || null; + const activeCount = grants.filter((g) => g.status !== "revoked").length; + + function addLog(entry) { + setExtraLog((cur) => { + const next = [{ ...entry, fresh: true }, ...cur]; + saveState({ extraLog: next.map(({ fresh, ...e }) => e) }); + return next; + }); + } + + function approve() { + setPressing(true); + const allowed = reqScopes.filter((s) => s.allowed); + const declined = reqScopes.filter((s) => !s.allowed).map((s) => s.name); + const newGrant = { + id: "grt_atlas_05", + client: INCOMING.client, + purpose: "mortgage_preapproval", + scopes: allowed.map(({ name, terms }) => ({ name, terms })), + declined, + status: "active", + issued: nowStamp(), + expiry: "exp 2026-09-09", + expiresFull: "2026-09-09 · 90 d term", + justAdded: true, + projections: { + pay_statements: ["employer", "period_start", "period_end", "gross_pay", "net_pay"], + employment: ["employer", "start_date", "end_date"], + }, + }; + setTimeout(() => { + setExtraGrants((cur) => { + const next = [newGrant, ...cur]; + saveState({ extraGrants: next.map(({ justAdded, ...g }) => g), requestState: "approved" }); + return next; + }); + addLog({ + t: nowStamp(), + kind: "consent", + verb: "grant", + what: `${allowed.length} scopes · ${declined.length} declined · ${INCOMING.client}`, + ref: newGrant.id, + }); + setRequestState("approved"); + setPressing(false); + setSelected(newGrant.id); + setView("grants"); + }, 1900); + } - {navOpen && ( -
setNavOpen(false)}> - -
- )} - - {requestState === "open" && ( - { if (!pressing) setRequestState("pending"); }} - onRefuse={refuse} - onToggle={(i) => setReqScopes((cur) => cur.map((s, j) => (j === i ? { ...s, allowed: !s.allowed } : s)))} - pressing={pressing} - request={{ ...INCOMING, scopes: reqScopes }} +
+
+ + {navOpen && ( +
setNavOpen(false)}> + +
+ )} + + {requestState === "open" && ( + { + if (!pressing) setRequestState("pending"); + }} + onRefuse={refuse} + onToggle={(i) => setReqScopes((cur) => cur.map((s, j) => (j === i ? { ...s, allowed: !s.allowed } : s)))} + pressing={pressing} + request={{ ...INCOMING, scopes: reqScopes }} + /> + )} + + setPaletteOpen(false)} + onExec={recordRecent} + open={paletteOpen} + recents={recents} /> - )} - - setPaletteOpen(false)} onExec={recordRecent} open={paletteOpen} recents={recents} /> - - - - setTweak("theme", v)} options={["dark", "light"]} value={t.theme} /> - setTweak("density", v)} options={["comfortable", "compact"]} value={t.density} /> - - setTweak("carbonOffset", v)} unit="px" value={t.carbonOffset} /> - - { localStorage.setItem(LS_KEY, "{}"); location.reload(); }} /> - - - ); -} - -ReactDOM.createRoot(document.getElementById("root")).render(); + + + + setTweak("theme", v)} + options={["dark", "light"]} + value={t.theme} + /> + setTweak("density", v)} + options={["comfortable", "compact"]} + value={t.density} + /> + + setTweak("carbonOffset", v)} + unit="px" + value={t.carbonOffset} + /> + + { + localStorage.setItem(LS_KEY, "{}"); + location.reload(); + }} + /> + + + ); + } + + ReactDOM.createRoot(document.getElementById("root")).render(); })(); diff --git a/docs/design-system/ink-carbon/project/recordroom/rr-components.jsx b/docs/design-system/ink-carbon/project/recordroom/rr-components.jsx index 301800d5d..b4ee9a0f7 100644 --- a/docs/design-system/ink-carbon/project/recordroom/rr-components.jsx +++ b/docs/design-system/ink-carbon/project/recordroom/rr-components.jsx @@ -2,394 +2,428 @@ // SPDX-License-Identifier: Apache-2.0 /* RECORDROOM — view components. Data lives in rr-app.jsx; these render it. */ -;(() => { +(() => { + const { useState, useEffect } = React; -const { useState, useEffect } = React; + /* ─── Shared bits ─── */ -/* ─── Shared bits ─── */ - -function Endorse({ status, hours }) { - const map = { - active: ["pdpp-endorse--active", "active"], - continuous: ["pdpp-endorse--continuous", "continuous"], - expiring: ["pdpp-endorse--expiring", `expiring ${hours}h`], - revoked: ["pdpp-endorse--revoked", "revoked"], - }; - const [cls, label] = map[status] || map.active; - return {label}; -} - -function Sidebar({ view, onView, counts }) { - const items = [ - ["grants", "Grants", counts.grants], - ["streams", "Streams", counts.streams], - ["activity", "Activity", counts.activity], - ]; - return ( - - ); -} + function Endorse({ status, hours }) { + const map = { + active: ["pdpp-endorse--active", "active"], + continuous: ["pdpp-endorse--continuous", "continuous"], + expiring: ["pdpp-endorse--expiring", `expiring ${hours}h`], + revoked: ["pdpp-endorse--revoked", "revoked"], + }; + const [cls, label] = map[status] || map.active; + return {label}; + } -function SidebarFull({ view, onView, nav, counts }) { - return ( - - ); -} + + ); + } -/* ─── Grants view ─── */ + function SidebarFull({ view, onView, nav, counts }) { + return ( + + ); + } -function monogram(name) { - const words = name.split(/\s+/).filter(Boolean); - return (words.length > 1 ? words[0][0] + words[1][0] : name.slice(0, 2)).toUpperCase(); -} + /* ─── Grants view ─── */ -function GrantRow({ grant, selected, onSelect }) { - const cls = - "pdpp-data-row" + - (grant.status === "revoked" ? " pdpp-data-row--revoked" : "") + - (grant.justAdded ? " pdpp-data-row--landed" : ""); - return ( - - ); -} + function monogram(name) { + const words = name.split(/\s+/).filter(Boolean); + return (words.length > 1 ? words[0][0] + words[1][0] : name.slice(0, 2)).toUpperCase(); + } + + function GrantRow({ grant, selected, onSelect }) { + const cls = + "pdpp-data-row" + + (grant.status === "revoked" ? " pdpp-data-row--revoked" : "") + + (grant.justAdded ? " pdpp-data-row--landed" : ""); + return ( + + ); + } -function fmtToken(v) { - // allow long protocol identifiers to wrap at their own joints - return String(v).split("_").join("_\u200b"); -} + function fmtToken(v) { + // allow long protocol identifiers to wrap at their own joints + return String(v).split("_").join("_\u200b"); + } -const STREAM_HUMAN = { - pay_statements: "Your pay", - employment: "Your work history", - transactions: "Your spending", - listening_history: "Your listening", - tax_docs: "Your tax documents", - browsing: "Your browsing", -}; + const STREAM_HUMAN = { + pay_statements: "Your pay", + employment: "Your work history", + transactions: "Your spending", + listening_history: "Your listening", + tax_docs: "Your tax documents", + browsing: "Your browsing", + }; -function Inspector({ grant, streams, log, revoking, striking, onRevokeStart, onRevokeConfirm, onRevokeCancel }) { - if (!grant) { + function Inspector({ grant, streams, log, revoking, striking, onRevokeStart, onRevokeConfirm, onRevokeCancel }) { + if (!grant) { + return ( +
+
Select a grant to read your copy.
+
+ ); + } + const revoked = grant.status === "revoked"; + const granted = grant.scopes.map((s) => { + const sid = s.name.split(".")[0]; + const stream = (streams || []).find((x) => x.id === sid); + const proj = (grant.projections && grant.projections[sid]) || []; + const dropped = stream ? stream.fields.filter((f) => !proj.includes(f)) : []; + return { sid, s, proj, total: stream ? stream.fields.length : null, dropped }; + }); + const reads = (log || []).filter((e) => e.ref === grant.id && e.kind === "read"); return (
-
Select a grant to read your copy.
-
- ); - } - const revoked = grant.status === "revoked"; - const granted = grant.scopes.map((s) => { - const sid = s.name.split(".")[0]; - const stream = (streams || []).find((x) => x.id === sid); - const proj = (grant.projections && grant.projections[sid]) || []; - const dropped = stream ? stream.fields.filter((f) => !proj.includes(f)) : []; - return { sid, s, proj, total: stream ? stream.fields.length : null, dropped }; - }); - const reads = (log || []).filter((e) => e.ref === grant.id && e.kind === "read"); - return ( -
-
-
-
-

- - {grant.client} - - -

- {grant.id} -
-
-
-
- status - -
-
- purpose - {fmtToken(grant.purpose)} -
-
- {revoked ? "revoked" : "expires"} - {revoked ? grant.revokedFull : grant.expiresFull} -
+
+
+
+

+ + {grant.client} + + +

+ {grant.id}
- -
What {grant.client.split(" ")[0]} can assemble
- {granted.map(({ sid, s, proj, total }) => ( -
- {STREAM_HUMAN[sid] || sid} - {proj.length}{total ? ` of ${total}` : ""} fields cross · {s.terms} - {proj.join(" · ")} +
+
+
+ status + + + +
+
+ purpose + {fmtToken(grant.purpose)} +
+
+ {revoked ? "revoked" : "expires"} + {revoked ? grant.revokedFull : grant.expiresFull} +
- ))} - {(granted.some((g) => g.dropped.length > 0) || grant.declined.length > 0) && ( -
What stays yours
- )} - {granted.filter((g) => g.dropped.length > 0).map(({ sid, dropped }) => ( -
- {dropped.join(" · ")} - projected out of {sid} — never crosses -
- ))} - {grant.declined.map((d) => ( -
- {d} - declined by you at consent -
- ))} +
What {grant.client.split(" ")[0]} can assemble
+ {granted.map(({ sid, s, proj, total }) => ( +
+ {STREAM_HUMAN[sid] || sid} + + {proj.length} + {total ? ` of ${total}` : ""} fields cross · {s.terms} + + {proj.join(" · ")} +
+ ))} - {reads.length > 0 && ( -
{reads.length} read{reads.length === 1 ? "" : "s"} on record · last {reads[0].t.slice(5)}
- )} -
-
- {!revoking && Carbon — your copy stays here} - {!revoked && !revoking && ( - - )} - {!revoked && revoking && ( - - -
+
+ {!revoking && Carbon — your copy stays here} + {!revoked && !revoking && ( + - - )} - {revoked && ( - - struck, not erased - - )} + )} + {!revoked && revoking && ( + + + + + )} + {revoked && ( + + struck, not erased + + )} +
-
- ); -} + ); + } -/* ─── Streams view ─── */ + /* ─── Streams view ─── */ -function StreamSheet({ stream, grants }) { - const [lens, setLens] = useState(null); // grant id or null - const lensGrant = lens ? grants.find((g) => g.id === lens) : null; - const projected = lensGrant ? lensGrant.projections[stream.id] : null; - const granted = grants.filter((g) => g.status !== "revoked" && g.projections[stream.id]); - return ( -
-
-

{stream.id}

- - {granted.map((g) => ( - - ))} - {stream.connector} · {stream.records} records - -
-
- {stream.fields.map((f) => { - const dropped = projected && !projected.includes(f); - return ( - - {f} + function StreamSheet({ stream, grants }) { + const [lens, setLens] = useState(null); // grant id or null + const lensGrant = lens ? grants.find((g) => g.id === lens) : null; + const projected = lensGrant ? lensGrant.projections[stream.id] : null; + const granted = grants.filter((g) => g.status !== "revoked" && g.projections[stream.id]); + return ( +
+
+

{stream.id}

+ + {granted.map((g) => ( + + ))} + + {stream.connector} · {stream.records} records - ); - })} -
- {projected && ( -
- - {projected.length} of {stream.fields.length} fields cross · projection enforced at the server
- )} -
- ); -} +
+ {stream.fields.map((f) => { + const dropped = projected && !projected.includes(f); + return ( + + {f} + + ); + })} +
+ {projected && ( +
+ + {projected.length} of {stream.fields.length} fields cross · projection enforced at the server + +
+ )} +
+ ); + } -/* ─── Activity view ─── */ + /* ─── Activity view ─── */ -function ActivityLog({ entries }) { - return ( -
- {entries.map((e, i) => ( -
- {e.t} - {e.verb} - {e.what} - {e.ref} -
- ))} -
- ); -} + function ActivityLog({ entries }) { + return ( +
+ {entries.map((e, i) => ( +
+ {e.t} + {e.verb} + {e.what} + {e.ref} +
+ ))} +
+ ); + } -/* ─── The consent ceremony ─── */ + /* ─── The consent ceremony ─── */ -function Ceremony({ request, pressing, onToggle, onApprove, onRefuse, onDismiss }) { - useEffect(() => { - function onKey(e) { - if (e.key === "Escape") onDismiss(); - } - window.addEventListener("keydown", onKey); - return () => window.removeEventListener("keydown", onKey); - }, [onDismiss]); - const allowed = request.scopes.filter((s) => s.allowed); - return ( -
-
e.stopPropagation()}> -
-
-
- Access request · staged - {request.id} -
-
-

{request.client} asks to read {request.scopes.length} streams

-

- Purpose: {request.purposeHuman}. Decide stream by stream — anything you decline - stays on the record as declined. -

-
- {request.scopes.map((s, i) => ( -
- {s.name} - {s.terms} - - {s.desc} -
- ))} + function Ceremony({ request, pressing, onToggle, onApprove, onRefuse, onDismiss }) { + useEffect(() => { + function onKey(e) { + if (e.key === "Escape") onDismiss(); + } + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }, [onDismiss]); + const allowed = request.scopes.filter((s) => s.allowed); + return ( +
+
e.stopPropagation()} + > +
+
+
+ Access request · staged + {request.id} +
+
+

+ {request.client} asks to read {request.scopes.length} streams +

+

+ Purpose: {request.purposeHuman}. Decide stream by stream — anything you decline stays on the record as + declined. +

+
+ {request.scopes.map((s, i) => ( +
+ {s.name} + {s.terms} + + {s.desc} +
+ ))} +
+
+
+ {!pressing && ( + + )} + {!pressing && ( + + )} + {pressing && ( + Carbon pressed — your copy stays here + )} + {pressing && ( + + recording grant… + + )}
-
-
- {!pressing && ( - - )} - {!pressing && ( - - )} - {pressing && ( - Carbon pressed — your copy stays here - )} - {pressing && ( - - recording grant… - - )}
-
- ); -} + ); + } -Object.assign(window, { - RREndorse: Endorse, - RRSidebar: Sidebar, - RRSidebarFull: SidebarFull, - RRGrantRow: GrantRow, - RRInspector: Inspector, - RRStreamSheet: StreamSheet, - RRActivityLog: ActivityLog, - RRCeremony: Ceremony, -}); + Object.assign(window, { + RREndorse: Endorse, + RRSidebar: Sidebar, + RRSidebarFull: SidebarFull, + RRGrantRow: GrantRow, + RRInspector: Inspector, + RRStreamSheet: StreamSheet, + RRActivityLog: ActivityLog, + RRCeremony: Ceremony, + }); })(); diff --git a/docs/design-system/ink-carbon/project/recordroom/rr-data.js b/docs/design-system/ink-carbon/project/recordroom/rr-data.js index 8e70cb58a..cc1bc89ab 100644 --- a/docs/design-system/ink-carbon/project/recordroom/rr-data.js +++ b/docs/design-system/ink-carbon/project/recordroom/rr-data.js @@ -4,70 +4,244 @@ /* RECORDROOM — fixture data for the full console surface (window.RR2). */ window.RR2 = { traces: [ - { id: "trc_9f2k0419", t: "2026-06-11 07:58:12Z", client: "Longview Planning", grant: "grt_lngvw_01", stream: "pay_statements", op: "records.query", records: 12, fields: "5/8", dur: "42 ms", decision: "allow", + { + id: "trc_9f2k0419", + t: "2026-06-11 07:58:12Z", + client: "Longview Planning", + grant: "grt_lngvw_01", + stream: "pay_statements", + op: "records.query", + records: 12, + fields: "5/8", + dur: "42 ms", + decision: "allow", steps: [ ["token", "DPoP verified · client longview_planning_v1"], ["grant", "grt_lngvw_01 active · purpose long_term_financial_planning"], ["projection", "8 fields requested → 5 allowed by grant"], ["response", "12 records · append-only cursor advanced · 42 ms"], - ] }, - { id: "trc_3km1180a", t: "2026-06-11 06:02:44Z", client: "Concert Recommendations", grant: "grt_cncrt_02", stream: "listening_history", op: "records.query", records: 214, fields: "3/5", dur: "61 ms", decision: "allow", + ], + }, + { + id: "trc_3km1180a", + t: "2026-06-11 06:02:44Z", + client: "Concert Recommendations", + grant: "grt_cncrt_02", + stream: "listening_history", + op: "records.query", + records: 214, + fields: "3/5", + dur: "61 ms", + decision: "allow", steps: [ ["token", "DPoP verified · client concert_recs_v2"], ["grant", "grt_cncrt_02 continuous · rolling 12 mo window"], ["projection", "5 fields requested → 3 allowed by grant"], ["response", "214 records · 61 ms"], - ] }, - { id: "trc_77fe2c01", t: "2026-06-10 22:17:09Z", client: "Longview Planning", grant: "grt_lngvw_01", stream: "employment", op: "records.query", records: 4, fields: "3/5", dur: "12 ms", decision: "allow", + ], + }, + { + id: "trc_77fe2c01", + t: "2026-06-10 22:17:09Z", + client: "Longview Planning", + grant: "grt_lngvw_01", + stream: "employment", + op: "records.query", + records: 4, + fields: "3/5", + dur: "12 ms", + decision: "allow", steps: [ ["token", "DPoP verified · client longview_planning_v1"], ["grant", "grt_lngvw_01 active · purpose long_term_financial_planning"], ["projection", "5 fields requested → 3 allowed by grant"], ["response", "4 records · 12 ms"], - ] }, - { id: "trc_b2d90377", t: "2026-06-10 22:17:08Z", client: "Longview Planning", grant: "grt_lngvw_01", stream: "tax_docs", op: "records.query", records: 0, fields: "—", dur: "4 ms", decision: "deny", reason: "scope not granted", + ], + }, + { + id: "trc_b2d90377", + t: "2026-06-10 22:17:08Z", + client: "Longview Planning", + grant: "grt_lngvw_01", + stream: "tax_docs", + op: "records.query", + records: 0, + fields: "—", + dur: "4 ms", + decision: "deny", + reason: "scope not granted", steps: [ ["token", "DPoP verified · client longview_planning_v1"], ["grant", "tax_docs.read not in grant — declined by owner at consent"], ["deny", "request refused · nothing crossed · 4 ms"], - ] }, - { id: "trc_8d114b2e", t: "2026-05-02 14:39:51Z", client: "Crosswise Ads", grant: "grt_xwise_09", stream: "transactions", op: "records.query", records: 0, fields: "—", dur: "6 ms", decision: "deny", reason: "grant revoked", + ], + }, + { + id: "trc_8d114b2e", + t: "2026-05-02 14:39:51Z", + client: "Crosswise Ads", + grant: "grt_xwise_09", + stream: "transactions", + op: "records.query", + records: 0, + fields: "—", + dur: "6 ms", + decision: "deny", + reason: "grant revoked", steps: [ ["token", "DPoP verified · client crosswise_ads_v1"], ["grant", "grt_xwise_09 revoked 2026-05-02 14:40Z · by owner"], ["deny", "request refused · revocation authoritative at server · 6 ms"], - ] }, + ], + }, ], runs: [ - { id: "run_a6e201", connector: "Northstar HR", stream: "pay_statements", started: "2026-06-11 06:00Z", dur: "18 s", upserts: 2, cursor: "→ 2026-06-01", status: "ok" }, - { id: "run_a6e1f4", connector: "Tonal", stream: "listening_history", started: "2026-06-11 05:45Z", dur: "7 s", upserts: 96, cursor: "→ 05:45Z", status: "ok" }, - { id: "run_a6df02", connector: "First Meridian", stream: "transactions", started: "2026-06-11 05:00Z", dur: "31 s", upserts: 41, cursor: "→ 06-10", status: "ok" }, - { id: "run_a6d0c8", connector: "First Meridian", stream: "transactions", started: "2026-06-10 05:00Z", dur: "2 s", upserts: 0, cursor: "held", status: "failed", note: "OFX session expired — reauthorize connector" }, - { id: "run_a6cf11", connector: "Northstar HR", stream: "employment", started: "2026-06-10 06:00Z", dur: "4 s", upserts: 0, cursor: "—", status: "ok" }, + { + id: "run_a6e201", + connector: "Northstar HR", + stream: "pay_statements", + started: "2026-06-11 06:00Z", + dur: "18 s", + upserts: 2, + cursor: "→ 2026-06-01", + status: "ok", + }, + { + id: "run_a6e1f4", + connector: "Tonal", + stream: "listening_history", + started: "2026-06-11 05:45Z", + dur: "7 s", + upserts: 96, + cursor: "→ 05:45Z", + status: "ok", + }, + { + id: "run_a6df02", + connector: "First Meridian", + stream: "transactions", + started: "2026-06-11 05:00Z", + dur: "31 s", + upserts: 41, + cursor: "→ 06-10", + status: "ok", + }, + { + id: "run_a6d0c8", + connector: "First Meridian", + stream: "transactions", + started: "2026-06-10 05:00Z", + dur: "2 s", + upserts: 0, + cursor: "held", + status: "failed", + note: "OFX session expired — reauthorize connector", + }, + { + id: "run_a6cf11", + connector: "Northstar HR", + stream: "employment", + started: "2026-06-10 06:00Z", + dur: "4 s", + upserts: 0, + cursor: "—", + status: "ok", + }, ], schedules: [ - { connector: "Northstar HR", stream: "pay_statements", cadence: "daily · 06:00Z", next: "2026-06-12 06:00Z", last: "ok" }, - { connector: "Northstar HR", stream: "employment", cadence: "daily · 06:00Z", next: "2026-06-12 06:00Z", last: "ok" }, - { connector: "First Meridian", stream: "transactions", cadence: "daily · 05:00Z", next: "2026-06-12 05:00Z", last: "failed" }, + { + connector: "Northstar HR", + stream: "pay_statements", + cadence: "daily · 06:00Z", + next: "2026-06-12 06:00Z", + last: "ok", + }, + { + connector: "Northstar HR", + stream: "employment", + cadence: "daily · 06:00Z", + next: "2026-06-12 06:00Z", + last: "ok", + }, + { + connector: "First Meridian", + stream: "transactions", + cadence: "daily · 05:00Z", + next: "2026-06-12 05:00Z", + last: "failed", + }, { connector: "Tonal", stream: "listening_history", cadence: "every 15 min", next: "06:00Z", last: "ok" }, { connector: "Northstar HR", stream: "tax_docs", cadence: "yearly · Feb 01", next: "2027-02-01", last: "ok" }, ], sources: [ - { name: "Northstar HR", kind: "employer payroll", streams: "pay_statements · employment · tax_docs", auth: "service token", authOk: true, last: "2026-06-11 06:00Z" }, - { name: "First Meridian", kind: "bank · OFX", streams: "transactions", auth: "session expired", authOk: false, last: "2026-06-11 05:00Z" }, - { name: "Tonal", kind: "music service", streams: "listening_history", auth: "oauth refresh", authOk: true, last: "2026-06-11 05:45Z" }, + { + name: "Northstar HR", + kind: "employer payroll", + streams: "pay_statements · employment · tax_docs", + auth: "service token", + authOk: true, + last: "2026-06-11 06:00Z", + }, + { + name: "First Meridian", + kind: "bank · OFX", + streams: "transactions", + auth: "session expired", + authOk: false, + last: "2026-06-11 05:00Z", + }, + { + name: "Tonal", + kind: "music service", + streams: "listening_history", + auth: "oauth refresh", + authOk: true, + last: "2026-06-11 05:45Z", + }, ], feed: [ - { t: "06-11 06:00Z", stream: "pay_statements", body: "Acme Co · 2026-05 · gross $5,210.00 · net $3,508.12", id: "rec_ps_0312" }, + { + t: "06-11 06:00Z", + stream: "pay_statements", + body: "Acme Co · 2026-05 · gross $5,210.00 · net $3,508.12", + id: "rec_ps_0312", + }, { t: "06-11 05:45Z", stream: "listening_history", body: "96 plays upserted · Tonal", id: "rec_lh_6597" }, - { t: "06-11 05:00Z", stream: "transactions", body: "41 records · First Meridian · 06-10 cursor", id: "rec_tx_41203" }, - { t: "06-10 06:00Z", stream: "pay_statements", body: "Acme Co · 2026-05 · correction · taxes_withheld", id: "rec_ps_0311" }, + { + t: "06-11 05:00Z", + stream: "transactions", + body: "41 records · First Meridian · 06-10 cursor", + id: "rec_tx_41203", + }, + { + t: "06-10 06:00Z", + stream: "pay_statements", + body: "Acme Co · 2026-05 · correction · taxes_withheld", + id: "rec_ps_0311", + }, { t: "06-09 11:38Z", stream: "tax_docs", body: "W-2 · 2025 · Acme Co · document_blob 218 KB", id: "rec_td_0012" }, - { t: "06-08 06:00Z", stream: "employment", body: "Acme Co · senior analyst · 2023-04 → present", id: "rec_em_0004" }, + { + t: "06-08 06:00Z", + stream: "employment", + body: "Acme Co · senior analyst · 2023-04 → present", + id: "rec_em_0004", + }, ], apps: [ - { name: "Claude Desktop", via: "MCP · stdio bridge", status: "connected", detail: "reads via grants only · no owner token", added: "2026-03-02" }, - { name: "Cursor", via: "MCP · device code", status: "pending", detail: "code KZT-44Q · expires in 9 min", added: "—" }, + { + name: "Claude Desktop", + via: "MCP · stdio bridge", + status: "connected", + detail: "reads via grants only · no owner token", + added: "2026-03-02", + }, + { + name: "Cursor", + via: "MCP · device code", + status: "pending", + detail: "code KZT-44Q · expires in 9 min", + added: "—", + }, ], checks: [ { name: "AS discovery", detail: "/.well-known/oauth-authorization-server · 200", ok: true }, @@ -77,9 +251,7 @@ window.RR2 = { { name: "Version", detail: "pdpp 0.1.0 · reference implementation · current", ok: true }, { name: "Backups", detail: "no snapshot target configured", ok: false }, ], - tokens: [ - { id: "tok_owner_01", label: "CLI on framework", created: "2026-04-18", last: "2026-06-10 21:12Z" }, - ], + tokens: [{ id: "tok_owner_01", label: "CLI on framework", created: "2026-04-18", last: "2026-06-10 21:12Z" }], exporters: [ { device: "iPhone 15 · Photos exporter", last: "2026-06-10 21:12Z", records: "1,204", status: "ok" }, { device: "MacBook · Browser history", last: "paused by owner", records: "—", status: "paused" }, diff --git a/docs/design-system/ink-carbon/project/recordroom/rr-explore-data.js b/docs/design-system/ink-carbon/project/recordroom/rr-explore-data.js index fd2d174b6..60d88508b 100644 --- a/docs/design-system/ink-carbon/project/recordroom/rr-explore-data.js +++ b/docs/design-system/ink-carbon/project/recordroom/rr-explore-data.js @@ -17,85 +17,183 @@ window.RRX = { spark: [2, 1, 3, 2, 1, 2, 4, 3, 2, 1, 2, 3, 5, 4, 3, 2, 4, 6, 5, 7, 6, 8, 7, 9, 8, 10, 12, 9, 11, 16], connections: [ - { id: "cdx", name: "Codex CLI — laptop", kind: "codex", status: "active", - cin: "cin_cdx_91ad02", account: "local agent · stdio bridge", config: "full session capture", - added: "2026-02-11", auth: "device-bound key", schedule: "live", lastRun: "streaming", + { + id: "cdx", + name: "Codex CLI — laptop", + kind: "codex", + status: "active", + cin: "cin_cdx_91ad02", + account: "local agent · stdio bridge", + config: "full session capture", + added: "2026-02-11", + auth: "device-bound key", + schedule: "live", + lastRun: "streaming", streams: [ { name: "sessions", records: "1,204", cursor: "live", searchable: true, last: "7 min ago" }, { name: "messages", records: "18,440", cursor: "live", searchable: true, last: "22 min ago" }, { name: "function_calls", records: "22,901", cursor: "live", searchable: false, last: "22 min ago" }, - ] }, - { id: "cc1", name: "Claude Code — MacBook", kind: "claude-code", status: "active", - cin: "cin_cc_77b210", account: "local agent · MacBook", config: "sessions + messages + attachments", - added: "2026-01-08", auth: "device-bound key", schedule: "live", lastRun: "streaming", + ], + }, + { + id: "cc1", + name: "Claude Code — MacBook", + kind: "claude-code", + status: "active", + cin: "cin_cc_77b210", + account: "local agent · MacBook", + config: "sessions + messages + attachments", + added: "2026-01-08", + auth: "device-bound key", + schedule: "live", + lastRun: "streaming", streams: [ { name: "sessions", records: "312", cursor: "live", searchable: true, last: "20 h ago" }, { name: "messages", records: "4,180", cursor: "live", searchable: true, last: "20 h ago" }, { name: "attachments", records: "96", cursor: "live", searchable: false, last: "3 d ago" }, { name: "skills", records: "14", cursor: "live", searchable: true, last: "9 d ago" }, - ] }, - { id: "cc2", name: "Claude Code — build VM", kind: "claude-code", status: "revoked", - cin: "cin_cc_2a96ef", account: "local agent · build VM", config: "sessions + messages", - added: "2026-03-02", auth: "revoked May 28", schedule: "—", lastRun: "refused (revoked)", + ], + }, + { + id: "cc2", + name: "Claude Code — build VM", + kind: "claude-code", + status: "revoked", + cin: "cin_cc_2a96ef", + account: "local agent · build VM", + config: "sessions + messages", + added: "2026-03-02", + auth: "revoked May 28", + schedule: "—", + lastRun: "refused (revoked)", streams: [ { name: "sessions", records: "88", cursor: "frozen May 28", searchable: true, last: "May 28" }, { name: "messages", records: "1,402", cursor: "frozen May 28", searchable: true, last: "May 28" }, { name: "attachments", records: "12", cursor: "frozen May 28", searchable: false, last: "May 24" }, { name: "skills", records: "6", cursor: "frozen May 28", searchable: true, last: "May 12" }, - ] }, - { id: "gm1", name: "Gmail — personal", kind: "gmail", status: "active", - cin: "cin_gm_410c2b", account: "m.okafor@gmail.example", config: "all mail · bodies + attachments", - added: "2025-11-20", auth: "oauth refresh ok", schedule: "every 15 min", lastRun: "ok · 31 min ago", + ], + }, + { + id: "gm1", + name: "Gmail — personal", + kind: "gmail", + status: "active", + cin: "cin_gm_410c2b", + account: "m.okafor@gmail.example", + config: "all mail · bodies + attachments", + added: "2025-11-20", + auth: "oauth refresh ok", + schedule: "every 15 min", + lastRun: "ok · 31 min ago", streams: [ { name: "messages", records: "31,007", cursor: "→ 14:28Z", searchable: true, last: "31 min ago" }, { name: "threads", records: "12,884", cursor: "→ 14:28Z", searchable: true, last: "31 min ago" }, { name: "message_bodies", records: "30,910", cursor: "→ 14:28Z", searchable: true, last: "31 min ago" }, { name: "attachments", records: "2,114", cursor: "→ 13:02Z", searchable: false, last: "2 h ago" }, - ] }, - { id: "gm2", name: "Gmail — work (filtered)", kind: "gmail", status: "active", - cin: "cin_gm_88e1f0", account: "m.okafor@acme.example", config: "label:receipts + label:travel only", - added: "2026-04-02", auth: "oauth refresh ok", schedule: "hourly", lastRun: "ok · 50 min ago", + ], + }, + { + id: "gm2", + name: "Gmail — work (filtered)", + kind: "gmail", + status: "active", + cin: "cin_gm_88e1f0", + account: "m.okafor@acme.example", + config: "label:receipts + label:travel only", + added: "2026-04-02", + auth: "oauth refresh ok", + schedule: "hourly", + lastRun: "ok · 50 min ago", streams: [ { name: "messages", records: "1,201", cursor: "→ 14:10Z", searchable: true, last: "20 h ago" }, { name: "threads", records: "884", cursor: "→ 14:10Z", searchable: true, last: "20 h ago" }, { name: "attachments", records: "310", cursor: "→ 14:10Z", searchable: false, last: "20 h ago" }, - ] }, - { id: "gh", name: "GitHub — mokafor", kind: "github", status: "active", - cin: "cin_gh_53aa19", account: "github.com/mokafor", config: "repos + daily stats", - added: "2026-01-15", auth: "fine-grained token ok", schedule: "hourly", lastRun: "ok · 23 min ago", + ], + }, + { + id: "gh", + name: "GitHub — mokafor", + kind: "github", + status: "active", + cin: "cin_gh_53aa19", + account: "github.com/mokafor", + config: "repos + daily stats", + added: "2026-01-15", + auth: "fine-grained token ok", + schedule: "hourly", + lastRun: "ok · 23 min ago", streams: [ { name: "repositories", records: "41", cursor: "→ 14:40Z", searchable: true, last: "23 min ago" }, { name: "user_stats", records: "365", cursor: "→ today", searchable: false, last: "15 h ago" }, - ] }, - { id: "cg", name: "ChatGPT — personal", kind: "chatgpt", status: "active", - cin: "cin_cg_b042d8", account: "everyone@okafor.example", config: "conversations + messages", - added: "2026-02-28", auth: "session export ok", schedule: "daily · 06:00Z", lastRun: "ok · 9 h ago", + ], + }, + { + id: "cg", + name: "ChatGPT — personal", + kind: "chatgpt", + status: "active", + cin: "cin_cg_b042d8", + account: "everyone@okafor.example", + config: "conversations + messages", + added: "2026-02-28", + auth: "session export ok", + schedule: "daily · 06:00Z", + lastRun: "ok · 9 h ago", streams: [ { name: "conversations", records: "871", cursor: "→ 06-12", searchable: true, last: "2 h ago" }, { name: "messages", records: "12,030", cursor: "→ 06-12", searchable: true, last: "2 h ago" }, - ] }, - { id: "fm", name: "First Meridian — checking", kind: "bank", status: "reauth", - cin: "cin_fm_206b11", account: "checking ····4417", config: "transactions + statements + balances", - added: "2025-12-01", auth: "OFX session expired", schedule: "daily · 05:00Z", lastRun: "failed · 06-11", + ], + }, + { + id: "fm", + name: "First Meridian — checking", + kind: "bank", + status: "reauth", + cin: "cin_fm_206b11", + account: "checking ····4417", + config: "transactions + statements + balances", + added: "2025-12-01", + auth: "OFX session expired", + schedule: "daily · 05:00Z", + lastRun: "failed · 06-11", streams: [ { name: "transactions", records: "41,203", cursor: "held at 06-10", searchable: true, last: "2 d ago" }, { name: "statements", records: "72", cursor: "→ May", searchable: false, last: "12 d ago" }, { name: "balances", records: "365", cursor: "held at 06-10", searchable: false, last: "2 d ago" }, - ] }, - { id: "nh", name: "Northstar HR", kind: "payroll", status: "active", - cin: "cin_nh_e3391c", account: "employee 41880 · Acme Co", config: "pay + employment + tax docs", - added: "2025-10-02", auth: "service token ok", schedule: "with payroll", lastRun: "ok · 33 min ago", + ], + }, + { + id: "nh", + name: "Northstar HR", + kind: "payroll", + status: "active", + cin: "cin_nh_e3391c", + account: "employee 41880 · Acme Co", + config: "pay + employment + tax docs", + added: "2025-10-02", + auth: "service token ok", + schedule: "with payroll", + lastRun: "ok · 33 min ago", streams: [ { name: "pay_statements", records: "313", cursor: "→ Jun 12", searchable: true, last: "33 min ago" }, { name: "employment", records: "4", cursor: "→ current", searchable: true, last: "4 d ago" }, { name: "tax_docs", records: "12", cursor: "→ 2025", searchable: false, last: "Feb 02" }, - ] }, - { id: "tn", name: "Tonal", kind: "music", status: "active", - cin: "cin_tn_77f024", account: "m.okafor", config: "listening history", - added: "2026-01-30", auth: "oauth refresh ok", schedule: "every 15 min", lastRun: "ok · 12 min ago", - streams: [ - { name: "listening_history", records: "6,597", cursor: "→ 05:41Z", searchable: true, last: "9 h ago" }, - ] }, + ], + }, + { + id: "tn", + name: "Tonal", + kind: "music", + status: "active", + cin: "cin_tn_77f024", + account: "m.okafor", + config: "listening history", + added: "2026-01-30", + auth: "oauth refresh ok", + schedule: "every 15 min", + lastRun: "ok · 12 min ago", + streams: [{ name: "listening_history", records: "6,597", cursor: "→ 05:41Z", searchable: true, last: "9 h ago" }], + }, ], partial: { @@ -107,106 +205,487 @@ window.RRX = { records: [ /* ── Fri, Jun 12 ── */ - { id: "rec_cdx_s_0911", con: "cdx", stream: "sessions", day: "2026-06-12", rel: "7 min ago", + { + id: "rec_cdx_s_0911", + con: "cdx", + stream: "sessions", + day: "2026-06-12", + rel: "7 min ago", title: "Pick up yesterday's auth review thread and summarize the open items…", - fields: [["prompt", "Pick up yesterday's auth review thread and summarize the open items…"], ["model", "gpt-5.5"], ["turns", "31"], ["started", "2026-06-12 14:48Z"]] }, - { id: "rec_cdx_s_0910", con: "cdx", stream: "sessions", day: "2026-06-12", rel: "22 min ago", + fields: [ + ["prompt", "Pick up yesterday's auth review thread and summarize the open items…"], + ["model", "gpt-5.5"], + ["turns", "31"], + ["started", "2026-06-12 14:48Z"], + ], + }, + { + id: "rec_cdx_s_0910", + con: "cdx", + stream: "sessions", + day: "2026-06-12", + rel: "22 min ago", title: "Bounded task: implement admin management for the waitlist service…", - links: [["messages in session", "rec_cdx_m_4410"], ["tool calls", "rec_cdx_f_fold1"]], - fields: [["prompt", "Bounded task: implement admin management for the waitlist service…"], ["model", "gpt-5.5 · low"], ["turns", "58"], ["started", "2026-06-12 14:21Z"]] }, - { id: "rec_cdx_m_4410", con: "cdx", stream: "messages", day: "2026-06-12", rel: "22 min ago", - role: "assistant", title: "All checks are green — build, tests, typecheck. PR is ready for review.", + links: [ + ["messages in session", "rec_cdx_m_4410"], + ["tool calls", "rec_cdx_f_fold1"], + ], + fields: [ + ["prompt", "Bounded task: implement admin management for the waitlist service…"], + ["model", "gpt-5.5 · low"], + ["turns", "58"], + ["started", "2026-06-12 14:21Z"], + ], + }, + { + id: "rec_cdx_m_4410", + con: "cdx", + stream: "messages", + day: "2026-06-12", + rel: "22 min ago", + role: "assistant", + title: "All checks are green — build, tests, typecheck. PR is ready for review.", links: [["session", "rec_cdx_s_0910"]], - fields: [["role", "assistant"], ["session", "ses_a41"], ["chars", "212"], ["content", "All checks are green — build, tests, typecheck. PR is ready for review."]] }, - { id: "rec_cdx_m_4409", con: "cdx", stream: "messages", day: "2026-06-12", rel: "22 min ago", - role: "developer", degraded: true, title: "no text — tool turn", + fields: [ + ["role", "assistant"], + ["session", "ses_a41"], + ["chars", "212"], + ["content", "All checks are green — build, tests, typecheck. PR is ready for review."], + ], + }, + { + id: "rec_cdx_m_4409", + con: "cdx", + stream: "messages", + day: "2026-06-12", + rel: "22 min ago", + role: "developer", + degraded: true, + title: "no text — tool turn", links: [["session", "rec_cdx_s_0910"]], - fields: [["role", "developer"], ["session", "ses_a41"], ["content", "— (tool output only)"]] }, - { id: "rec_cdx_f_fold1", con: "cdx", stream: "function_calls", day: "2026-06-12", rel: "22 min ago", - fold: 9, title: "9 tool calls in 4 minutes", snippet: "exec_command ×6 · write_stdin ×3", + fields: [ + ["role", "developer"], + ["session", "ses_a41"], + ["content", "— (tool output only)"], + ], + }, + { + id: "rec_cdx_f_fold1", + con: "cdx", + stream: "function_calls", + day: "2026-06-12", + rel: "22 min ago", + fold: 9, + title: "9 tool calls in 4 minutes", + snippet: "exec_command ×6 · write_stdin ×3", links: [["session", "rec_cdx_s_0910"]], - fields: [["calls", "9"], ["span", "4 min"], ["breakdown", "exec_command ×6 · write_stdin ×3"], ["session", "ses_a41"]] }, - { id: "rec_gh_r_0182", con: "gh", stream: "repositories", day: "2026-06-12", rel: "23 min ago", title: "recordroom", - fields: [["repo", "mokafor/recordroom"], ["visibility", "private"], ["pushed", "2026-06-12 14:39Z"], ["open_prs", "2"]] }, - { id: "rec_gm1_t_2210", con: "gm1", stream: "threads", day: "2026-06-12", rel: "31 min ago", + fields: [ + ["calls", "9"], + ["span", "4 min"], + ["breakdown", "exec_command ×6 · write_stdin ×3"], + ["session", "ses_a41"], + ], + }, + { + id: "rec_gh_r_0182", + con: "gh", + stream: "repositories", + day: "2026-06-12", + rel: "23 min ago", + title: "recordroom", + fields: [ + ["repo", "mokafor/recordroom"], + ["visibility", "private"], + ["pushed", "2026-06-12 14:39Z"], + ["open_prs", "2"], + ], + }, + { + id: "rec_gm1_t_2210", + con: "gm1", + stream: "threads", + day: "2026-06-12", + rel: "31 min ago", title: "Your June 12 pay is on its way", - links: [["message", "rec_gm1_m_5121"], ["body", "rec_gm1_b_5121"]], - fields: [["subject", "Your June 12 pay is on its way"], ["messages", "1"], ["participants", "payroll@northstar-hr.example"]] }, - { id: "rec_gm1_m_5121", con: "gm1", stream: "messages", day: "2026-06-12", rel: "31 min ago", - degraded: true, title: "no subject", snippet: "received 14:28Z · from payroll@northstar-hr.example", - links: [["thread", "rec_gm1_t_2210"], ["body", "rec_gm1_b_5121"]], - fields: [["from", "payroll@northstar-hr.example"], ["received", "2026-06-12 14:28Z"], ["subject", "—"], ["size", "12 KB"]] }, - { id: "rec_gm1_b_5121", con: "gm1", stream: "message_bodies", day: "2026-06-12", rel: "31 min ago", - degraded: true, title: "message body · utf-8 · 38 KB", - links: [["message", "rec_gm1_m_5121"], ["thread", "rec_gm1_t_2210"]], - fields: [["charset", "utf-8"], ["bytes", "38,114"], ["message_ref", "rec_gm1_m_5121"], ["text", "Hi M. — your pay for the period ending Jun 12 has been issued…"]] }, - { id: "rec_ps_0313", con: "nh", stream: "pay_statements", day: "2026-06-12", rel: "33 min ago", - title: "Acme Co · Jun 2026", snippet: "net $3,508.12", - fields: [["employer", "Acme Co"], ["period_start", "2026-06-01"], ["period_end", "2026-06-12"], ["gross_pay", "$5,210.00"], ["net_pay", "$3,508.12"], ["taxes_withheld", "$1,214.38"], ["benefits_detail", "401k 5% · health PPO"], ["bank_routing", "checking ····4417"]] }, - { id: "rec_gh_r_0181", con: "gh", stream: "repositories", day: "2026-06-12", rel: "1 h ago", title: "pdpp-spec", - fields: [["repo", "mokafor/pdpp-spec"], ["visibility", "public"], ["pushed", "2026-06-12 14:02Z"], ["open_prs", "0"]] }, - { id: "rec_cg_c_0871", con: "cg", stream: "conversations", day: "2026-06-12", rel: "2 h ago", + links: [ + ["message", "rec_gm1_m_5121"], + ["body", "rec_gm1_b_5121"], + ], + fields: [ + ["subject", "Your June 12 pay is on its way"], + ["messages", "1"], + ["participants", "payroll@northstar-hr.example"], + ], + }, + { + id: "rec_gm1_m_5121", + con: "gm1", + stream: "messages", + day: "2026-06-12", + rel: "31 min ago", + degraded: true, + title: "no subject", + snippet: "received 14:28Z · from payroll@northstar-hr.example", + links: [ + ["thread", "rec_gm1_t_2210"], + ["body", "rec_gm1_b_5121"], + ], + fields: [ + ["from", "payroll@northstar-hr.example"], + ["received", "2026-06-12 14:28Z"], + ["subject", "—"], + ["size", "12 KB"], + ], + }, + { + id: "rec_gm1_b_5121", + con: "gm1", + stream: "message_bodies", + day: "2026-06-12", + rel: "31 min ago", + degraded: true, + title: "message body · utf-8 · 38 KB", + links: [ + ["message", "rec_gm1_m_5121"], + ["thread", "rec_gm1_t_2210"], + ], + fields: [ + ["charset", "utf-8"], + ["bytes", "38,114"], + ["message_ref", "rec_gm1_m_5121"], + ["text", "Hi M. — your pay for the period ending Jun 12 has been issued…"], + ], + }, + { + id: "rec_ps_0313", + con: "nh", + stream: "pay_statements", + day: "2026-06-12", + rel: "33 min ago", + title: "Acme Co · Jun 2026", + snippet: "net $3,508.12", + fields: [ + ["employer", "Acme Co"], + ["period_start", "2026-06-01"], + ["period_end", "2026-06-12"], + ["gross_pay", "$5,210.00"], + ["net_pay", "$3,508.12"], + ["taxes_withheld", "$1,214.38"], + ["benefits_detail", "401k 5% · health PPO"], + ["bank_routing", "checking ····4417"], + ], + }, + { + id: "rec_gh_r_0181", + con: "gh", + stream: "repositories", + day: "2026-06-12", + rel: "1 h ago", + title: "pdpp-spec", + fields: [ + ["repo", "mokafor/pdpp-spec"], + ["visibility", "public"], + ["pushed", "2026-06-12 14:02Z"], + ["open_prs", "0"], + ], + }, + { + id: "rec_cg_c_0871", + con: "cg", + stream: "conversations", + day: "2026-06-12", + rel: "2 h ago", title: "Summarize my May spending by category", - fields: [["title", "Summarize my May spending by category"], ["messages", "14"], ["model", "gpt-5.5"], ["started", "2026-06-12 12:40Z"]] }, - { id: "rec_cdx_s_fold2", con: "cdx", stream: "sessions", day: "2026-06-12", rel: "3–11 h ago", - fold: 4, title: "4 connector test pings", snippet: "\u201cReply with exactly: ok\u201d ×4", - fields: [["pings", "4"], ["span", "8 h"], ["prompt", "Reply with exactly: ok"]] }, - { id: "rec_lh_6597", con: "tn", stream: "listening_history", day: "2026-06-12", rel: "9 h ago", - title: "Hejira — Joni Mitchell", snippet: "kitchen speaker · 05:41Z", - fields: [["track", "Hejira"], ["artist", "Joni Mitchell"], ["played_at", "2026-06-12 05:41Z"], ["device", "kitchen speaker"], ["playlist_ref", "morning"]] }, - { id: "rec_lh_6596", con: "tn", stream: "listening_history", day: "2026-06-12", rel: "9 h ago", - title: "Pyramids — Frank Ocean", snippet: "kitchen speaker · 05:32Z", - fields: [["track", "Pyramids"], ["artist", "Frank Ocean"], ["played_at", "2026-06-12 05:32Z"], ["device", "kitchen speaker"], ["playlist_ref", "morning"]] }, - { id: "rec_gh_u_0044", con: "gh", stream: "user_stats", day: "2026-06-12", rel: "15 h ago", - degraded: true, title: "daily stats snapshot · 2026-06-12", - fields: [["date", "2026-06-12"], ["commits", "9"], ["prs_opened", "1"], ["reviews", "3"]] }, + fields: [ + ["title", "Summarize my May spending by category"], + ["messages", "14"], + ["model", "gpt-5.5"], + ["started", "2026-06-12 12:40Z"], + ], + }, + { + id: "rec_cdx_s_fold2", + con: "cdx", + stream: "sessions", + day: "2026-06-12", + rel: "3–11 h ago", + fold: 4, + title: "4 connector test pings", + snippet: "\u201cReply with exactly: ok\u201d ×4", + fields: [ + ["pings", "4"], + ["span", "8 h"], + ["prompt", "Reply with exactly: ok"], + ], + }, + { + id: "rec_lh_6597", + con: "tn", + stream: "listening_history", + day: "2026-06-12", + rel: "9 h ago", + title: "Hejira — Joni Mitchell", + snippet: "kitchen speaker · 05:41Z", + fields: [ + ["track", "Hejira"], + ["artist", "Joni Mitchell"], + ["played_at", "2026-06-12 05:41Z"], + ["device", "kitchen speaker"], + ["playlist_ref", "morning"], + ], + }, + { + id: "rec_lh_6596", + con: "tn", + stream: "listening_history", + day: "2026-06-12", + rel: "9 h ago", + title: "Pyramids — Frank Ocean", + snippet: "kitchen speaker · 05:32Z", + fields: [ + ["track", "Pyramids"], + ["artist", "Frank Ocean"], + ["played_at", "2026-06-12 05:32Z"], + ["device", "kitchen speaker"], + ["playlist_ref", "morning"], + ], + }, + { + id: "rec_gh_u_0044", + con: "gh", + stream: "user_stats", + day: "2026-06-12", + rel: "15 h ago", + degraded: true, + title: "daily stats snapshot · 2026-06-12", + fields: [ + ["date", "2026-06-12"], + ["commits", "9"], + ["prs_opened", "1"], + ["reviews", "3"], + ], + }, /* ── Thu, Jun 11 ── */ - { id: "rec_gm1_m_5108", con: "gm1", stream: "messages", day: "2026-06-11", rel: "16 h ago", - title: "New sign-in detected on your hosting account", snippet: "Chrome on macOS · Tacoma, WA", - fields: [["from", "security@hosting.example"], ["subject", "New sign-in detected on your hosting account"], ["received", "2026-06-11 23:09Z"], ["size", "9 KB"]] }, - { id: "rec_cc1_s_0392", con: "cc1", stream: "sessions", day: "2026-06-11", rel: "20 h ago", + { + id: "rec_gm1_m_5108", + con: "gm1", + stream: "messages", + day: "2026-06-11", + rel: "16 h ago", + title: "New sign-in detected on your hosting account", + snippet: "Chrome on macOS · Tacoma, WA", + fields: [ + ["from", "security@hosting.example"], + ["subject", "New sign-in detected on your hosting account"], + ["received", "2026-06-11 23:09Z"], + ["size", "9 KB"], + ], + }, + { + id: "rec_cc1_s_0392", + con: "cc1", + stream: "sessions", + day: "2026-06-11", + rel: "20 h ago", title: "Refactor the DS compiler's error messages for clarity…", links: [["messages in session", "rec_cc1_m_1840"]], - fields: [["prompt", "Refactor the DS compiler's error messages for clarity…"], ["model", "fable-4"], ["turns", "22"], ["started", "2026-06-11 19:02Z"]] }, - { id: "rec_cc1_m_1840", con: "cc1", stream: "messages", day: "2026-06-11", rel: "20 h ago", - role: "assistant", title: "Renamed the diagnostics — all 14 fixtures pass.", + fields: [ + ["prompt", "Refactor the DS compiler's error messages for clarity…"], + ["model", "fable-4"], + ["turns", "22"], + ["started", "2026-06-11 19:02Z"], + ], + }, + { + id: "rec_cc1_m_1840", + con: "cc1", + stream: "messages", + day: "2026-06-11", + rel: "20 h ago", + role: "assistant", + title: "Renamed the diagnostics — all 14 fixtures pass.", links: [["session", "rec_cc1_s_0392"]], - fields: [["role", "assistant"], ["session", "ses_c12"], ["chars", "148"], ["content", "Renamed the diagnostics — all 14 fixtures pass."]] }, - { id: "rec_gm2_m_7011", con: "gm2", stream: "messages", day: "2026-06-11", rel: "20 h ago", - title: "Receipt — storage unit autopay", snippet: "label:receipts · next payment 7/11", + fields: [ + ["role", "assistant"], + ["session", "ses_c12"], + ["chars", "148"], + ["content", "Renamed the diagnostics — all 14 fixtures pass."], + ], + }, + { + id: "rec_gm2_m_7011", + con: "gm2", + stream: "messages", + day: "2026-06-11", + rel: "20 h ago", + title: "Receipt — storage unit autopay", + snippet: "label:receipts · next payment 7/11", links: [["attachment", "rec_gm2_a_0310"]], - fields: [["from", "billing@extraspace.example"], ["subject", "Receipt — storage unit autopay"], ["label", "receipts"], ["received", "2026-06-11 19:40Z"], ["size", "640 KB"]] }, - { id: "rec_gm2_a_0310", con: "gm2", stream: "attachments", day: "2026-06-11", rel: "20 h ago", - image: true, title: "receipt-scan.jpg", snippet: "image/jpeg · 412 KB", + fields: [ + ["from", "billing@extraspace.example"], + ["subject", "Receipt — storage unit autopay"], + ["label", "receipts"], + ["received", "2026-06-11 19:40Z"], + ["size", "640 KB"], + ], + }, + { + id: "rec_gm2_a_0310", + con: "gm2", + stream: "attachments", + day: "2026-06-11", + rel: "20 h ago", + image: true, + title: "receipt-scan.jpg", + snippet: "image/jpeg · 412 KB", links: [["message", "rec_gm2_m_7011"]], - fields: [["filename", "receipt-scan.jpg"], ["content_type", "image/jpeg"], ["bytes", "412,381"], ["message_ref", "rec_gm2_m_7011"]] }, - { id: "rec_cg_m_3301", con: "cg", stream: "messages", day: "2026-06-11", rel: "20 h ago", - role: "system", degraded: true, title: "no text — system turn", - fields: [["role", "system"], ["conversation", "cnv_864"], ["content", "—"]] }, - { id: "rec_cg_c_0864", con: "cg", stream: "conversations", day: "2026-06-11", rel: "21 h ago", + fields: [ + ["filename", "receipt-scan.jpg"], + ["content_type", "image/jpeg"], + ["bytes", "412,381"], + ["message_ref", "rec_gm2_m_7011"], + ], + }, + { + id: "rec_cg_m_3301", + con: "cg", + stream: "messages", + day: "2026-06-11", + rel: "20 h ago", + role: "system", + degraded: true, + title: "no text — system turn", + fields: [ + ["role", "system"], + ["conversation", "cnv_864"], + ["content", "—"], + ], + }, + { + id: "rec_cg_c_0864", + con: "cg", + stream: "conversations", + day: "2026-06-11", + rel: "21 h ago", title: "Quick meatball recipes", - fields: [["title", "Quick meatball recipes"], ["messages", "6"], ["model", "gpt-5.5"], ["started", "2026-06-11 18:55Z"]] }, - { id: "rec_gh_r_0179", con: "gh", stream: "repositories", day: "2026-06-11", rel: "22 h ago", title: "ds-compiler", - fields: [["repo", "mokafor/ds-compiler"], ["visibility", "private"], ["pushed", "2026-06-11 17:20Z"], ["open_prs", "1"]] }, - { id: "rec_lh_6580", con: "tn", stream: "listening_history", day: "2026-06-11", rel: "1 d ago", - title: "Both Sides Now — Joni Mitchell", snippet: "living room · 22:14Z", - fields: [["track", "Both Sides Now"], ["artist", "Joni Mitchell"], ["played_at", "2026-06-11 22:14Z"], ["device", "living room"], ["playlist_ref", "—"]] }, + fields: [ + ["title", "Quick meatball recipes"], + ["messages", "6"], + ["model", "gpt-5.5"], + ["started", "2026-06-11 18:55Z"], + ], + }, + { + id: "rec_gh_r_0179", + con: "gh", + stream: "repositories", + day: "2026-06-11", + rel: "22 h ago", + title: "ds-compiler", + fields: [ + ["repo", "mokafor/ds-compiler"], + ["visibility", "private"], + ["pushed", "2026-06-11 17:20Z"], + ["open_prs", "1"], + ], + }, + { + id: "rec_lh_6580", + con: "tn", + stream: "listening_history", + day: "2026-06-11", + rel: "1 d ago", + title: "Both Sides Now — Joni Mitchell", + snippet: "living room · 22:14Z", + fields: [ + ["track", "Both Sides Now"], + ["artist", "Joni Mitchell"], + ["played_at", "2026-06-11 22:14Z"], + ["device", "living room"], + ["playlist_ref", "—"], + ], + }, /* ── Wed, Jun 10 ── */ - { id: "rec_tx_41203", con: "fm", stream: "transactions", day: "2026-06-10", rel: "2 d ago", - title: "Blue Bottle Coffee", snippet: "−$6.40 · coffee & cafes", - fields: [["date", "2026-06-10"], ["amount", "−$6.40"], ["merchant", "Blue Bottle Coffee"], ["category", "coffee & cafes"], ["account_ref", "checking ····4417"], ["memo", "card present"]] }, - { id: "rec_tx_41202", con: "fm", stream: "transactions", day: "2026-06-10", rel: "2 d ago", - title: "Rainier Grocery", snippet: "−$54.20 · groceries", - fields: [["date", "2026-06-10"], ["amount", "−$54.20"], ["merchant", "Rainier Grocery"], ["category", "groceries"], ["account_ref", "checking ····4417"], ["memo", "—"]] }, - { id: "rec_tx_41195", con: "fm", stream: "transactions", day: "2026-06-10", rel: "2 d ago", - title: "Hawthorne Property Mgmt", snippet: "−$1,850.00 · rent · june", - fields: [["date", "2026-06-10"], ["amount", "−$1,850.00"], ["merchant", "Hawthorne Property Mgmt"], ["category", "rent"], ["account_ref", "checking ····4417"], ["memo", "june"]] }, - { id: "rec_cc1_s_0388", con: "cc1", stream: "sessions", day: "2026-06-10", rel: "2 d ago", + { + id: "rec_tx_41203", + con: "fm", + stream: "transactions", + day: "2026-06-10", + rel: "2 d ago", + title: "Blue Bottle Coffee", + snippet: "−$6.40 · coffee & cafes", + fields: [ + ["date", "2026-06-10"], + ["amount", "−$6.40"], + ["merchant", "Blue Bottle Coffee"], + ["category", "coffee & cafes"], + ["account_ref", "checking ····4417"], + ["memo", "card present"], + ], + }, + { + id: "rec_tx_41202", + con: "fm", + stream: "transactions", + day: "2026-06-10", + rel: "2 d ago", + title: "Rainier Grocery", + snippet: "−$54.20 · groceries", + fields: [ + ["date", "2026-06-10"], + ["amount", "−$54.20"], + ["merchant", "Rainier Grocery"], + ["category", "groceries"], + ["account_ref", "checking ····4417"], + ["memo", "—"], + ], + }, + { + id: "rec_tx_41195", + con: "fm", + stream: "transactions", + day: "2026-06-10", + rel: "2 d ago", + title: "Hawthorne Property Mgmt", + snippet: "−$1,850.00 · rent · june", + fields: [ + ["date", "2026-06-10"], + ["amount", "−$1,850.00"], + ["merchant", "Hawthorne Property Mgmt"], + ["category", "rent"], + ["account_ref", "checking ····4417"], + ["memo", "june"], + ], + }, + { + id: "rec_cc1_s_0388", + con: "cc1", + stream: "sessions", + day: "2026-06-10", + rel: "2 d ago", title: "Sketch the consent ceremony copy variants…", - fields: [["prompt", "Sketch the consent ceremony copy variants…"], ["model", "fable-4"], ["turns", "9"], ["started", "2026-06-10 16:11Z"]] }, - { id: "rec_gh_r_0175", con: "gh", stream: "repositories", day: "2026-06-10", rel: "2 d ago", title: "pdpp-explorer", - fields: [["repo", "mokafor/pdpp-explorer"], ["visibility", "public"], ["pushed", "2026-06-10 15:44Z"], ["open_prs", "0"]] }, + fields: [ + ["prompt", "Sketch the consent ceremony copy variants…"], + ["model", "fable-4"], + ["turns", "9"], + ["started", "2026-06-10 16:11Z"], + ], + }, + { + id: "rec_gh_r_0175", + con: "gh", + stream: "repositories", + day: "2026-06-10", + rel: "2 d ago", + title: "pdpp-explorer", + fields: [ + ["repo", "mokafor/pdpp-explorer"], + ["visibility", "public"], + ["pushed", "2026-06-10 15:44Z"], + ["open_prs", "0"], + ], + }, ], }; diff --git a/docs/design-system/ink-carbon/project/recordroom/rr-explore.jsx b/docs/design-system/ink-carbon/project/recordroom/rr-explore.jsx index 34a5b5a0e..c092954e3 100644 --- a/docs/design-system/ink-carbon/project/recordroom/rr-explore.jsx +++ b/docs/design-system/ink-carbon/project/recordroom/rr-explore.jsx @@ -5,435 +5,580 @@ product. Owner-grade query power (typed operators, machine-parity query line), instance-true facets, full fields always, relationships, images. Sources is the loading dock; it links INTO this view, never duplicates it. */ -;(() => { +(() => { + const { useState, useEffect, useMemo } = React; + const RRX = window.RRX; + const { labelFor, nounFor, displayTitle, RecordBody } = window.RRREC; -const { useState, useEffect, useMemo } = React; -const RRX = window.RRX; -const { labelFor, nounFor, displayTitle, RecordBody } = window.RRREC; - -const conById = {}; -RRX.connections.forEach((c) => { conById[c.id] = c; }); -const recById = {}; -RRX.records.forEach((r) => { recById[r.id] = r; }); + const conById = {}; + RRX.connections.forEach((c) => { + conById[c.id] = c; + }); + const recById = {}; + RRX.records.forEach((r) => { + recById[r.id] = r; + }); -/* Reverse links: every relationship reads in both directions. */ -const backlinks = {}; -RRX.records.forEach((r) => { - (r.links || []).forEach(([rel, id]) => { - backlinks[id] = backlinks[id] || []; - backlinks[id].push(["linked from " + r.stream, r.id]); + /* Reverse links: every relationship reads in both directions. */ + const backlinks = {}; + RRX.records.forEach((r) => { + (r.links || []).forEach(([rel, id]) => { + backlinks[id] = backlinks[id] || []; + backlinks[id].push(["linked from " + r.stream, r.id]); + }); }); -}); -function CopyMono({ text }) { - const [ok, setOk] = useState(false); - return ( - - ); -} + function CopyMono({ text }) { + const [ok, setOk] = useState(false); + return ( + + ); + } -/* ── Query language: free text + typed operators, the same axes the RS + /* ── Query language: free text + typed operators, the same axes the RS API exposes. con: stream: role: has:image|link is:folded before:/after: field:value (matches any field key~value). Everything composes. */ -function parseQuery(q) { - const out = { text: [], con: null, stream: null, role: null, hasImage: false, hasLink: false, folded: false, before: null, after: null, fields: [], tokens: [] }; - q.trim().split(/\s+/).filter(Boolean).forEach((tok) => { - const m = tok.match(/^([a-z_]+):(.+)$/i); - if (!m) { out.text.push(tok.toLowerCase()); out.tokens.push({ raw: tok, label: tok }); return; } - const k = m[1].toLowerCase(), v = m[2], kv = v.toLowerCase(); - if (k === "con") { out.con = kv; out.tokens.push({ raw: tok, label: "in " + v }); } - else if (k === "stream") { out.stream = kv; out.tokens.push({ raw: tok, label: "stream: " + v }); } - else if (k === "role") { out.role = kv; out.tokens.push({ raw: tok, label: "role: " + v }); } - else if (k === "has" && kv === "image") { out.hasImage = true; out.tokens.push({ raw: tok, label: "has image" }); } - else if (k === "has" && kv === "link") { out.hasLink = true; out.tokens.push({ raw: tok, label: "has link" }); } - else if (k === "is" && kv === "folded") { out.folded = true; out.tokens.push({ raw: tok, label: "folded" }); } - else if (k === "before") { out.before = v; out.tokens.push({ raw: tok, label: "before " + v }); } - else if (k === "after") { out.after = v; out.tokens.push({ raw: tok, label: "after " + v }); } - else { out.fields.push([k, kv]); out.tokens.push({ raw: tok, label: k + ": " + v }); } - }); - return out; -} + function parseQuery(q) { + const out = { + text: [], + con: null, + stream: null, + role: null, + hasImage: false, + hasLink: false, + folded: false, + before: null, + after: null, + fields: [], + tokens: [], + }; + q.trim() + .split(/\s+/) + .filter(Boolean) + .forEach((tok) => { + const m = tok.match(/^([a-z_]+):(.+)$/i); + if (!m) { + out.text.push(tok.toLowerCase()); + out.tokens.push({ raw: tok, label: tok }); + return; + } + const k = m[1].toLowerCase(), + v = m[2], + kv = v.toLowerCase(); + if (k === "con") { + out.con = kv; + out.tokens.push({ raw: tok, label: "in " + v }); + } else if (k === "stream") { + out.stream = kv; + out.tokens.push({ raw: tok, label: "stream: " + v }); + } else if (k === "role") { + out.role = kv; + out.tokens.push({ raw: tok, label: "role: " + v }); + } else if (k === "has" && kv === "image") { + out.hasImage = true; + out.tokens.push({ raw: tok, label: "has image" }); + } else if (k === "has" && kv === "link") { + out.hasLink = true; + out.tokens.push({ raw: tok, label: "has link" }); + } else if (k === "is" && kv === "folded") { + out.folded = true; + out.tokens.push({ raw: tok, label: "folded" }); + } else if (k === "before") { + out.before = v; + out.tokens.push({ raw: tok, label: "before " + v }); + } else if (k === "after") { + out.after = v; + out.tokens.push({ raw: tok, label: "after " + v }); + } else { + out.fields.push([k, kv]); + out.tokens.push({ raw: tok, label: k + ": " + v }); + } + }); + return out; + } -function ExploreView({ grants, onGo, onJump, seed }) { - const [q, setQ] = useState(""); - const [range, setRange] = useState("all"); - const [conSel, setConSel] = useState(null); - const [streamSel, setStreamSel] = useState(null); - const [sel, setSel] = useState(RRX.records[0].id); - const [lens, setLens] = useState(null); - const [partialOpen, setPartialOpen] = useState(false); + function ExploreView({ grants, onGo, onJump, seed }) { + const [q, setQ] = useState(""); + const [range, setRange] = useState("all"); + const [conSel, setConSel] = useState(null); + const [streamSel, setStreamSel] = useState(null); + const [sel, setSel] = useState(RRX.records[0].id); + const [lens, setLens] = useState(null); + const [partialOpen, setPartialOpen] = useState(false); - /* Sources hands off here with a preselected instance/stream. */ - useEffect(() => { - if (!seed) return; - setConSel(seed.con || null); - setStreamSel(seed.stream || null); - setQ(""); - setRange("all"); - }, [seed && seed.n]); + /* Sources hands off here with a preselected instance/stream. */ + useEffect(() => { + if (!seed) return; + setConSel(seed.con || null); + setStreamSel(seed.stream || null); + setQ(""); + setRange("all"); + }, [seed && seed.n]); - const parsed = useMemo(() => parseQuery(q), [q]); - const [sort, setSort] = useState("newest"); + const parsed = useMemo(() => parseQuery(q), [q]); + const [sort, setSort] = useState("newest"); - function passes(r, opts) { - opts = opts || {}; - const con = conById[r.con]; - if (!opts.ignoreCon && conSel && r.con !== conSel) return false; - if (!opts.ignoreStream && streamSel && r.stream !== streamSel) return false; - if (range === "today" && r.day !== RRX.now) return false; - if (range === "7d" && r.day < "2026-06-06") return false; - if (range === "30d" && r.day < "2026-05-13") return false; - if (parsed.con && !(con.name.toLowerCase().includes(parsed.con) || r.con === parsed.con)) return false; - if (parsed.stream && r.stream !== parsed.stream) return false; - if (parsed.role && r.role !== parsed.role) return false; - if (parsed.hasImage && !r.image) return false; - if (parsed.hasLink && !(r.links && r.links.length)) return false; - if (parsed.folded && !r.fold) return false; - if (parsed.before && !(r.day < parsed.before)) return false; - if (parsed.after && !(r.day > parsed.after)) return false; - if (parsed.fields.length) { - const fm = r.fields || []; - if (!parsed.fields.every(([k, v]) => fm.some(([fk, fv]) => fk.toLowerCase().includes(k) && String(fv).toLowerCase().includes(v)))) return false; - } - if (parsed.text.length) { - const ft = (r.fields || []).map((f) => f[0] + " " + f[1]).join(" "); - const hay = (r.title + " " + (r.snippet || "") + " " + r.stream + " " + con.name + " " + ft).toLowerCase(); - if (!parsed.text.every((t) => hay.includes(t))) return false; + function passes(r, opts) { + opts = opts || {}; + const con = conById[r.con]; + if (!opts.ignoreCon && conSel && r.con !== conSel) return false; + if (!opts.ignoreStream && streamSel && r.stream !== streamSel) return false; + if (range === "today" && r.day !== RRX.now) return false; + if (range === "7d" && r.day < "2026-06-06") return false; + if (range === "30d" && r.day < "2026-05-13") return false; + if (parsed.con && !(con.name.toLowerCase().includes(parsed.con) || r.con === parsed.con)) return false; + if (parsed.stream && r.stream !== parsed.stream) return false; + if (parsed.role && r.role !== parsed.role) return false; + if (parsed.hasImage && !r.image) return false; + if (parsed.hasLink && !(r.links && r.links.length)) return false; + if (parsed.folded && !r.fold) return false; + if (parsed.before && !(r.day < parsed.before)) return false; + if (parsed.after && !(r.day > parsed.after)) return false; + if (parsed.fields.length) { + const fm = r.fields || []; + if ( + !parsed.fields.every(([k, v]) => + fm.some(([fk, fv]) => fk.toLowerCase().includes(k) && String(fv).toLowerCase().includes(v)) + ) + ) + return false; + } + if (parsed.text.length) { + const ft = (r.fields || []).map((f) => f[0] + " " + f[1]).join(" "); + const hay = (r.title + " " + (r.snippet || "") + " " + r.stream + " " + con.name + " " + ft).toLowerCase(); + if (!parsed.text.every((t) => hay.includes(t))) return false; + } + return true; } - return true; - } - const rows = useMemo(() => { - let list = RRX.records.filter((r) => passes(r)); - if (sort === "oldest") list = [...list].reverse(); - return list; - }, [parsed, range, conSel, streamSel, sort]); + const rows = useMemo(() => { + let list = RRX.records.filter((r) => passes(r)); + if (sort === "oldest") list = [...list].reverse(); + return list; + }, [parsed, range, conSel, streamSel, sort]); - const recordCount = rows.reduce((n, r) => n + (r.fold || 1), 0); + const recordCount = rows.reduce((n, r) => n + (r.fold || 1), 0); - /* The machine-parity line: the exact call this view is making. */ - const compiled = useMemo(() => { - const parts = []; - if (conSel || parsed.con) parts.push("connection=" + (conSel ? conById[conSel].cin : parsed.con)); - if (streamSel || parsed.stream) parts.push("stream=" + (streamSel || parsed.stream)); - if (parsed.role) parts.push("role=" + parsed.role); - if (parsed.hasImage) parts.push("content_type=image/*"); - if (parsed.hasLink) parts.push("has=link"); - if (parsed.folded) parts.push("folded=true"); - if (parsed.before) parts.push("before=" + parsed.before); - if (parsed.after) parts.push("after=" + parsed.after); - parsed.fields.forEach(([k, v]) => parts.push(k + "~" + v)); - if (range === "today") parts.push("since=" + RRX.now); - else if (range === "7d") parts.push("since=2026-06-06"); - else if (range === "30d") parts.push("since=2026-05-13"); - if (parsed.text.length) parts.push("match=" + parsed.text.join("+")); - parts.push("order=" + sort, "limit=50"); - return "GET /v1/records?" + parts.join("&"); - }, [parsed, range, conSel, streamSel, sort]); + /* The machine-parity line: the exact call this view is making. */ + const compiled = useMemo(() => { + const parts = []; + if (conSel || parsed.con) parts.push("connection=" + (conSel ? conById[conSel].cin : parsed.con)); + if (streamSel || parsed.stream) parts.push("stream=" + (streamSel || parsed.stream)); + if (parsed.role) parts.push("role=" + parsed.role); + if (parsed.hasImage) parts.push("content_type=image/*"); + if (parsed.hasLink) parts.push("has=link"); + if (parsed.folded) parts.push("folded=true"); + if (parsed.before) parts.push("before=" + parsed.before); + if (parsed.after) parts.push("after=" + parsed.after); + parsed.fields.forEach(([k, v]) => parts.push(k + "~" + v)); + if (range === "today") parts.push("since=" + RRX.now); + else if (range === "7d") parts.push("since=2026-06-06"); + else if (range === "30d") parts.push("since=2026-05-13"); + if (parsed.text.length) parts.push("match=" + parsed.text.join("+")); + parts.push("order=" + sort, "limit=50"); + return "GET /v1/records?" + parts.join("&"); + }, [parsed, range, conSel, streamSel, sort]); - /* Streams facet: instance-true when a connection is selected; + /* Streams facet: instance-true when a connection is selected; otherwise an explicit NAME-match filter (overlap is incidental). */ - const streamFacets = useMemo(() => { - if (conSel) return conById[conSel].streams.map((s) => [s.name, s.records]); - const m = {}; - RRX.connections.forEach((c) => c.streams.forEach((s) => { - m[s.name] = (m[s.name] || 0) + 1; - })); - return Object.entries(m).sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1)); - }, [conSel]); - - const rec = recById[sel] || rows[0] || RRX.records[0]; - const recCon = conById[rec.con]; - const watchers = grants.filter((g) => g.status !== "revoked" && g.projections && g.projections[rec.stream]); - const lensGrant = lens ? watchers.find((g) => g.id === lens) : null; - const proj = lensGrant && rec.fields ? lensGrant.projections[rec.stream] : null; - const baseFields = rec.fields || []; - const shown = proj ? baseFields.filter(([k]) => proj.includes(k)) : baseFields; - const kept = proj ? baseFields.filter(([k]) => !proj.includes(k)) : []; - const fwdIds = new Set((rec.links || []).map(([, id]) => id)); - const related = [...(rec.links || []).map(([rel, id]) => [rel, id]), ...(backlinks[rec.id] || []).filter(([, id]) => !fwdIds.has(id))] - .filter(([, id]) => recById[id]); + const streamFacets = useMemo(() => { + if (conSel) return conById[conSel].streams.map((s) => [s.name, s.records]); + const m = {}; + RRX.connections.forEach((c) => + c.streams.forEach((s) => { + m[s.name] = (m[s.name] || 0) + 1; + }) + ); + return Object.entries(m).sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1)); + }, [conSel]); - useEffect(() => { setLens(null); }, [sel]); + const rec = recById[sel] || rows[0] || RRX.records[0]; + const recCon = conById[rec.con]; + const watchers = grants.filter((g) => g.status !== "revoked" && g.projections && g.projections[rec.stream]); + const lensGrant = lens ? watchers.find((g) => g.id === lens) : null; + const proj = lensGrant && rec.fields ? lensGrant.projections[rec.stream] : null; + const baseFields = rec.fields || []; + const shown = proj ? baseFields.filter(([k]) => proj.includes(k)) : baseFields; + const kept = proj ? baseFields.filter(([k]) => !proj.includes(k)) : []; + const fwdIds = new Set((rec.links || []).map(([, id]) => id)); + const related = [ + ...(rec.links || []).map(([rel, id]) => [rel, id]), + ...(backlinks[rec.id] || []).filter(([, id]) => !fwdIds.has(id)), + ].filter(([, id]) => recById[id]); - useEffect(() => { - function onKey(e) { - if (e.target && /^(INPUT|TEXTAREA|SELECT)$/.test(e.target.tagName)) return; - if (document.querySelector(".rr-overlay, .rr-palette-overlay")) return; - if (e.key !== "ArrowDown" && e.key !== "ArrowUp") return; - e.preventDefault(); - const i = rows.findIndex((r) => r.id === sel); - const n = e.key === "ArrowDown" ? Math.min(i + 1, rows.length - 1) : Math.max(i - 1, 0); - if (rows[n]) setSel(rows[n].id); - } - window.addEventListener("keydown", onKey); - return () => window.removeEventListener("keydown", onKey); - }, [rows, sel]); + useEffect(() => { + setLens(null); + }, [sel]); - const showPartial = !conSel || conSel === RRX.partial.con; - const partialCon = conById[RRX.partial.con]; + useEffect(() => { + function onKey(e) { + if (e.target && /^(INPUT|TEXTAREA|SELECT)$/.test(e.target.tagName)) return; + if (document.querySelector(".rr-overlay, .rr-palette-overlay")) return; + if (e.key !== "ArrowDown" && e.key !== "ArrowUp") return; + e.preventDefault(); + const i = rows.findIndex((r) => r.id === sel); + const n = e.key === "ArrowDown" ? Math.min(i + 1, rows.length - 1) : Math.max(i - 1, 0); + if (rows[n]) setSel(rows[n].id); + } + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }, [rows, sel]); - const activeChips = []; - if (conSel) activeChips.push({ id: "con", label: conById[conSel].name, clear: () => setConSel(null) }); - if (streamSel) activeChips.push({ id: "stream", label: "stream: " + streamSel, clear: () => setStreamSel(null) }); - if (range !== "all") activeChips.push({ id: "range", label: range, clear: () => setRange("all") }); - parsed.tokens.forEach((tk, i) => activeChips.push({ id: "tok" + i, label: tk.label, clear: () => setQ(q.split(/\s+/).filter((x) => x !== tk.raw).join(" ")) })); - const clearAll = () => { setConSel(null); setStreamSel(null); setRange("all"); setQ(""); }; + const showPartial = !conSel || conSel === RRX.partial.con; + const partialCon = conById[RRX.partial.con]; - return ( -
- {/* ── Facet rail ── */} - + const activeChips = []; + if (conSel) activeChips.push({ id: "con", label: conById[conSel].name, clear: () => setConSel(null) }); + if (streamSel) activeChips.push({ id: "stream", label: "stream: " + streamSel, clear: () => setStreamSel(null) }); + if (range !== "all") activeChips.push({ id: "range", label: range, clear: () => setRange("all") }); + parsed.tokens.forEach((tk, i) => + activeChips.push({ + id: "tok" + i, + label: tk.label, + clear: () => + setQ( + q + .split(/\s+/) + .filter((x) => x !== tk.raw) + .join(" ") + ), + }) + ); + const clearAll = () => { + setConSel(null); + setStreamSel(null); + setRange("all"); + setQ(""); + }; - {/* ── Feed ── */} -
-
-
- setQ(e.target.value)} - placeholder="Search names, fields, and values — or type an operator" - type="text" - value={q} - /> -
- sort - - -
+ return ( +
+ {/* ── Facet rail ── */} + - {showPartial && ( -
- - {partialOpen && ( -
-

- This connection was revoked {RRX.partial.revokedOn}. Its streams ({RRX.partial.streams.join(", ")}) refuse - new reads — that's the revocation holding, not a fault. Records ingested before revocation remain on your server. -

- {RRX.partial.raw} - -
- )} -
- )} - - {RRX.days.map(([day, label]) => { - const dayRows = rows.filter((r) => r.day === day); - if (dayRows.length === 0) return null; - return ( -
-
- {label} - {dayRows.reduce((n, r) => n + (r.fold || 1), 0)} + {/* ── Feed ── */} +
+
+
+ setQ(e.target.value)} + placeholder="Search names, fields, and values — or type an operator" + type="text" + value={q} + /> +
+ sort + +
- {dayRows.map((r) => ( +
+
+ {[ + ["today", "today"], + ["7d", "7d"], + ["30d", "30d"], + ["all", "all"], + ].map(([v, label]) => ( ))} +
+ operators +
+ con: stream: role: has:image has:link{" "} + is:folded before:2026-06-11 after:2026-06-10{" "} + merchant:coffee — combine freely; everything composes. +
+
+ +
+ {activeChips.length > 0 && ( +
+ {activeChips.map((c) => ( + + ))} + +
+ )} +
+ the same call any client makes: +
- ); - })} - - {rows.length === 0 && ( -
- Nothing matches{q ? ` \u201c${q}\u201d` : ""} in this window. -
- )} -
- {/* ── The record sheet — the product's ONLY record viewer ── */} -
-
-
-
-

- {(() => { const dt = displayTitle(rec); return dt.kicker - ? {dt.kicker}{dt.primary} - : dt.primary; })()} -

- +

+ {recordCount} records shown · {RRX.totalOnServer} on your server +

+ + {showPartial && ( +
+ + {partialOpen && ( +
+

+ This connection was revoked {RRX.partial.revokedOn}. Its streams ({RRX.partial.streams.join(", ")}) + refuse new reads — that's the revocation holding, not a fault. Records ingested before revocation + remain on your server. +

+ {RRX.partial.raw} + +
+ )}
+ )} - {watchers.length > 0 && ( -
- read it as - - {watchers.map((g) => ( + {RRX.days.map(([day, label]) => { + const dayRows = rows.filter((r) => r.day === day); + if (dayRows.length === 0) return null; + return ( +
+
+ {label} + {dayRows.reduce((n, r) => n + (r.fold || 1), 0)} +
+ {dayRows.map((r) => ( ))}
- )} - {watchers.length === 0 && ( -

Only you can read this. No grant covers {rec.stream} on {recCon.name} — nothing here crosses.

- )} + ); + })} -
- - {rec.fold && ( -

Folded in the feed — every call is kept in the stream, unabridged.

- )} - {rec.degraded && ( -

Title derived from the fields below — every field is listed.

- )} - {kept.length > 0 && ( -
- Stays with you - {kept.map(([k]) => labelFor(k)).join(" · ")} - {kept.length} {kept.length === 1 ? "field" : "fields"} never leave your server — never sent, not blacked out. -
- )} - {related.length > 0 && ( -
- Connected - {related.map(([relName, id]) => { - const dt = displayTitle(recById[id]); - return ( - + {rows.length === 0 && ( +
+ Nothing matches{q ? ` \u201c${q}\u201d` : ""} in this window. + +
+ )} +
+ + {/* ── The record sheet — the product's ONLY record viewer ── */} +
+
+
+
+

+ {(() => { + const dt = displayTitle(rec); + return dt.kicker ? ( + + {dt.kicker} + {dt.primary} + + ) : ( + dt.primary ); - })} + })()} +

+ +
+ + {watchers.length > 0 && ( +
+ read it as + + {watchers.map((g) => ( + + ))}
)} -
- -
- {proj ? ( - {shown.length} of {baseFields.length} fields cross to {lensGrant.client} · enforced on every read - ) : ( - - {baseFields.length} fields · readable by you - {watchers.length > 0 ? ` · ${watchers.length} ${watchers.length === 1 ? "grant reads" : "grants read"} a projection` : ""} - + {watchers.length === 0 && ( +

+ Only you can read this. No grant covers {rec.stream} on {recCon.name} — nothing here crosses. +

)} + +
+ + {rec.fold && ( +

Folded in the feed — every call is kept in the stream, unabridged.

+ )} + {rec.degraded && ( +

Title derived from the fields below — every field is listed.

+ )} + {kept.length > 0 && ( +
+ Stays with you + {kept.map(([k]) => labelFor(k)).join(" · ")} + + {kept.length} {kept.length === 1 ? "field" : "fields"} never leave your server — never sent, not + blacked out. + +
+ )} + {related.length > 0 && ( +
+ Connected + {related.map(([relName, id]) => { + const dt = displayTitle(recById[id]); + return ( + + ); + })} +
+ )} +
+ +
+ {proj ? ( + + {shown.length} of {baseFields.length} fields cross to {lensGrant.client} · enforced on every read + + ) : ( + + {baseFields.length} fields · readable by you + {watchers.length > 0 + ? ` · ${watchers.length} ${watchers.length === 1 ? "grant reads" : "grants read"} a projection` + : ""} + + )} +
-
- ); -} + ); + } -Object.assign(window, { RRExploreView: ExploreView }); + Object.assign(window, { RRExploreView: ExploreView }); })(); diff --git a/docs/design-system/ink-carbon/project/recordroom/rr-overview.jsx b/docs/design-system/ink-carbon/project/recordroom/rr-overview.jsx index 346c773a9..5a5db1a10 100644 --- a/docs/design-system/ink-carbon/project/recordroom/rr-overview.jsx +++ b/docs/design-system/ink-carbon/project/recordroom/rr-overview.jsx @@ -4,179 +4,252 @@ /* RECORDROOM — "Standing": the home as the product's point of view. Person-first language, one hero truth, the owner's three questions, calm↔alarm emotional center, a deliberate warm reassurance moment. */ -;(() => { +(() => { + const { useState } = React; + const RR2 = window.RR2; -const { useState } = React; -const RR2 = window.RR2; - -/* ── Plain-language lexicon: scope → what it means to a person ── */ -const SCOPE_HUMAN = { - "pay_statements.read": "your pay", - "employment.read": "your employment history", - "listening_history.read": "what you listen to", - "transactions.read": "your spending", - "tax_docs.read": "your tax documents", - "browsing.read": "your browsing", -}; -function scopeHuman(name) { return SCOPE_HUMAN[name] || name.replace(/\.read$/, "").replace(/_/g, " "); } + /* ── Plain-language lexicon: scope → what it means to a person ── */ + const SCOPE_HUMAN = { + "pay_statements.read": "your pay", + "employment.read": "your employment history", + "listening_history.read": "what you listen to", + "transactions.read": "your spending", + "tax_docs.read": "your tax documents", + "browsing.read": "your browsing", + }; + function scopeHuman(name) { + return SCOPE_HUMAN[name] || name.replace(/\.read$/, "").replace(/_/g, " "); + } -/* ── What holds BEARER access — acts as you, reads everything. The tier + /* ── What holds BEARER access — acts as you, reads everything. The tier most owners actually use; grants are the scoped minority case. ── */ -const BEARER = [ - { who: "Claude Desktop", how: "owner token · MCP", last: "read everything · 2 h ago", kind: "app" }, - { who: "CLI on framework", how: "owner token", last: "last used yesterday", kind: "key" }, -]; -function joinHuman(arr) { - if (arr.length <= 1) return arr[0] || ""; - if (arr.length === 2) return arr[0] + " and " + arr[1]; - return arr.slice(0, -1).join(", ") + ", and " + arr[arr.length - 1]; -} - -const STREAM_RECORD_NOUN = { - pay_statements: "pay records", employment: "employment records", - listening_history: "listening records", tax_docs: "tax records", - transactions: "transactions", -}; -function recordNoun(stream) { return STREAM_RECORD_NOUN[stream] || stream.replace(/_/g, " ") + " records"; } - -function relDay(t) { - // t like "2026-06-11 07:58:12Z"; now is 2026-06-12 - const d = t.slice(0, 10); - if (d === "2026-06-12") return "today"; - if (d === "2026-06-11") return "yesterday"; - if (d === "2026-05-02") return "May 2"; - return d.slice(5); -} + const BEARER = [ + { who: "Claude Desktop", how: "owner token · MCP", last: "read everything · 2 h ago", kind: "app" }, + { who: "CLI on framework", how: "owner token", last: "last used yesterday", kind: "key" }, + ]; + function joinHuman(arr) { + if (arr.length <= 1) return arr[0] || ""; + if (arr.length === 2) return arr[0] + " and " + arr[1]; + return arr.slice(0, -1).join(", ") + ", and " + arr[arr.length - 1]; + } -function Overview({ grants, requestState, onReview, onGo, onOpenGrant }) { - const [resolved, setResolved] = useState(false); - const active = grants.filter((g) => g.status !== "revoked"); - const pending = requestState === "pending"; - const hasFailure = !resolved; // First Meridian sync, part of the standing fixture + const STREAM_RECORD_NOUN = { + pay_statements: "pay records", + employment: "employment records", + listening_history: "listening records", + tax_docs: "tax records", + transactions: "transactions", + }; + function recordNoun(stream) { + return STREAM_RECORD_NOUN[stream] || stream.replace(/_/g, " ") + " records"; + } - /* ── The hero: one truth, computed from state ── */ - let hero; - if (pending) { - hero = { - tone: "decide", - kicker: "A request is waiting on you", - line: Atlas Mortgage wants to read your pay, employment, and spending., - sub: "Nothing leaves until you say so — approve it one piece at a time.", - cta: , - }; - } else if (hasFailure) { - hero = { - tone: "alarm", - kicker: "One thing needs you", - line: Your bank data stopped arriving on Jun 11., - sub: "First Meridian's connection expired. Nothing you already have is lost — but nothing new arrives until you reconnect.", - cta: , - }; - } else { - hero = { - tone: "calm", - kicker: "Where you stand", - line: 48,120 records from 10 sources — all yours to read., - sub: BEARER.length + " tokens can act as you, with full access. " + active.length + " apps read only the slices you granted. Revoke any of them instantly.", - }; + function relDay(t) { + // t like "2026-06-11 07:58:12Z"; now is 2026-06-12 + const d = t.slice(0, 10); + if (d === "2026-06-12") return "today"; + if (d === "2026-06-11") return "yesterday"; + if (d === "2026-05-02") return "May 2"; + return d.slice(5); } - /* ── What's crossed lately (humanized traces) ── */ - const lately = RR2.traces.slice(0, 4).map((tr) => { - if (tr.decision === "deny") { - const why = tr.reason === "scope not granted" ? "you never allowed it" - : tr.reason === "grant revoked" ? "you'd revoked it" - : tr.reason; - return { id: tr.id, when: relDay(tr.t), deny: true, - text: {tr.client} tried to read {tr.stream.replace(/_/g, " ")} — turned away, {why}. }; + function Overview({ grants, requestState, onReview, onGo, onOpenGrant }) { + const [resolved, setResolved] = useState(false); + const active = grants.filter((g) => g.status !== "revoked"); + const pending = requestState === "pending"; + const hasFailure = !resolved; // First Meridian sync, part of the standing fixture + + /* ── The hero: one truth, computed from state ── */ + let hero; + if (pending) { + hero = { + tone: "decide", + kicker: "A request is waiting on you", + line: ( + + Atlas Mortgage wants to read your pay, employment, and spending. + + ), + sub: "Nothing leaves until you say so — approve it one piece at a time.", + cta: ( + + ), + }; + } else if (hasFailure) { + hero = { + tone: "alarm", + kicker: "One thing needs you", + line: ( + + Your bank data stopped arriving on Jun 11. + + ), + sub: "First Meridian's connection expired. Nothing you already have is lost — but nothing new arrives until you reconnect.", + cta: ( + + ), + }; + } else { + hero = { + tone: "calm", + kicker: "Where you stand", + line: ( + + 48,120 records from 10 sources — all yours to read. + + ), + sub: + BEARER.length + + " tokens can act as you, with full access. " + + active.length + + " apps read only the slices you granted. Revoke any of them instantly.", + }; } - return { id: tr.id, when: relDay(tr.t), deny: false, - text: {tr.client} read {tr.records} {recordNoun(tr.stream)} — {tr.fields} fields each. }; - }); - return ( -
- {/* HERO */} -
- {hero.kicker} -

{hero.line}

-

{hero.sub}

- {hero.cta && ( -
- {hero.cta} -
- )} -
+ /* ── What's crossed lately (humanized traces) ── */ + const lately = RR2.traces.slice(0, 4).map((tr) => { + if (tr.decision === "deny") { + const why = + tr.reason === "scope not granted" + ? "you never allowed it" + : tr.reason === "grant revoked" + ? "you'd revoked it" + : tr.reason; + return { + id: tr.id, + when: relDay(tr.t), + deny: true, + text: ( + + {tr.client} tried to read {tr.stream.replace(/_/g, " ")} — turned away, {why}. + + ), + }; + } + return { + id: tr.id, + when: relDay(tr.t), + deny: false, + text: ( + + {tr.client} read {tr.records} {recordNoun(tr.stream)} — {tr.fields} fields each. + + ), + }; + }); - {/* WHAT CAN ACT AS YOU — bearer access, the primary tier */} -
-
-

What can act as you

- -
-
- {BEARER.map((b) => ( -
- {b.who} - reads everything - {b.how} · {b.last} - -
- ))} -

An owner token reads everything — every source, every field, exactly what you see. Keep the list short; revoke anytime.

-
-
+ return ( +
+ {/* HERO */} +
+ {hero.kicker} +

{hero.line}

+

{hero.sub}

+ {hero.cta &&
{hero.cta}
} +
-
- {/* WHO CAN READ PARTS OF YOU — grants, the scoped tier */} + {/* WHAT CAN ACT AS YOU — bearer access, the primary tier */}
-

Who can read parts of you

- +

What can act as you

+
-
- {active.map((g) => ( -
- {g.client} - reads only {joinHuman(g.scopes.map((s) => scopeHuman(s.name)))} - +
+ {BEARER.map((b) => ( +
+ {b.who} + reads everything + + {b.how} · {b.last} + +
))} - {active.length === 0 &&

No grant is out. Nothing is shared — only you and what you've given a token read this server.

} +

+ An owner token reads everything — every source, every field, exactly what you see. Keep the list short; + revoke anytime. +

- {/* WHAT'S CROSSED LATELY */} +
+ {/* WHO CAN READ PARTS OF YOU — grants, the scoped tier */} +
+
+

Who can read parts of you

+ +
+
+ {active.map((g) => ( +
+ {g.client} + reads only {joinHuman(g.scopes.map((s) => scopeHuman(s.name)))} + +
+ ))} + {active.length === 0 && ( +

+ No grant is out. Nothing is shared — only you and what you've given a token read this server. +

+ )} +
+
+ + {/* WHAT'S CROSSED LATELY */} +
+
+

What's been read

+ +
+
+ {lately.map((e) => ( +
+ {e.text} + {e.when} +
+ ))} +
+
+
+ + {/* ANYTHING WRONG */}
-
-

What's been read

- -
-
- {lately.map((e) => ( -
- {e.text} - {e.when} -
- ))} -
+

Anything wrong

+ {hasFailure ? ( + window.RRAttentionList ? ( + + ) : null + ) : ( +
+ + Nothing needs you. Grants are within their limits, backups are on, and everything's syncing. + +
+ )}
+ ); + } - {/* ANYTHING WRONG */} -
-

Anything wrong

- {hasFailure ? ( - window.RRAttentionList ? : null - ) : ( -
- Nothing needs you. Grants are within their limits, backups are on, and everything's syncing. -
- )} -
-
- ); -} - -Object.assign(window, { RROverview2: Overview }); + Object.assign(window, { RROverview2: Overview }); })(); diff --git a/docs/design-system/ink-carbon/project/recordroom/rr-record.jsx b/docs/design-system/ink-carbon/project/recordroom/rr-record.jsx index 332b56d4a..5665c74d5 100644 --- a/docs/design-system/ink-carbon/project/recordroom/rr-record.jsx +++ b/docs/design-system/ink-carbon/project/recordroom/rr-record.jsx @@ -5,145 +5,217 @@ Wire keys never leak alone: every field shows a human label AND its wire key (what a client literally receives). Long text gets a reading region; money gets tabular figures; derived titles get one grammar. */ -;(() => { +(() => { + /* ── Lexicon: wire key → human label. Fallback prettifies snake_case. ── */ + const FIELD_LABELS = { + employer: "Employer", + period_start: "Period start", + period_end: "Period end", + gross_pay: "Gross pay", + net_pay: "Net pay", + taxes_withheld: "Taxes withheld", + benefits_detail: "Benefits", + bank_routing: "Deposited to", + date: "Date", + amount: "Amount", + merchant: "Merchant", + category: "Category", + account_ref: "Account", + memo: "Memo", + track: "Track", + artist: "Artist", + played_at: "Played", + device: "Device", + playlist_ref: "Playlist", + from: "From", + subject: "Subject", + received: "Received", + size: "Size", + label: "Label", + participants: "Participants", + messages: "Messages", + role: "Role", + session: "Session", + chars: "Length", + content: "Message", + model: "Model", + charset: "Encoding", + bytes: "Size", + message_ref: "Message", + text: "Body", + repo: "Repository", + visibility: "Visibility", + pushed: "Last push", + open_prs: "Open PRs", + commits: "Commits", + prs_opened: "PRs opened", + reviews: "Reviews", + title: "Title", + started: "Started", + prompt: "Prompt", + turns: "Turns", + calls: "Calls", + span: "Over", + breakdown: "Breakdown", + pings: "Pings", + filename: "File", + content_type: "Type", + doc_type: "Document", + tax_year: "Tax year", + }; -/* ── Lexicon: wire key → human label. Fallback prettifies snake_case. ── */ -const FIELD_LABELS = { - employer: "Employer", period_start: "Period start", period_end: "Period end", - gross_pay: "Gross pay", net_pay: "Net pay", taxes_withheld: "Taxes withheld", - benefits_detail: "Benefits", bank_routing: "Deposited to", - date: "Date", amount: "Amount", merchant: "Merchant", category: "Category", - account_ref: "Account", memo: "Memo", - track: "Track", artist: "Artist", played_at: "Played", device: "Device", playlist_ref: "Playlist", - from: "From", subject: "Subject", received: "Received", size: "Size", label: "Label", - participants: "Participants", messages: "Messages", - role: "Role", session: "Session", chars: "Length", content: "Message", model: "Model", - charset: "Encoding", bytes: "Size", message_ref: "Message", text: "Body", - repo: "Repository", visibility: "Visibility", pushed: "Last push", open_prs: "Open PRs", - commits: "Commits", prs_opened: "PRs opened", reviews: "Reviews", - title: "Title", started: "Started", prompt: "Prompt", turns: "Turns", - calls: "Calls", span: "Over", breakdown: "Breakdown", pings: "Pings", - filename: "File", content_type: "Type", doc_type: "Document", tax_year: "Tax year", -}; - -function prettify(k) { - return k.replace(/_/g, " ").replace(/\bref\b/, "").trim().replace(/^\w/, (c) => c.toUpperCase()); -} -function labelFor(k) { return FIELD_LABELS[k] || prettify(k); } + function prettify(k) { + return k + .replace(/_/g, " ") + .replace(/\bref\b/, "") + .trim() + .replace(/^\w/, (c) => c.toUpperCase()); + } + function labelFor(k) { + return FIELD_LABELS[k] || prettify(k); + } -const STREAM_NOUN = { - messages: "message", message_bodies: "message body", threads: "thread", - attachments: "attachment", sessions: "session", function_calls: "tool calls", - conversations: "conversation", repositories: "repository", user_stats: "stats snapshot", - pay_statements: "pay statement", transactions: "transaction", listening_history: "play", - tax_docs: "document", employment: "record", balances: "balance", statements: "statement", - skills: "skill", user: "record", -}; -function nounFor(stream) { return STREAM_NOUN[stream] || "record"; } + const STREAM_NOUN = { + messages: "message", + message_bodies: "message body", + threads: "thread", + attachments: "attachment", + sessions: "session", + function_calls: "tool calls", + conversations: "conversation", + repositories: "repository", + user_stats: "stats snapshot", + pay_statements: "pay statement", + transactions: "transaction", + listening_history: "play", + tax_docs: "document", + employment: "record", + balances: "balance", + statements: "statement", + skills: "skill", + user: "record", + }; + function nounFor(stream) { + return STREAM_NOUN[stream] || "record"; + } -function fieldMap(rec) { - return Object.fromEntries((rec.fields || []).map((f) => [f[0], f[1]])); -} + function fieldMap(rec) { + return Object.fromEntries((rec.fields || []).map((f) => [f[0], f[1]])); + } -/* ── Kind dispatch — by field signature, not stream name (a "messages" + /* ── Kind dispatch — by field signature, not stream name (a "messages" stream is email from Gmail but an agent turn from Codex). ── */ -function kindOf(rec) { - const k = new Set((rec.fields || []).map((f) => f[0])); - if (rec.image || k.has("filename") || k.has("content_type")) return "attachment"; - if (k.has("amount") || k.has("gross_pay") || k.has("net_pay")) return "money"; - if (k.has("track") || k.has("artist")) return "media"; - if (k.has("charset") && k.has("text")) return "body"; - if (k.has("role")) return "agent"; - if (k.has("from") || k.has("subject") || k.has("participants")) return "email"; - if (k.has("repo") || k.has("commits")) return "code"; - return "generic"; -} + function kindOf(rec) { + const k = new Set((rec.fields || []).map((f) => f[0])); + if (rec.image || k.has("filename") || k.has("content_type")) return "attachment"; + if (k.has("amount") || k.has("gross_pay") || k.has("net_pay")) return "money"; + if (k.has("track") || k.has("artist")) return "media"; + if (k.has("charset") && k.has("text")) return "body"; + if (k.has("role")) return "agent"; + if (k.has("from") || k.has("subject") || k.has("participants")) return "email"; + if (k.has("repo") || k.has("commits")) return "code"; + return "generic"; + } -const MONEY_RE = /^[−-]?\$|^\$/; -function isMoneyVal(v) { return typeof v === "string" && MONEY_RE.test(v); } -function isLongVal(key, v) { return (key === "text" || key === "content") && typeof v === "string" && v.length > 56; } + const MONEY_RE = /^[−-]?\$|^\$/; + function isMoneyVal(v) { + return typeof v === "string" && MONEY_RE.test(v); + } + function isLongVal(key, v) { + return (key === "text" || key === "content") && typeof v === "string" && v.length > 56; + } -/* ── One derived-title grammar. Never "no X" — a quiet kicker + a fact. ── */ -function displayTitle(rec) { - if (!rec.degraded) return { primary: rec.title, kicker: null }; - const f = fieldMap(rec); - const noun = nounFor(rec.stream); - let hint = ""; - if (f.from) hint = "from " + f.from; - else if (f.role) hint = f.role + " turn"; - else if (f.bytes || f.charset) hint = [f.charset, f.bytes ? Math.round(String(f.bytes).replace(/[^\d]/g, "") / 1024) + " KB" : ""].filter(Boolean).join(" · "); - else if (f.date) hint = f.date; - return { primary: hint || noun, kicker: "untitled " + noun }; -} + /* ── One derived-title grammar. Never "no X" — a quiet kicker + a fact. ── */ + function displayTitle(rec) { + if (!rec.degraded) return { primary: rec.title, kicker: null }; + const f = fieldMap(rec); + const noun = nounFor(rec.stream); + let hint = ""; + if (f.from) hint = "from " + f.from; + else if (f.role) hint = f.role + " turn"; + else if (f.bytes || f.charset) + hint = [f.charset, f.bytes ? Math.round(String(f.bytes).replace(/[^\d]/g, "") / 1024) + " KB" : ""] + .filter(Boolean) + .join(" · "); + else if (f.date) hint = f.date; + return { primary: hint || noun, kicker: "untitled " + noun }; + } -/* ── Dual-key field row ── */ -function Field({ k, v }) { - return ( -
- - {labelFor(k)} - {k} - - {v} -
- ); -} + /* ── Dual-key field row ── */ + function Field({ k, v }) { + return ( +
+ + {labelFor(k)} + {k} + + {v} +
+ ); + } -/* ── Kind-aware body: hero + image + reading region + dual-key fields ── */ -function RecordBody({ rec, pairs }) { - const kind = kindOf(rec); - const present = (key) => pairs.find(([k]) => k === key); - const heroKey = kind === "money" ? (["net_pay", "amount", "gross_pay"].find((k) => present(k))) : null; - const bodyPair = pairs.find(([k, v]) => isLongVal(k, v)); - const heroVal = heroKey ? present(heroKey)[1] : null; - const negative = heroVal && /^[−-]/.test(heroVal); + /* ── Kind-aware body: hero + image + reading region + dual-key fields ── */ + function RecordBody({ rec, pairs }) { + const kind = kindOf(rec); + const present = (key) => pairs.find(([k]) => k === key); + const heroKey = kind === "money" ? ["net_pay", "amount", "gross_pay"].find((k) => present(k)) : null; + const bodyPair = pairs.find(([k, v]) => isLongVal(k, v)); + const heroVal = heroKey ? present(heroKey)[1] : null; + const negative = heroVal && /^[−-]/.test(heroVal); - const captionParts = []; - if (kind === "money") { - ["merchant", "employer", "category", "period_end", "date"].forEach((k) => { - const p = present(k); if (p) captionParts.push(p[1]); - }); - } + const captionParts = []; + if (kind === "money") { + ["merchant", "employer", "category", "period_end", "date"].forEach((k) => { + const p = present(k); + if (p) captionParts.push(p[1]); + }); + } - const skip = new Set(); - if (heroKey) skip.add(heroKey); - if (bodyPair) skip.add(bodyPair[0]); - const rest = pairs.filter(([k]) => !skip.has(k)); + const skip = new Set(); + if (heroKey) skip.add(heroKey); + if (bodyPair) skip.add(bodyPair[0]); + const rest = pairs.filter(([k]) => !skip.has(k)); - return ( - - {heroKey && ( -
- {heroVal} - {captionParts.length > 0 && {captionParts.slice(0, 2).join(" · ")}} - {labelFor(heroKey)} · {heroKey} -
- )} - {rec.image && ( - - )} - {bodyPair && ( -
- {labelFor(bodyPair[0])} {bodyPair[0]} -

{bodyPair[1]}

-
- )} - {rest.length > 0 && ( -
- {rest.map(([k, v]) => )} -
- )} -
- ); -} + return ( + + {heroKey && ( +
+ {heroVal} + {captionParts.length > 0 && {captionParts.slice(0, 2).join(" · ")}} + + {labelFor(heroKey)} · {heroKey} + +
+ )} + {rec.image && ( + + )} + {bodyPair && ( +
+ + {labelFor(bodyPair[0])} {bodyPair[0]} + +

{bodyPair[1]}

+
+ )} + {rest.length > 0 && ( +
+ {rest.map(([k, v]) => ( + + ))} +
+ )} +
+ ); + } -Object.assign(window, { - RRREC: { labelFor, nounFor, kindOf, displayTitle, Field, RecordBody }, -}); + Object.assign(window, { + RRREC: { labelFor, nounFor, kindOf, displayTitle, Field, RecordBody }, + }); })(); diff --git a/docs/design-system/ink-carbon/project/recordroom/rr-sources.jsx b/docs/design-system/ink-carbon/project/recordroom/rr-sources.jsx index 288cfe0d9..3b20eee9c 100644 --- a/docs/design-system/ink-carbon/project/recordroom/rr-sources.jsx +++ b/docs/design-system/ink-carbon/project/recordroom/rr-sources.jsx @@ -4,153 +4,206 @@ /* RECORDROOM — Sources: the loading dock. Per-instance operational truth: identity, config, auth, stream manifests, health. Records are never viewed here — every record path hands off to Explore, the one reader. */ -;(() => { +(() => { + const { useState } = React; + const RRX = window.RRX; -const { useState } = React; -const RRX = window.RRX; + function SourcesView({ grants, onBrowse, onGo }) { + const [sel, setSel] = useState(RRX.connections[0].id); + const [revoking, setRevoking] = useState(false); + const [localRevoked, setLocalRevoked] = useState([]); + const [syncing, setSyncing] = useState(false); + const [synced, setSynced] = useState([]); -function SourcesView({ grants, onBrowse, onGo }) { - const [sel, setSel] = useState(RRX.connections[0].id); - const [revoking, setRevoking] = useState(false); - const [localRevoked, setLocalRevoked] = useState([]); - const [syncing, setSyncing] = useState(false); - const [synced, setSynced] = useState([]); + const con = RRX.connections.find((c) => c.id === sel); + const status = localRevoked.includes(con.id) ? "revoked" : con.status; + const revoked = status === "revoked"; - const con = RRX.connections.find((c) => c.id === sel); - const status = localRevoked.includes(con.id) ? "revoked" : con.status; - const revoked = status === "revoked"; + /* which grants read each stream of this instance */ + function readBy(streamName) { + const names = grants + .filter((g) => g.status !== "revoked" && g.projections && g.projections[streamName]) + .map((g) => g.client); + return names.length ? names.join(" · ") : "—"; + } - /* which grants read each stream of this instance */ - function readBy(streamName) { - const names = grants - .filter((g) => g.status !== "revoked" && g.projections && g.projections[streamName]) - .map((g) => g.client); - return names.length ? names.join(" · ") : "—"; - } - - function syncNow() { - setSyncing(true); - setTimeout(() => { - setSyncing(false); - setSynced((cur) => [...cur, con.id]); - }, 800); - } + function syncNow() { + setSyncing(true); + setTimeout(() => { + setSyncing(false); + setSynced((cur) => [...cur, con.id]); + }, 800); + } - return ( -
- {/* ── Instance list ── */} - - - {/* ── Instance passport ── */} -
-
-
-

- {revoked ? {con.name} : con.name} -

- {con.cin} + a source pushes into your streams · nothing leaves
-
-
-
kind{con.kind}
-
account{con.account}
-
config{con.config}
-
auth{revoked ? "revoked" : con.auth}
-
schedule{con.schedule}
-
last run{synced.includes(con.id) ? "ok · just now" : con.lastRun}
-
added{con.added}
+ + + {/* ── Instance passport ── */} +
+
+
+

{revoked ? {con.name} : con.name}

+ {con.cin}
- {revoked && ( -

- Revoked {con.id === "cc2" ? RRX.partial.revokedOn : "just now"} — this instance can no longer push. - Records ingested before revocation remain on your server, in your streams. -

- )} -
-
- - {!revoked && ( - - )} - {status === "reauth" && ( - - )} - {!revoked && !revoking && ( - - )} - {!revoked && revoking && ( - - - - +
+
+
+ kind + {con.kind} +
+
+ account + {con.account} +
+
+ config + {con.config} +
+
+ auth + {revoked ? "revoked" : con.auth} +
+
+ schedule + {con.schedule} +
+
+ last run + {synced.includes(con.id) ? "ok · just now" : con.lastRun} +
+
+ added + {con.added} +
+
+ {revoked && ( +

+ Revoked {con.id === "cc2" ? RRX.partial.revokedOn : "just now"} — this instance can no longer push. + Records ingested before revocation remain on your server, in your streams. +

)} - - +
+
+ + {!revoked && ( + + )} + {status === "reauth" && ( + + )} + {!revoked && !revoking && ( + + )} + {!revoked && revoking && ( + + + + + )} + + +
-
- {/* ── Stream manifest — schema-level truth, not records ── */} -
-
-

Streams on this instance

- {con.streams.length} -
-
-
- stream - records - cursor - search - read by + {/* ── Stream manifest — schema-level truth, not records ── */} +
+
+

Streams on this instance

+ {con.streams.length}
- {con.streams.map((s) => ( - - ))} +
+
+ stream + records + cursor + search + read by +
+ {con.streams.map((s) => ( + + ))} +
+

+ {"\u201c"}sealed{"\u201d"} streams hold binary or machine arguments — browsable and linked, not + text-searched. Click any stream to read its records in Explore. +

-

- {"\u201c"}sealed{"\u201d"} streams hold binary or machine arguments — browsable and linked, not text-searched. - Click any stream to read its records in Explore. -

-
- ); -} + ); + } -Object.assign(window, { RRSourcesView2: SourcesView }); + Object.assign(window, { RRSourcesView2: SourcesView }); })(); diff --git a/docs/design-system/ink-carbon/project/recordroom/rr-syncs.jsx b/docs/design-system/ink-carbon/project/recordroom/rr-syncs.jsx index c75489acc..4f67e45e5 100644 --- a/docs/design-system/ink-carbon/project/recordroom/rr-syncs.jsx +++ b/docs/design-system/ink-carbon/project/recordroom/rr-syncs.jsx @@ -5,293 +5,451 @@ Connect / Exporters / Subscriptions + the Overview attention list. Health-first: the failure is a card with an action, not a row. Data: window.RR2 + a compact sync model authored here. */ -;(() => { +(() => { + const { useState } = React; + const RR2 = window.RR2; -const { useState } = React; -const RR2 = window.RR2; - -/* ── Sync model: grouped by connection instance, per-stream rhythm ── */ -const SYNCS = [ - { con: "Northstar HR", cin: "cin_nh_e3391c", health: "ok", - streams: [ - { stream: "pay_statements", cadence: "with payroll", next: "Jun 12 · 06:00Z", rhythm: ["ok", "ok", "ok", "ok", "ok"], delta: "+2 records", when: "today 06:00Z", dur: "18 s" }, - { stream: "employment", cadence: "daily · 06:00Z", next: "Jun 12 · 06:00Z", rhythm: ["ok", "ok", "ok", "ok", "ok"], delta: "no change", when: "today 06:00Z", dur: "4 s", quiet: true }, - { stream: "tax_docs", cadence: "yearly · Feb 01", next: "2027 · Feb 01", rhythm: ["ok"], delta: "no change", when: "Feb 01", dur: "2 s", quiet: true }, - ] }, - { con: "First Meridian — checking", cin: "cin_fm_206b11", health: "failing", - fix: { - title: "First Meridian — checking can't sync", - body: "The bank's OFX session expired on Jun 11. New transactions aren't arriving — the cursor is held at Jun 10, so nothing already on your server is lost, but nothing new is coming in either.", - action: "Reauthorize bank", + /* ── Sync model: grouped by connection instance, per-stream rhythm ── */ + const SYNCS = [ + { + con: "Northstar HR", + cin: "cin_nh_e3391c", + health: "ok", + streams: [ + { + stream: "pay_statements", + cadence: "with payroll", + next: "Jun 12 · 06:00Z", + rhythm: ["ok", "ok", "ok", "ok", "ok"], + delta: "+2 records", + when: "today 06:00Z", + dur: "18 s", + }, + { + stream: "employment", + cadence: "daily · 06:00Z", + next: "Jun 12 · 06:00Z", + rhythm: ["ok", "ok", "ok", "ok", "ok"], + delta: "no change", + when: "today 06:00Z", + dur: "4 s", + quiet: true, + }, + { + stream: "tax_docs", + cadence: "yearly · Feb 01", + next: "2027 · Feb 01", + rhythm: ["ok"], + delta: "no change", + when: "Feb 01", + dur: "2 s", + quiet: true, + }, + ], + }, + { + con: "First Meridian — checking", + cin: "cin_fm_206b11", + health: "failing", + fix: { + title: "First Meridian — checking can't sync", + body: "The bank's OFX session expired on Jun 11. New transactions aren't arriving — the cursor is held at Jun 10, so nothing already on your server is lost, but nothing new is coming in either.", + action: "Reauthorize bank", + }, + streams: [ + { + stream: "transactions", + cadence: "daily · 05:00Z", + next: "held", + rhythm: ["ok", "ok", "ok", "ok", "fail"], + delta: "held at Jun 10", + when: "Jun 11 05:00Z", + dur: "2 s", + failed: true, + }, + { + stream: "statements", + cadence: "monthly", + next: "held", + rhythm: ["ok", "ok", "fail"], + delta: "held at May", + when: "Jun 11 05:00Z", + dur: "—", + failed: true, + }, + { + stream: "balances", + cadence: "daily · 05:00Z", + next: "held", + rhythm: ["ok", "ok", "ok", "fail"], + delta: "held at Jun 10", + when: "Jun 11 05:00Z", + dur: "—", + failed: true, + }, + ], + }, + { + con: "Gmail — personal", + cin: "cin_gm_410c2b", + health: "ok", + streams: [ + { + stream: "messages", + cadence: "every 15 min", + next: "in 11 min", + rhythm: ["ok", "ok", "ok", "ok", "ok"], + delta: "+38 records", + when: "31 min ago", + dur: "6 s", + }, + { + stream: "threads", + cadence: "every 15 min", + next: "in 11 min", + rhythm: ["ok", "ok", "ok", "ok", "ok"], + delta: "+12 records", + when: "31 min ago", + dur: "5 s", + }, + { + stream: "attachments", + cadence: "every 15 min", + next: "in 11 min", + rhythm: ["ok", "ok", "ok", "ok", "ok"], + delta: "+3 records", + when: "2 h ago", + dur: "9 s", + }, + ], + }, + { + con: "Tonal", + cin: "cin_tn_77f024", + health: "ok", + streams: [ + { + stream: "listening_history", + cadence: "every 15 min", + next: "in 3 min", + rhythm: ["ok", "ok", "ok", "ok", "ok"], + delta: "+4 records", + when: "12 min ago", + dur: "7 s", + }, + ], }, - streams: [ - { stream: "transactions", cadence: "daily · 05:00Z", next: "held", rhythm: ["ok", "ok", "ok", "ok", "fail"], delta: "held at Jun 10", when: "Jun 11 05:00Z", dur: "2 s", failed: true }, - { stream: "statements", cadence: "monthly", next: "held", rhythm: ["ok", "ok", "fail"], delta: "held at May", when: "Jun 11 05:00Z", dur: "—", failed: true }, - { stream: "balances", cadence: "daily · 05:00Z", next: "held", rhythm: ["ok", "ok", "ok", "fail"], delta: "held at Jun 10", when: "Jun 11 05:00Z", dur: "—", failed: true }, - ] }, - { con: "Gmail — personal", cin: "cin_gm_410c2b", health: "ok", - streams: [ - { stream: "messages", cadence: "every 15 min", next: "in 11 min", rhythm: ["ok", "ok", "ok", "ok", "ok"], delta: "+38 records", when: "31 min ago", dur: "6 s" }, - { stream: "threads", cadence: "every 15 min", next: "in 11 min", rhythm: ["ok", "ok", "ok", "ok", "ok"], delta: "+12 records", when: "31 min ago", dur: "5 s" }, - { stream: "attachments", cadence: "every 15 min", next: "in 11 min", rhythm: ["ok", "ok", "ok", "ok", "ok"], delta: "+3 records", when: "2 h ago", dur: "9 s" }, - ] }, - { con: "Tonal", cin: "cin_tn_77f024", health: "ok", - streams: [ - { stream: "listening_history", cadence: "every 15 min", next: "in 3 min", rhythm: ["ok", "ok", "ok", "ok", "ok"], delta: "+4 records", when: "12 min ago", dur: "7 s" }, - ] }, -]; + ]; -function Rhythm({ runs }) { - return ( - - {runs.map((r, i) => ( - - ))} - - ); -} + function Rhythm({ runs }) { + return ( + + {runs.map((r, i) => ( + + ))} + + ); + } -/* ─── Syncs ─── */ + /* ─── Syncs ─── */ -function SyncsView() { - const [open, setOpen] = useState(null); - const [fixed, setFixed] = useState(false); - const groups = SYNCS; - const streamTotal = groups.reduce((n, g) => n + g.streams.length, 0); - const failing = fixed ? 0 : groups.filter((g) => g.health === "failing").reduce((n, g) => n + g.streams.length, 0); - const onSched = streamTotal - failing; - const fixGroup = groups.find((g) => g.fix); + function SyncsView() { + const [open, setOpen] = useState(null); + const [fixed, setFixed] = useState(false); + const groups = SYNCS; + const streamTotal = groups.reduce((n, g) => n + g.streams.length, 0); + const failing = fixed ? 0 : groups.filter((g) => g.health === "failing").reduce((n, g) => n + g.streams.length, 0); + const onSched = streamTotal - failing; + const fixGroup = groups.find((g) => g.fix); - return ( -
-
-
- {onSched} - streams on schedule -
-
- {failing} - {failing ? "need your hand" : "need attention"} -
-
- Nothing already saved is ever lost — a held connection only pauses new arrivals. -
-
- - {fixGroup && !fixed && ( -
-
-

{fixGroup.fix.title}

-

{fixGroup.fix.body}

+ return ( +
+
+
+ {onSched} + streams on schedule
-
- +
+ {failing} + {failing ? "need your hand" : "need attention"} +
+
+ + Nothing already saved is ever lost — a held connection only pauses new arrivals. +
- )} - {groups.map((g) => { - const healthy = fixed || g.health !== "failing"; - return ( -
-
- - {g.con} - {g.cin} - {g.streams.length} {g.streams.length === 1 ? "stream" : "streams"} + {fixGroup && !fixed && ( +
+
+

{fixGroup.fix.title}

+

{fixGroup.fix.body}

-
-
- stream - cadence - recent - last result - next +
+ +
+
+ )} + + {groups.map((g) => { + const healthy = fixed || g.health !== "failing"; + return ( +
+
+ + {g.con} + {g.cin} + + {g.streams.length} {g.streams.length === 1 ? "stream" : "streams"} +
- {g.streams.map((s) => { - const failed = s.failed && !fixed; - const isOpen = open === g.cin + s.stream; - return ( - - - {isOpen && ( -
-
- last run - {s.when} · {s.dur} - delta - {failed ? "0 records — cursor held" : s.delta} - cadence - {s.cadence} +
+
+ stream + cadence + recent + last result + next +
+ {g.streams.map((s) => { + const failed = s.failed && !fixed; + const isOpen = open === g.cin + s.stream; + return ( + + + {isOpen && ( +
+
+ last run + + {s.when} · {s.dur} + + delta + {failed ? "0 records — cursor held" : s.delta} + cadence + {s.cadence} +
+
- -
- )} - - ); - })} + )} + + ); + })} +
-
- ); - })} -
- ); -} + ); + })} +
+ ); + } -/* ─── Connect (de-souped) ─── */ + /* ─── Connect (de-souped) ─── */ -function ConnectView() { - return ( -
- {RR2.apps.map((a) => { - const pending = a.status !== "connected"; - const code = pending ? (a.detail.match(/code (\S+)/) || [])[1] : null; - return ( -
- - {a.name} - {a.via} - - - {pending ? "Waiting for the device code to be entered on this console." : "Reads through your grants — never more than the grant behind it."} - - - {pending ? ( - - {code} - expires 9 min - - - ) : ( + function ConnectView() { + return ( +
+ {RR2.apps.map((a) => { + const pending = a.status !== "connected"; + const code = pending ? (a.detail.match(/code (\S+)/) || [])[1] : null; + return ( +
+ + {a.name} + {a.via} + + + {pending + ? "Waiting for the device code to be entered on this console." + : "Reads through your grants — never more than the grant behind it."} + + + {pending ? ( + + {code} + expires 9 min + + + ) : ( + + connected + since {a.added} + + )} + +
+ ); + })} +
+ + apps read through grants — never more than the grant behind them +
+
+ ); + } + + /* ─── Device exporters (de-souped) ─── */ + + function ExportersView() { + const [paused, setPaused] = useState({}); + return ( +
+ {RR2.exporters.map((e) => { + const isPaused = paused[e.device] != null ? paused[e.device] : e.status === "paused"; + return ( +
+ + {e.device} + device push + + Pushes straight to your server — nothing transits a third party. + - connected - since {a.added} + {isPaused ? ( + paused + ) : ( + exporting + )} + {isPaused ? "—" : e.records + " records"} - )} - -
- ); - })} -
- - apps read through grants — never more than the grant behind them + + +
+ ); + })} +
+ + device flow · approve the code on this console +
-
- ); -} + ); + } -/* ─── Device exporters (de-souped) ─── */ + /* ─── Event subscriptions (de-souped) ─── */ -function ExportersView() { - const [paused, setPaused] = useState({}); - return ( -
- {RR2.exporters.map((e) => { - const isPaused = paused[e.device] != null ? paused[e.device] : e.status === "paused"; - return ( -
+ function SubscriptionsView() { + return ( +
+ {RR2.subscriptions.map((s) => ( +
- {e.device} - device push + {s.url} + + + {s.events.split(" · ").map((ev) => ( + + {ev} + + ))} - Pushes straight to your server — nothing transits a third party. - {isPaused - ? paused - : exporting} - {isPaused ? "—" : e.records + " records"} + {s.status} + -
- ); - })} -
- - device flow · approve the code on this console + ))} +
+ + fires on protocol events · grant.created · grant.revoked · run.failed +
-
- ); -} + ); + } -/* ─── Event subscriptions (de-souped) ─── */ + /* ─── Overview attention list (de-souped, action-bearing) ─── */ -function SubscriptionsView() { - return ( -
- {RR2.subscriptions.map((s) => ( -
- - {s.url} - - - {s.events.split(" · ").map((ev) => {ev})} - - - - {s.status} - + function AttentionList({ onGo }) { + const items = [ + { + sev: "fail", + name: "First Meridian can't sync", + say: "OFX session expired — transactions held at the Jun 10 cursor.", + action: "Reauthorize", + go: "syncs", + }, + { + sev: "warn", + name: "TaxPrep Co grant expiring", + say: "tax_docs.read · single use · still unused.", + meta: "26 h left", + go: "grants", + }, + { + sev: "warn", + name: "Backups not configured", + say: "Your copy deserves a copy — set a snapshot target.", + action: "Set up", + go: "deployment", + }, + ]; + return ( +
+ {items.map((it) => ( +
+ + + {it.name} - -
- ))} -
- - fires on protocol events · grant.created · grant.revoked · run.failed + {it.say} + + {it.action ? ( + + ) : ( + + )} + +
+ ))}
-
- ); -} - -/* ─── Overview attention list (de-souped, action-bearing) ─── */ - -function AttentionList({ onGo }) { - const items = [ - { sev: "fail", name: "First Meridian can't sync", say: "OFX session expired — transactions held at the Jun 10 cursor.", action: "Reauthorize", go: "syncs" }, - { sev: "warn", name: "TaxPrep Co grant expiring", say: "tax_docs.read · single use · still unused.", meta: "26 h left", go: "grants" }, - { sev: "warn", name: "Backups not configured", say: "Your copy deserves a copy — set a snapshot target.", action: "Set up", go: "deployment" }, - ]; - return ( -
- {items.map((it) => ( -
- - - {it.name} - - {it.say} - - {it.action - ? - : } - -
- ))} -
- ); -} + ); + } -Object.assign(window, { - RRSyncsView: SyncsView, - RRConnectView2: ConnectView, - RRExportersView2: ExportersView, - RRSubscriptionsView2: SubscriptionsView, - RRAttentionList: AttentionList, -}); + Object.assign(window, { + RRSyncsView: SyncsView, + RRConnectView2: ConnectView, + RRExportersView2: ExportersView, + RRSubscriptionsView2: SubscriptionsView, + RRAttentionList: AttentionList, + }); })(); diff --git a/docs/design-system/ink-carbon/project/recordroom/rr-views2.jsx b/docs/design-system/ink-carbon/project/recordroom/rr-views2.jsx index 4696ca64a..1d0ff83af 100644 --- a/docs/design-system/ink-carbon/project/recordroom/rr-views2.jsx +++ b/docs/design-system/ink-carbon/project/recordroom/rr-views2.jsx @@ -4,453 +4,528 @@ /* RECORDROOM — full-surface views: Overview, Explore, Traces, Runs, Schedules, Sources, Connect, Deployment, Exporters, Subscriptions, plus the command palette. Data from window.RR2. */ -;(() => { +(() => { + const { useState, useEffect, useRef } = React; + const RR2 = window.RR2; -const { useState, useEffect, useRef } = React; -const RR2 = window.RR2; - -function CopyId({ id }) { - const [done, setDone] = useState(false); - return ( - - ); -} - -function MiniHead({ title, action, onAction }) { - return ( -
-

{title}

- {action && } -
- ); -} - -/* ─── Traces ─── */ + function CopyId({ id }) { + const [done, setDone] = useState(false); + return ( + + ); + } -function TraceList({ selected, onSelect, limit, traces }) { - const rows = limit ? traces.slice(0, limit) : traces; - return ( -
-
- time - client - request - recs - fields - ruling + function MiniHead({ title, action, onAction }) { + return ( +
+

{title}

+ {action && ( + + )}
- {rows.map((tr) => ( - - ))} -
- ); -} + ); + } -function TraceDetail({ trace }) { - if (!trace) return null; - return ( -
-
-

{trace.decision === "deny" ? "Refused" : "Served"} in {trace.dur}

- -
-
-
- {trace.steps.map(([k, v], i) => ( -
- {k} - {v} -
- ))} + /* ─── Traces ─── */ + + function TraceList({ selected, onSelect, limit, traces }) { + const rows = limit ? traces.slice(0, limit) : traces; + return ( +
+
+ time + client + request + recs + fields + ruling
+ {rows.map((tr) => ( + + ))}
-
- - {trace.decision === "deny" ? "boundary held · " + trace.reason : "every response stays inside the grant"} - -
-
- ); -} - -function TracesView() { - const [sel, setSel] = useState(RR2.traces[3].id); - const trace = RR2.traces.find((t) => t.id === sel); - return ( -
- - -
- ); -} - -/* ─── Overview ─── */ + ); + } -function OverviewView({ pending, onReview, onGo, grantsSummary }) { - return ( -
- {pending && ( -
-
- - Access request · staged · waiting on you -

Atlas Mortgage asks to read 3 streams

- req_atlas_7f2k · purpose: mortgage_preapproval · nothing crosses until you decide -
- -
+ function TraceDetail({ trace }) { + if (!trace) return null; + return ( +
+
+

+ {trace.decision === "deny" ? "Refused" : "Served"} in {trace.dur} +

+
- )} -
-
10connections
-
34streams
-
48,120records
-
{grantsSummary}grants in effect
-
14reads this week
-
-
-
- onGo("traces")} title="Latest traces" /> - +
+
+ {trace.steps.map(([k, v], i) => ( +
+ {k} + {v} +
+ ))} +
-
- onGo("syncs")} title="Needs attention" /> - {window.RRAttentionList ? : null} +
+ + {trace.decision === "deny" ? "boundary held · " + trace.reason : "every response stays inside the grant"} +
-
- ); -} + ); + } + + function TracesView() { + const [sel, setSel] = useState(RR2.traces[3].id); + const trace = RR2.traces.find((t) => t.id === sel); + return ( +
+ + +
+ ); + } -/* ─── Explore ─── */ + /* ─── Overview ─── */ -function ExploreView() { - return ( -
-
-
- arrived - stream - record - id + function OverviewView({ pending, onReview, onGo, grantsSummary }) { + return ( +
+ {pending && ( +
+
+ + Access request · staged · waiting on you +

Atlas Mortgage asks to read 3 streams

+ + req_atlas_7f2k · purpose: mortgage_preapproval · nothing crosses until you decide + +
+ +
+
+ )} +
+
+ 10 + connections +
+
+ 34 + streams +
+
+ 48,120 + records +
+
+ {grantsSummary} + grants in effect +
+
+ 14 + reads this week +
- {RR2.feed.map((r) => ( -
- {r.t} - {r.stream} - {r.body} - {r.id} +
+
+ onGo("traces")} title="Latest traces" /> +
- ))} +
+ onGo("syncs")} title="Needs attention" /> + {window.RRAttentionList ? : null} +
+
-

- newest first · the feed is your own data arriving — nothing here has crossed to anyone -

-
- ); -} + ); + } -/* ─── Sources ─── */ + /* ─── Explore ─── */ -function SourcesView() { - return ( -
- {RR2.sources.map((s) => ( -
- {s.name} - {s.kind} · {s.streams} · last sync {s.last} - - {s.authOk - ? auth ok - : reauthorize} - {s.auth} - + function ExploreView() { + return ( +
+
+
+ arrived + stream + record + id +
+ {RR2.feed.map((r) => ( +
+ {r.t} + {r.stream} + {r.body} + {r.id} +
+ ))}
- ))} -
- - a source pushes into your streams · nothing leaves +

+ newest first · the feed is your own data arriving — nothing here has crossed to anyone +

-
- ); -} + ); + } -/* ─── Runs ─── */ + /* ─── Sources ─── */ -function RunsView() { - return ( -
-
-
- connector - stream · result - status - took - started -
- {RR2.runs.map((r) => ( -
- - {r.connector} - {r.id} + function SourcesView() { + return ( +
+ {RR2.sources.map((s) => ( +
+ {s.name} + + {s.kind} · {s.streams} · last sync {s.last} - - {r.stream} · {r.upserts} upserts · cursor {r.cursor}{r.note ? " · " + r.note : ""} + + {s.authOk ? ( + auth ok + ) : ( + reauthorize + )} + {s.auth} - {r.status === "ok" - ? ok - : failed} - {r.dur} - {r.started.slice(5)}
))} +
+ + a source pushes into your streams · nothing leaves +
-
- ); -} + ); + } -/* ─── Schedules ─── */ + /* ─── Runs ─── */ -function SchedulesView() { - return ( -
-
-
- stream - cadence - last run - next run -
- {RR2.schedules.map((s, i) => ( -
- - {s.stream} - {s.connector} - - {s.cadence} - {s.last === "ok" - ? last ok - : last failed} - next {s.next} + function RunsView() { + return ( +
+
+
+ connector + stream · result + status + took + started
- ))} -
-
- ); -} - -function ConnectView() { - return ( -
- {RR2.apps.map((a) => ( -
- {a.name} - {a.via} · {a.detail} - - {a.status === "connected" - ? connected - : pending code} - {a.status === "connected" ? "since " + a.added : "device flow"} - + {RR2.runs.map((r) => ( +
+ + {r.connector} + {r.id} + + + {r.stream} · {r.upserts} upserts · cursor {r.cursor} + {r.note ? " · " + r.note : ""} + + {r.status === "ok" ? ( + ok + ) : ( + failed + )} + {r.dur} + {r.started.slice(5)} +
+ ))}
- ))} -
- - apps read through grants — never more than the grant behind them
-
- ); -} + ); + } -/* ─── Deployment ─── */ + /* ─── Schedules ─── */ -function DeploymentView() { - return ( -
+ function SchedulesView() { + return (
- -
- {RR2.checks.map((c) => ( -
- {c.ok ? "ok" : "check"} - {c.name} - {c.detail} +
+
+ stream + cadence + last run + next run +
+ {RR2.schedules.map((s, i) => ( +
+ + {s.stream} + {s.connector} + + {s.cadence} + {s.last === "ok" ? ( + last ok + ) : ( + last failed + )} + next {s.next}
))}
-
- -
- {RR2.tokens.map((t) => ( -
- {t.label} - {t.id} · created {t.created} · for the operator and trusted local agents only - - active - last used {t.last} - + ); + } + + function ConnectView() { + return ( +
+ {RR2.apps.map((a) => ( +
+ {a.name} + + {a.via} · {a.detail} + + + {a.status === "connected" ? ( + connected + ) : ( + pending code + )} + {a.status === "connected" ? "since " + a.added : "device flow"} + +
+ ))} +
+ + apps read through grants — never more than the grant behind them +
+
+ ); + } + + /* ─── Deployment ─── */ + + function DeploymentView() { + return ( +
+
+ +
+ {RR2.checks.map((c) => ( +
+ {c.ok ? "ok" : "check"} + {c.name} + {c.detail} +
+ ))} +
+
+
+ +
+ {RR2.tokens.map((t) => ( +
+ {t.label} + + {t.id} · created {t.created} · for the operator and trusted local agents only + + + active + last used {t.last} + +
+ ))} +
+ + owner tokens bypass grants · issue sparingly
- ))} -
- - owner tokens bypass grants · issue sparingly
-
- ); -} + ); + } -/* ─── Device exporters / Event subscriptions ─── */ + /* ─── Device exporters / Event subscriptions ─── */ -function ExportersView() { - return ( -
- {RR2.exporters.map((e) => ( -
- {e.device} - pushes to your server — nothing transits a third party · {e.records} records - - {e.status === "ok" - ? exporting - : paused} - {e.last} - + function ExportersView() { + return ( +
+ {RR2.exporters.map((e) => ( +
+ {e.device} + + pushes to your server — nothing transits a third party · {e.records} records + + + {e.status === "ok" ? ( + exporting + ) : ( + paused + )} + {e.last} + +
+ ))} +
+ + device flow · approve the code on this console
- ))} -
- - device flow · approve the code on this console
-
- ); -} + ); + } -function SubscriptionsView() { - return ( -
- {RR2.subscriptions.map((s) => ( -
- {s.url} - {s.events} - - {s.status} - + function SubscriptionsView() { + return ( +
+ {RR2.subscriptions.map((s) => ( +
+ {s.url} + {s.events} + + {s.status} + +
+ ))} +
+ + fires on protocol events · grant.created · grant.revoked · run.failed
- ))} -
- - fires on protocol events · grant.created · grant.revoked · run.failed
-
- ); -} + ); + } -/* ─── Command palette ─── */ + /* ─── Command palette ─── */ -function CommandPalette({ open, onClose, items, recents = [], onExec }) { - const [q, setQ] = useState(""); - const [hl, setHl] = useState(0); - const inputRef = useRef(null); - useEffect(() => { if (open) { setQ(""); setHl(0); setTimeout(() => inputRef.current && inputRef.current.focus(), 30); } }, [open]); - if (!open) return null; - const ql = q.toLowerCase(); - function score(it) { - const hay = (it.label + " " + it.kind).toLowerCase(); - if (!hay.includes(ql)) return -1; - let s = it.label.toLowerCase().startsWith(ql) ? 3 : 1; - if (it.kind === "action") s += 0.5; - return s; - } - let filtered; - if (!q) { - const rec = recents.map((l) => items.find((i) => i.label === l)).filter(Boolean).map((i) => ({ ...i, kind: "recent" })); - const rest = items.filter((i) => !recents.includes(i.label)); - filtered = [...rec, ...rest].slice(0, 9); - } else { - filtered = items - .map((i) => [score(i), i]) - .filter(([s]) => s >= 0) - .sort((a, b) => b[0] - a[0]) - .map(([, i]) => i) - .slice(0, 9); - } - function choose(it) { - it.run(); - onExec && onExec(it.label); - onClose(); - } - function onKey(e) { - if (e.key === "Escape") onClose(); - if (e.key === "ArrowDown") { e.preventDefault(); setHl((h) => Math.min(h + 1, filtered.length - 1)); } - if (e.key === "ArrowUp") { e.preventDefault(); setHl((h) => Math.max(h - 1, 0)); } - if (e.key === "Enter" && filtered[hl]) choose(filtered[hl]); - } - return ( -
-
e.stopPropagation()}> - { setQ(e.target.value); setHl(0); }} - onKeyDown={onKey} - placeholder="Jump to a view, grant, stream, or action…" - ref={inputRef} - value={q} - /> -
- {filtered.length === 0 &&
Nothing matches — the record is honest about that.
} - {filtered.map((it, i) => ( - - ))} + function CommandPalette({ open, onClose, items, recents = [], onExec }) { + const [q, setQ] = useState(""); + const [hl, setHl] = useState(0); + const inputRef = useRef(null); + useEffect(() => { + if (open) { + setQ(""); + setHl(0); + setTimeout(() => inputRef.current && inputRef.current.focus(), 30); + } + }, [open]); + if (!open) return null; + const ql = q.toLowerCase(); + function score(it) { + const hay = (it.label + " " + it.kind).toLowerCase(); + if (!hay.includes(ql)) return -1; + let s = it.label.toLowerCase().startsWith(ql) ? 3 : 1; + if (it.kind === "action") s += 0.5; + return s; + } + let filtered; + if (!q) { + const rec = recents + .map((l) => items.find((i) => i.label === l)) + .filter(Boolean) + .map((i) => ({ ...i, kind: "recent" })); + const rest = items.filter((i) => !recents.includes(i.label)); + filtered = [...rec, ...rest].slice(0, 9); + } else { + filtered = items + .map((i) => [score(i), i]) + .filter(([s]) => s >= 0) + .sort((a, b) => b[0] - a[0]) + .map(([, i]) => i) + .slice(0, 9); + } + function choose(it) { + it.run(); + onExec && onExec(it.label); + onClose(); + } + function onKey(e) { + if (e.key === "Escape") onClose(); + if (e.key === "ArrowDown") { + e.preventDefault(); + setHl((h) => Math.min(h + 1, filtered.length - 1)); + } + if (e.key === "ArrowUp") { + e.preventDefault(); + setHl((h) => Math.max(h - 1, 0)); + } + if (e.key === "Enter" && filtered[hl]) choose(filtered[hl]); + } + return ( +
+
e.stopPropagation()}> + { + setQ(e.target.value); + setHl(0); + }} + onKeyDown={onKey} + placeholder="Jump to a view, grant, stream, or action…" + ref={inputRef} + value={q} + /> +
+ {filtered.length === 0 && ( +
Nothing matches — the record is honest about that.
+ )} + {filtered.map((it, i) => ( + + ))} +
-
- ); -} + ); + } -Object.assign(window, { - RRTracesView: TracesView, - RROverviewView: OverviewView, - RRSourcesView: SourcesView, - RRRunsView: RunsView, - RRSchedulesView: SchedulesView, - RRConnectView: ConnectView, - RRDeploymentView: DeploymentView, - RRExportersView: ExportersView, - RRSubscriptionsView: SubscriptionsView, - RRCommandPalette: CommandPalette, -}); + Object.assign(window, { + RRTracesView: TracesView, + RROverviewView: OverviewView, + RRSourcesView: SourcesView, + RRRunsView: RunsView, + RRSchedulesView: SchedulesView, + RRConnectView: ConnectView, + RRDeploymentView: DeploymentView, + RRExportersView: ExportersView, + RRSubscriptionsView: SubscriptionsView, + RRCommandPalette: CommandPalette, + }); })(); diff --git a/docs/design-system/ink-carbon/project/recordroom/sources.css b/docs/design-system/ink-carbon/project/recordroom/sources.css index aa5aec238..5b738d61c 100644 --- a/docs/design-system/ink-carbon/project/recordroom/sources.css +++ b/docs/design-system/ink-carbon/project/recordroom/sources.css @@ -13,7 +13,11 @@ } /* ── Instance list ── */ -.rr-s-list { display: flex; flex-direction: column; border-top: 1px solid var(--border-strong); } +.rr-s-list { + display: flex; + flex-direction: column; + border-top: 1px solid var(--border-strong); +} .rr-s-item { display: grid; grid-template-columns: minmax(0, 1fr) auto; @@ -28,8 +32,13 @@ transition: background var(--motion-state); min-width: 0; } -.rr-s-item:hover { background: var(--muted); } -.rr-s-item.is-on { background: var(--primary-tint); box-shadow: inset 0 0 0 1px var(--carbon-outline); } +.rr-s-item:hover { + background: var(--muted); +} +.rr-s-item.is-on { + background: var(--primary-tint); + box-shadow: inset 0 0 0 1px var(--carbon-outline); +} .rr-s-item__name { font-size: 0.875rem; font-weight: 600; @@ -37,7 +46,11 @@ overflow: hidden; text-overflow: ellipsis; } -.rr-s-item.is-revoked .rr-s-item__name { text-decoration: line-through; text-decoration-thickness: 1px; color: var(--muted-foreground); } +.rr-s-item.is-revoked .rr-s-item__name { + text-decoration: line-through; + text-decoration-thickness: 1px; + color: var(--muted-foreground); +} .rr-s-item__kind { font-family: var(--font-mono); font-size: 10px; @@ -54,16 +67,53 @@ text-overflow: ellipsis; grid-column: 1; } -.rr-s-item__flag { grid-row: 2; grid-column: 2; justify-self: end; } -.rr-s-item__ok { color: var(--success); font-size: 9px; } +.rr-s-item__flag { + grid-row: 2; + grid-column: 2; + justify-self: end; +} +.rr-s-item__ok { + color: var(--success); + font-size: 9px; +} /* ── Detail column ── */ -.rr-s-detail { display: flex; flex-direction: column; gap: 26px; min-width: 0; } -.rr-s-actions { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; } -.rr-s-cols { --cols: minmax(0, 1.1fr) 76px 110px 56px minmax(0, 1fr); } -.rr-s-stream { font-family: var(--font-mono); font-size: 0.8125rem; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.rr-s-cursor { font-family: var(--font-mono); font-size: 0.69rem; color: var(--muted-foreground); white-space: nowrap; } -.rr-s-readby { font-size: 0.75rem; color: var(--muted-foreground); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.rr-s-detail { + display: flex; + flex-direction: column; + gap: 26px; + min-width: 0; +} +.rr-s-actions { + display: flex; + gap: 8px; + align-items: center; + flex-wrap: wrap; +} +.rr-s-cols { + --cols: minmax(0, 1.1fr) 76px 110px 56px minmax(0, 1fr); +} +.rr-s-stream { + font-family: var(--font-mono); + font-size: 0.8125rem; + font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.rr-s-cursor { + font-family: var(--font-mono); + font-size: 0.69rem; + color: var(--muted-foreground); + white-space: nowrap; +} +.rr-s-readby { + font-size: 0.75rem; + color: var(--muted-foreground); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} .rr-s-note { margin: 10px 0 0; font-size: 11.5px; @@ -105,7 +155,14 @@ } /* Connected records (Explore sheet) */ -.rr-x-rel { margin-top: 10px; border-top: 1px solid var(--border); padding-top: 10px; display: flex; flex-direction: column; gap: 2px; } +.rr-x-rel { + margin-top: 10px; + border-top: 1px solid var(--border); + padding-top: 10px; + display: flex; + flex-direction: column; + gap: 2px; +} .rr-x-rel__row { display: grid; grid-template-columns: 110px minmax(0, 1fr); @@ -119,7 +176,14 @@ align-items: baseline; min-width: 0; } -.rr-x-rel__k { font-family: var(--font-mono); font-size: 10px; color: var(--muted-foreground); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.rr-x-rel__k { + font-family: var(--font-mono); + font-size: 10px; + color: var(--muted-foreground); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} .rr-x-rel__v { font-size: 12px; font-weight: 500; @@ -128,18 +192,46 @@ overflow: hidden; text-overflow: ellipsis; } -.rr-x-rel__row:hover .rr-x-rel__v { text-decoration: underline; text-underline-offset: 3px; } +.rr-x-rel__row:hover .rr-x-rel__v { + text-decoration: underline; + text-underline-offset: 3px; +} /* ── Responsive ── */ @media (max-width: 980px) { - .rr-s { grid-template-columns: minmax(0, 1fr); } + .rr-s { + grid-template-columns: minmax(0, 1fr); + } } @media (max-width: 860px) { - .rr-s-cols .pdpp-data-row { grid-template-columns: minmax(0, 1fr) auto; gap: 2px 12px; padding: 11px 0; } - .rr-s-cols .rr-s-stream { grid-row: 1; grid-column: 1; } - .rr-s-cols .pdpp-data-row__meta { grid-row: 1; grid-column: 2; } - .rr-s-cols .rr-s-cursor { grid-row: 2; grid-column: 1; } - .rr-s-cols .rr-s-cursor + .rr-s-cursor { grid-row: 2; grid-column: 2; text-align: right; } - .rr-s-cols .rr-s-readby { grid-row: 3; grid-column: 1 / -1; } - .rr-x-compiled { flex-direction: column; gap: 2px; } + .rr-s-cols .pdpp-data-row { + grid-template-columns: minmax(0, 1fr) auto; + gap: 2px 12px; + padding: 11px 0; + } + .rr-s-cols .rr-s-stream { + grid-row: 1; + grid-column: 1; + } + .rr-s-cols .pdpp-data-row__meta { + grid-row: 1; + grid-column: 2; + } + .rr-s-cols .rr-s-cursor { + grid-row: 2; + grid-column: 1; + } + .rr-s-cols .rr-s-cursor + .rr-s-cursor { + grid-row: 2; + grid-column: 2; + text-align: right; + } + .rr-s-cols .rr-s-readby { + grid-row: 3; + grid-column: 1 / -1; + } + .rr-x-compiled { + flex-direction: column; + gap: 2px; + } } diff --git a/docs/design-system/ink-carbon/project/recordroom/syncs.css b/docs/design-system/ink-carbon/project/recordroom/syncs.css index 671fab052..f4f40c3a7 100644 --- a/docs/design-system/ink-carbon/project/recordroom/syncs.css +++ b/docs/design-system/ink-carbon/project/recordroom/syncs.css @@ -6,7 +6,11 @@ /* RECORDROOM — Syncs + structured ops rows. Health-first, no soup. */ /* ─── Syncs ─── */ -.rr-sync { display: flex; flex-direction: column; gap: 26px; } +.rr-sync { + display: flex; + flex-direction: column; + gap: 26px; +} .rr-sync-health { display: grid; @@ -21,7 +25,9 @@ padding: 14px 18px; border-left: 1px solid var(--border); } -.rr-sync-health__stat:first-child { border-left: 0; } +.rr-sync-health__stat:first-child { + border-left: 0; +} .rr-sync-health__v { font-family: var(--font-mono); font-size: 1.5rem; @@ -30,10 +36,22 @@ letter-spacing: -0.01em; line-height: 1.05; } -.rr-sync-health__stat.is-warn .rr-sync-health__v { color: var(--warning); } -.rr-sync-health__k { font-size: 0.7rem; color: var(--muted-foreground); letter-spacing: 0.02em; } -.rr-sync-health__stat--note { justify-content: center; } -.rr-sync-health__note { font-size: 0.72rem; line-height: 1.5; color: var(--muted-foreground); } +.rr-sync-health__stat.is-warn .rr-sync-health__v { + color: var(--warning); +} +.rr-sync-health__k { + font-size: 0.7rem; + color: var(--muted-foreground); + letter-spacing: 0.02em; +} +.rr-sync-health__stat--note { + justify-content: center; +} +.rr-sync-health__note { + font-size: 0.72rem; + line-height: 1.5; + color: var(--muted-foreground); +} /* The failure as a card with an action — not a row */ .rr-fix { @@ -46,13 +64,34 @@ border: 1px solid var(--warning); box-shadow: inset 3px 0 0 var(--warning); } -.rr-fix__body { display: flex; flex-direction: column; gap: 5px; min-width: 0; } -.rr-fix__title { font-size: 0.95rem; font-weight: 700; margin: 0; } -.rr-fix__expl { font-size: 0.8rem; line-height: 1.55; color: var(--muted-foreground); margin: 0; max-width: 70ch; } -.rr-fix__act { align-self: center; } +.rr-fix__body { + display: flex; + flex-direction: column; + gap: 5px; + min-width: 0; +} +.rr-fix__title { + font-size: 0.95rem; + font-weight: 700; + margin: 0; +} +.rr-fix__expl { + font-size: 0.8rem; + line-height: 1.55; + color: var(--muted-foreground); + margin: 0; + max-width: 70ch; +} +.rr-fix__act { + align-self: center; +} /* Per-connector group */ -.rr-sync-group { display: flex; flex-direction: column; gap: 8px; } +.rr-sync-group { + display: flex; + flex-direction: column; + gap: 8px; +} .rr-sync-group__head { display: flex; align-items: baseline; @@ -60,13 +99,21 @@ padding-bottom: 2px; } .rr-sync-group__dot { - width: 7px; height: 7px; + width: 7px; + height: 7px; align-self: center; flex-shrink: 0; } -.rr-sync-group__dot.is-ok { background: var(--success); } -.rr-sync-group__dot.is-fail { background: var(--warning); } -.rr-sync-group__name { font-size: 0.9rem; font-weight: 700; } +.rr-sync-group__dot.is-ok { + background: var(--success); +} +.rr-sync-group__dot.is-fail { + background: var(--warning); +} +.rr-sync-group__name { + font-size: 0.9rem; + font-weight: 700; +} .rr-sync-group__cin { font-family: var(--font-mono); font-size: 0.62rem; @@ -77,9 +124,16 @@ text-overflow: ellipsis; white-space: nowrap; } -.rr-sync-group__count { font-family: var(--font-mono); font-size: 0.66rem; color: var(--muted-foreground); white-space: nowrap; } +.rr-sync-group__count { + font-family: var(--font-mono); + font-size: 0.66rem; + color: var(--muted-foreground); + white-space: nowrap; +} -.rr-cols-sync { --cols: minmax(0, 1.3fr) minmax(96px, 0.8fr) 78px minmax(0, 1.1fr) 110px; } +.rr-cols-sync { + --cols: minmax(0, 1.3fr) minmax(96px, 0.8fr) 78px minmax(0, 1.1fr) 110px; +} .rr-sync-row { display: grid; @@ -97,8 +151,12 @@ transition: background var(--motion-state); min-width: 0; } -.rr-sync-row:hover { background: var(--muted); } -.rr-sync-row.is-open { background: var(--muted); } +.rr-sync-row:hover { + background: var(--muted); +} +.rr-sync-row.is-open { + background: var(--muted); +} .rr-sync-row__stream { font-family: var(--font-mono); font-size: 0.8125rem; @@ -107,7 +165,12 @@ overflow: hidden; text-overflow: ellipsis; } -.rr-sync-row__cadence { font-family: var(--font-mono); font-size: 0.69rem; color: var(--muted-foreground); white-space: nowrap; } +.rr-sync-row__cadence { + font-family: var(--font-mono); + font-size: 0.69rem; + color: var(--muted-foreground); + white-space: nowrap; +} .rr-sync-row__delta { display: flex; flex-direction: column; @@ -116,9 +179,20 @@ font-weight: 500; min-width: 0; } -.rr-sync-row__delta.is-quiet { color: var(--muted-foreground); font-weight: 400; } -.rr-sync-row__delta.is-failed { color: var(--warning); font-weight: 600; } -.rr-sync-row__when { font-family: var(--font-mono); font-size: 0.62rem; color: var(--muted-foreground); font-weight: 400; } +.rr-sync-row__delta.is-quiet { + color: var(--muted-foreground); + font-weight: 400; +} +.rr-sync-row__delta.is-failed { + color: var(--warning); + font-weight: 600; +} +.rr-sync-row__when { + font-family: var(--font-mono); + font-size: 0.62rem; + color: var(--muted-foreground); + font-weight: 400; +} .rr-sync-row__next { font-family: var(--font-mono); font-size: 0.69rem; @@ -127,16 +201,26 @@ white-space: nowrap; font-variant-numeric: tabular-nums; } -.rr-sync-row.is-failed .rr-sync-row__next { color: var(--warning); } +.rr-sync-row.is-failed .rr-sync-row__next { + color: var(--warning); +} /* Rhythm — recent run history, oldest→newest */ -.rr-rhythm { display: inline-flex; gap: 3px; align-items: center; } +.rr-rhythm { + display: inline-flex; + gap: 3px; + align-items: center; +} .rr-rhythm__tick { - width: 6px; height: 14px; + width: 6px; + height: 14px; background: var(--success); opacity: 0.7; } -.rr-rhythm__tick.is-fail { background: var(--warning); opacity: 1; } +.rr-rhythm__tick.is-fail { + background: var(--warning); + opacity: 1; +} /* Expanded run detail */ .rr-sync-detail { @@ -148,12 +232,28 @@ align-items: flex-start; animation: slide-up var(--duration-base) var(--ease-enter) both; } -.rr-sync-detail__kv { display: grid; grid-template-columns: 90px 1fr; gap: 4px 14px; } -.rr-sync-detail__k { font-size: 0.68rem; color: var(--muted-foreground); text-transform: uppercase; letter-spacing: 0.06em; } -.rr-sync-detail__v { font-family: var(--font-mono); font-size: 0.74rem; } +.rr-sync-detail__kv { + display: grid; + grid-template-columns: 90px 1fr; + gap: 4px 14px; +} +.rr-sync-detail__k { + font-size: 0.68rem; + color: var(--muted-foreground); + text-transform: uppercase; + letter-spacing: 0.06em; +} +.rr-sync-detail__v { + font-family: var(--font-mono); + font-size: 0.74rem; +} /* ─── Structured ops rows (Connect / Exporters / Subscriptions / Attention) ─── */ -.rr-ops { display: flex; flex-direction: column; border-top: 1px solid var(--border-strong); } +.rr-ops { + display: flex; + flex-direction: column; + border-top: 1px solid var(--border-strong); +} .rr-op { display: grid; grid-template-columns: minmax(0, 0.95fr) minmax(0, 1.25fr) auto; @@ -163,7 +263,12 @@ border-bottom: 1px solid var(--border); min-width: 0; } -.rr-op__lead { display: flex; align-items: baseline; gap: 9px; min-width: 0; } +.rr-op__lead { + display: flex; + align-items: baseline; + gap: 9px; + min-width: 0; +} .rr-op__name { font-size: 0.875rem; font-weight: 600; @@ -171,7 +276,11 @@ overflow: hidden; text-overflow: ellipsis; } -.rr-op__name--mono { font-family: var(--font-mono); font-size: 0.78rem; font-weight: 500; } +.rr-op__name--mono { + font-family: var(--font-mono); + font-size: 0.78rem; + font-weight: 500; +} .rr-op__tag { font-family: var(--font-mono); font-size: 0.6rem; @@ -181,8 +290,18 @@ white-space: nowrap; flex-shrink: 0; } -.rr-op__say { font-size: 0.78rem; color: var(--muted-foreground); line-height: 1.5; min-width: 0; } -.rr-op__events { display: flex; flex-wrap: wrap; gap: 6px; min-width: 0; } +.rr-op__say { + font-size: 0.78rem; + color: var(--muted-foreground); + line-height: 1.5; + min-width: 0; +} +.rr-op__events { + display: flex; + flex-wrap: wrap; + gap: 6px; + min-width: 0; +} .rr-op__event { font-family: var(--font-mono); font-size: 0.64rem; @@ -191,10 +310,28 @@ box-shadow: inset 0 0 0 1px var(--border); white-space: nowrap; } -.rr-op__side { justify-self: end; display: flex; align-items: center; gap: 10px; } -.rr-op__settled { display: flex; align-items: center; gap: 9px; } -.rr-op__meta { font-family: var(--font-mono); font-size: 0.64rem; color: var(--muted-foreground); white-space: nowrap; } -.rr-op__pending { display: flex; align-items: center; gap: 9px; } +.rr-op__side { + justify-self: end; + display: flex; + align-items: center; + gap: 10px; +} +.rr-op__settled { + display: flex; + align-items: center; + gap: 9px; +} +.rr-op__meta { + font-family: var(--font-mono); + font-size: 0.64rem; + color: var(--muted-foreground); + white-space: nowrap; +} +.rr-op__pending { + display: flex; + align-items: center; + gap: 9px; +} .rr-op__code { font-family: var(--font-mono); font-size: 0.8rem; @@ -204,30 +341,85 @@ background: var(--primary-tint); color: var(--primary); } -.rr-op__test { font-size: 0.7rem; } +.rr-op__test { + font-size: 0.7rem; +} /* Attention row severity */ -.rr-op__sev { width: 7px; height: 7px; align-self: center; flex-shrink: 0; } -.rr-op__sev--fail { background: var(--warning); } -.rr-op__sev--warn { background: var(--warning); opacity: 0.5; } -.rr-op--attn.is-fail { box-shadow: inset 2px 0 0 var(--warning); padding-left: 12px; } +.rr-op__sev { + width: 7px; + height: 7px; + align-self: center; + flex-shrink: 0; +} +.rr-op__sev--fail { + background: var(--warning); +} +.rr-op__sev--warn { + background: var(--warning); + opacity: 0.5; +} +.rr-op--attn.is-fail { + box-shadow: inset 2px 0 0 var(--warning); + padding-left: 12px; +} /* ─── Responsive ─── */ @media (max-width: 920px) { - .rr-op { grid-template-columns: minmax(0, 1fr) auto; } - .rr-op__say, .rr-op__events { grid-column: 1 / -1; grid-row: 2; } - .rr-op__side { grid-row: 1; grid-column: 2; } - .rr-fix { grid-template-columns: auto minmax(0, 1fr); } - .rr-fix__act { grid-column: 2; justify-self: start; } + .rr-op { + grid-template-columns: minmax(0, 1fr) auto; + } + .rr-op__say, + .rr-op__events { + grid-column: 1 / -1; + grid-row: 2; + } + .rr-op__side { + grid-row: 1; + grid-column: 2; + } + .rr-fix { + grid-template-columns: auto minmax(0, 1fr); + } + .rr-fix__act { + grid-column: 2; + justify-self: start; + } } @media (max-width: 760px) { - .rr-sync-health { grid-auto-flow: row; grid-auto-columns: auto; } - .rr-sync-health__stat { border-left: 0; border-top: 1px solid var(--border); } - .rr-sync-health__stat:first-child { border-top: 0; } - .rr-cols-sync { --cols: minmax(0, 1fr) auto; } - .rr-sync-row { grid-template-columns: minmax(0, 1fr) auto; gap: 4px 12px; } - .rr-sync-row__cadence { grid-row: 2; grid-column: 1; } - .rr-rhythm { grid-row: 1; grid-column: 2; justify-self: end; } - .rr-sync-row__delta { grid-row: 3; grid-column: 1 / -1; } - .rr-sync-row__next { grid-row: 2; grid-column: 2; } + .rr-sync-health { + grid-auto-flow: row; + grid-auto-columns: auto; + } + .rr-sync-health__stat { + border-left: 0; + border-top: 1px solid var(--border); + } + .rr-sync-health__stat:first-child { + border-top: 0; + } + .rr-cols-sync { + --cols: minmax(0, 1fr) auto; + } + .rr-sync-row { + grid-template-columns: minmax(0, 1fr) auto; + gap: 4px 12px; + } + .rr-sync-row__cadence { + grid-row: 2; + grid-column: 1; + } + .rr-rhythm { + grid-row: 1; + grid-column: 2; + justify-self: end; + } + .rr-sync-row__delta { + grid-row: 3; + grid-column: 1 / -1; + } + .rr-sync-row__next { + grid-row: 2; + grid-column: 2; + } } diff --git a/docs/design-system/ink-carbon/project/recordroom/tweaks-panel.jsx b/docs/design-system/ink-carbon/project/recordroom/tweaks-panel.jsx index bc2112a16..503a91941 100644 --- a/docs/design-system/ink-carbon/project/recordroom/tweaks-panel.jsx +++ b/docs/design-system/ink-carbon/project/recordroom/tweaks-panel.jsx @@ -179,13 +179,12 @@ function useTweaks(defaults) { // useState-style call doesn't write a "[object Object]" key into the persisted // JSON block. const setTweak = React.useCallback((keyOrEdits, val) => { - const edits = typeof keyOrEdits === 'object' && keyOrEdits !== null - ? keyOrEdits : { [keyOrEdits]: val }; + const edits = typeof keyOrEdits === "object" && keyOrEdits !== null ? keyOrEdits : { [keyOrEdits]: val }; setValues((prev) => ({ ...prev, ...edits })); - window.parent.postMessage({ type: '__edit_mode_set_keys', edits }, '*'); + window.parent.postMessage({ type: "__edit_mode_set_keys", edits }, "*"); // Same-window signal so in-page listeners (deck-stage rail thumbnails) // can react — the parent message only reaches the host, not peers. - window.dispatchEvent(new CustomEvent('tweakchange', { detail: edits })); + window.dispatchEvent(new CustomEvent("tweakchange", { detail: edits })); }, []); return [values, setTweak]; } @@ -197,7 +196,7 @@ function useTweaks(defaults) { // The close button posts __edit_mode_dismissed so the host's toolbar toggle // flips off in lockstep; the host echoes __deactivate_edit_mode back which // is what actually hides the panel. -function TweaksPanel({ title = 'Tweaks', children }) { +function TweaksPanel({ title = "Tweaks", children }) { const [open, setOpen] = React.useState(false); const dragRef = React.useRef(null); const offsetRef = React.useRef({ x: 16, y: 16 }); @@ -206,23 +205,24 @@ function TweaksPanel({ title = 'Tweaks', children }) { const clampToViewport = React.useCallback(() => { const panel = dragRef.current; if (!panel) return; - const w = panel.offsetWidth, h = panel.offsetHeight; + const w = panel.offsetWidth, + h = panel.offsetHeight; const maxRight = Math.max(PAD, window.innerWidth - w - PAD); const maxBottom = Math.max(PAD, window.innerHeight - h - PAD); offsetRef.current = { x: Math.min(maxRight, Math.max(PAD, offsetRef.current.x)), y: Math.min(maxBottom, Math.max(PAD, offsetRef.current.y)), }; - panel.style.right = offsetRef.current.x + 'px'; - panel.style.bottom = offsetRef.current.y + 'px'; + panel.style.right = offsetRef.current.x + "px"; + panel.style.bottom = offsetRef.current.y + "px"; }, []); React.useEffect(() => { if (!open) return; clampToViewport(); - if (typeof ResizeObserver === 'undefined') { - window.addEventListener('resize', clampToViewport); - return () => window.removeEventListener('resize', clampToViewport); + if (typeof ResizeObserver === "undefined") { + window.addEventListener("resize", clampToViewport); + return () => window.removeEventListener("resize", clampToViewport); } const ro = new ResizeObserver(clampToViewport); ro.observe(document.documentElement); @@ -232,24 +232,25 @@ function TweaksPanel({ title = 'Tweaks', children }) { React.useEffect(() => { const onMsg = (e) => { const t = e?.data?.type; - if (t === '__activate_edit_mode') setOpen(true); - else if (t === '__deactivate_edit_mode') setOpen(false); + if (t === "__activate_edit_mode") setOpen(true); + else if (t === "__deactivate_edit_mode") setOpen(false); }; - window.addEventListener('message', onMsg); - window.parent.postMessage({ type: '__edit_mode_available' }, '*'); - return () => window.removeEventListener('message', onMsg); + window.addEventListener("message", onMsg); + window.parent.postMessage({ type: "__edit_mode_available" }, "*"); + return () => window.removeEventListener("message", onMsg); }, []); const dismiss = () => { setOpen(false); - window.parent.postMessage({ type: '__edit_mode_dismissed' }, '*'); + window.parent.postMessage({ type: "__edit_mode_dismissed" }, "*"); }; const onDragStart = (e) => { const panel = dragRef.current; if (!panel) return; const r = panel.getBoundingClientRect(); - const sx = e.clientX, sy = e.clientY; + const sx = e.clientX, + sy = e.clientY; const startRight = window.innerWidth - r.right; const startBottom = window.innerHeight - r.bottom; const move = (ev) => { @@ -260,28 +261,35 @@ function TweaksPanel({ title = 'Tweaks', children }) { clampToViewport(); }; const up = () => { - window.removeEventListener('mousemove', move); - window.removeEventListener('mouseup', up); + window.removeEventListener("mousemove", move); + window.removeEventListener("mouseup", up); }; - window.addEventListener('mousemove', move); - window.addEventListener('mouseup', up); + window.addEventListener("mousemove", move); + window.addEventListener("mouseup", up); }; if (!open) return null; return ( <> -
+
{title} - -
-
- {children} +
+
{children}
); @@ -300,7 +308,7 @@ function TweakSection({ label, children }) { function TweakRow({ label, value, children, inline = false }) { return ( -
+
{label} {value != null && {value}} @@ -312,11 +320,18 @@ function TweakRow({ label, value, children, inline = false }) { // ── Controls ──────────────────────────────────────────────────────────────── -function TweakSlider({ label, value, min = 0, max = 100, step = 1, unit = '', onChange }) { +function TweakSlider({ label, value, min = 0, max = 100, step = 1, unit = "", onChange }) { return ( - onChange(Number(e.target.value))} /> + onChange(Number(e.target.value))} + /> ); } @@ -324,10 +339,19 @@ function TweakSlider({ label, value, min = 0, max = 100, step = 1, unit = '', on function TweakToggle({ label, value, onChange }) { return (
-
{label}
- +
+ {label} +
+
); } @@ -345,21 +369,23 @@ function TweakRadio({ label, value, options, onChange }) { // to its own padding, and 11.5px system-ui averages ~6.3px/char — so 2 // options fit ~16 chars each, 3 fit ~10. Past that (or >3 options), fall // back to a dropdown rather than wrap. - const labelLen = (o) => String(typeof o === 'object' ? o.label : o).length; + const labelLen = (o) => String(typeof o === "object" ? o.label : o).length; const maxLen = options.reduce((m, o) => Math.max(m, labelLen(o)), 0); const fitsAsSegments = maxLen <= ({ 2: 16, 3: 10 }[options.length] ?? 0); if (!fitsAsSegments) { // onChange(e.target.value)}> {options.map((o) => { - const v = typeof o === 'object' ? o.value : o; - const l = typeof o === 'object' ? o.label : o; - return ; + const v = typeof o === "object" ? o.value : o; + const l = typeof o === "object" ? o.label : o; + return ( + + ); })} @@ -421,13 +456,18 @@ function TweakSelect({ label, value, options, onChange }) { function TweakText({ label, value, placeholder, onChange }) { return ( - onChange(e.target.value)} /> + onChange(e.target.value)} + /> ); } -function TweakNumber({ label, value, min, max, step = 1, unit = '', onChange }) { +function TweakNumber({ label, value, min, max, step = 1, unit = "", onChange }) { const clamp = (n) => { if (min != null && n < min) return min; if (max != null && n > max) return max; @@ -437,7 +477,7 @@ function TweakNumber({ label, value, min, max, step = 1, unit = '', onChange }) const onScrubStart = (e) => { e.preventDefault(); startRef.current = { x: e.clientX, val: value }; - const decimals = (String(step).split('.')[1] || '').length; + const decimals = (String(step).split(".")[1] || "").length; const move = (ev) => { const dx = ev.clientX - startRef.current.x; const raw = startRef.current.val + dx * step; @@ -445,17 +485,25 @@ function TweakNumber({ label, value, min, max, step = 1, unit = '', onChange }) onChange(clamp(Number(snapped.toFixed(decimals)))); }; const up = () => { - window.removeEventListener('pointermove', move); - window.removeEventListener('pointerup', up); + window.removeEventListener("pointermove", move); + window.removeEventListener("pointerup", up); }; - window.addEventListener('pointermove', move); - window.addEventListener('pointerup', up); + window.addEventListener("pointermove", move); + window.addEventListener("pointerup", up); }; return (
- {label} - onChange(clamp(Number(e.target.value)))} /> + + {label} + + onChange(clamp(Number(e.target.value)))} + /> {unit && {unit}}
); @@ -465,19 +513,26 @@ function TweakNumber({ label, value, min, max, step = 1, unit = '', onChange }) // read on both #111 and #fafafa without per-option configuration. Hex input // only (#rgb / #rrggbb); named or rgb()/hsl() colors fall through to "light". function __twkIsLight(hex) { - const h = String(hex).replace('#', ''); - const x = h.length === 3 ? h.replace(/./g, (c) => c + c) : h.padEnd(6, '0'); + const h = String(hex).replace("#", ""); + const x = h.length === 3 ? h.replace(/./g, (c) => c + c) : h.padEnd(6, "0"); const n = parseInt(x.slice(0, 6), 16); if (Number.isNaN(n)) return true; - const r = (n >> 16) & 255, g = (n >> 8) & 255, b = n & 255; + const r = (n >> 16) & 255, + g = (n >> 8) & 255, + b = n & 255; return r * 299 + g * 587 + b * 114 > 148000; } const __TwkCheck = ({ light }) => ( ); @@ -491,9 +546,10 @@ function TweakColor({ label, value, options, onChange }) { if (!options || !options.length) { return (
-
{label}
- onChange(e.target.value)} /> +
+ {label} +
+ onChange(e.target.value)} />
); } @@ -511,14 +567,23 @@ function TweakColor({ label, value, options, onChange }) { const sup = rest.slice(0, 4); const on = key(o) === cur; return ( - + ); } Object.assign(window, { - useTweaks, TweaksPanel, TweakSection, TweakRow, - TweakSlider, TweakToggle, TweakRadio, TweakSelect, - TweakText, TweakNumber, TweakColor, TweakButton, + useTweaks, + TweaksPanel, + TweakSection, + TweakRow, + TweakSlider, + TweakToggle, + TweakRadio, + TweakSelect, + TweakText, + TweakNumber, + TweakColor, + TweakButton, }); diff --git a/docs/design-system/ink-carbon/project/recordroom/watcher.css b/docs/design-system/ink-carbon/project/recordroom/watcher.css index bb07aa965..54d20e172 100644 --- a/docs/design-system/ink-carbon/project/recordroom/watcher.css +++ b/docs/design-system/ink-carbon/project/recordroom/watcher.css @@ -18,7 +18,9 @@ grid-template-columns: 340px minmax(0, 1fr); align-items: stretch; } -.wp * { box-sizing: border-box; } +.wp * { + box-sizing: border-box; +} /* ─── Left: the watcher ─── */ .wp-who { @@ -40,7 +42,9 @@ cursor: pointer; letter-spacing: 0.02em; } -.wp-back:hover { color: var(--foreground); } +.wp-back:hover { + color: var(--foreground); +} .wp-mono-tile { width: 56px; height: 56px; @@ -64,9 +68,24 @@ border: 1px solid var(--carbon-outline); z-index: -1; } -.wp-who__name { font-size: 25px; font-weight: 800; letter-spacing: -0.02em; line-height: 1.1; margin: 0; } -.wp-who__role { font-family: var(--font-mono); font-size: 11.5px; color: var(--primary); letter-spacing: 0.02em; } -.wp-facts { display: flex; flex-direction: column; margin-top: 4px; } +.wp-who__name { + font-size: 25px; + font-weight: 800; + letter-spacing: -0.02em; + line-height: 1.1; + margin: 0; +} +.wp-who__role { + font-family: var(--font-mono); + font-size: 11.5px; + color: var(--primary); + letter-spacing: 0.02em; +} +.wp-facts { + display: flex; + flex-direction: column; + margin-top: 4px; +} .wp-fact { display: grid; grid-template-columns: 88px 1fr; @@ -75,9 +94,19 @@ border-top: 1px solid var(--border); align-items: baseline; } -.wp-fact__k { font-size: 11.5px; font-weight: 500; color: var(--muted-foreground); } -.wp-fact__v { font-family: var(--font-mono); font-size: 12.5px; font-variant-numeric: tabular-nums; } -.wp-who__spacer { flex: 1; } +.wp-fact__k { + font-size: 11.5px; + font-weight: 500; + color: var(--muted-foreground); +} +.wp-fact__v { + font-family: var(--font-mono); + font-size: 12.5px; + font-variant-numeric: tabular-nums; +} +.wp-who__spacer { + flex: 1; +} .wp-pulse { border-top: 1px solid var(--border); padding-top: 16px; @@ -85,11 +114,33 @@ flex-direction: column; gap: 10px; } -.wp-pulse__label { font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted-foreground); } -.wp-spark { display: flex; align-items: flex-end; gap: 3px; height: 34px; } -.wp-spark__bar { flex: 1; background: var(--primary); opacity: 0.55; min-height: 2px; } -.wp-spark__bar.is-now { opacity: 1; } -.wp-pulse__note { font-family: var(--font-mono); font-size: 11px; color: var(--muted-foreground); } +.wp-pulse__label { + font-family: var(--font-mono); + font-size: 10px; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--muted-foreground); +} +.wp-spark { + display: flex; + align-items: flex-end; + gap: 3px; + height: 34px; +} +.wp-spark__bar { + flex: 1; + background: var(--primary); + opacity: 0.55; + min-height: 2px; +} +.wp-spark__bar.is-now { + opacity: 1; +} +.wp-pulse__note { + font-family: var(--font-mono); + font-size: 11px; + color: var(--muted-foreground); +} .wp-revoke { font-family: var(--font-sans); font-size: 12.5px; @@ -102,11 +153,22 @@ cursor: pointer; margin-top: 4px; } -.wp-revoke:hover { background: var(--destructive-tint); } +.wp-revoke:hover { + background: var(--destructive-tint); +} /* ─── Right: the portrait ─── */ -.wp-body { padding: 40px 56px 56px; max-width: 920px; } -.wp-eyebrow { font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--primary); } +.wp-body { + padding: 40px 56px 56px; + max-width: 920px; +} +.wp-eyebrow { + font-family: var(--font-mono); + font-size: 11px; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--primary); +} .wp-thesis { font-size: 34px; font-weight: 800; @@ -116,9 +178,14 @@ max-width: 18ch; text-wrap: balance; } -.wp-thesis em { font-style: normal; color: var(--primary); } +.wp-thesis em { + font-style: normal; + color: var(--primary); +} -.wp-section { margin-top: 40px; } +.wp-section { + margin-top: 40px; +} .wp-section__head { display: flex; align-items: baseline; @@ -126,11 +193,24 @@ padding-bottom: 8px; border-bottom: 1px solid var(--border-strong); } -.wp-section__title { font-size: 13px; font-weight: 700; letter-spacing: 0.01em; margin: 0; } -.wp-section__count { font-family: var(--font-mono); font-size: 11px; color: var(--muted-foreground); white-space: nowrap; } +.wp-section__title { + font-size: 13px; + font-weight: 700; + letter-spacing: 0.01em; + margin: 0; +} +.wp-section__count { + font-family: var(--font-mono); + font-size: 11px; + color: var(--muted-foreground); + white-space: nowrap; +} /* An inference — what a field BECOMES in their hands */ -.wp-know { display: flex; flex-direction: column; } +.wp-know { + display: flex; + flex-direction: column; +} .wp-infer { display: grid; grid-template-columns: 1fr auto; @@ -139,13 +219,35 @@ border-bottom: 1px solid var(--border); align-items: baseline; } -.wp-infer__claim { font-size: 18px; font-weight: 500; line-height: 1.4; letter-spacing: -0.01em; } -.wp-infer__claim b { font-weight: 700; } -.wp-infer__src { font-family: var(--font-mono); font-size: 10.5px; color: var(--muted-foreground); grid-column: 1; } -.wp-infer__from { font-family: var(--font-mono); font-size: 10px; color: var(--muted-foreground); white-space: nowrap; text-align: right; } +.wp-infer__claim { + font-size: 18px; + font-weight: 500; + line-height: 1.4; + letter-spacing: -0.01em; +} +.wp-infer__claim b { + font-weight: 700; +} +.wp-infer__src { + font-family: var(--font-mono); + font-size: 10.5px; + color: var(--muted-foreground); + grid-column: 1; +} +.wp-infer__from { + font-family: var(--font-mono); + font-size: 10px; + color: var(--muted-foreground); + white-space: nowrap; + text-align: right; +} /* What stays yours — equal weight, calm */ -.wp-keep { display: grid; grid-template-columns: 1fr 1fr; gap: 0 32px; } +.wp-keep { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0 32px; +} .wp-kept { padding: 15px 0; border-bottom: 1px solid var(--border); @@ -153,11 +255,29 @@ flex-direction: column; gap: 3px; } -.wp-kept__what { font-size: 16px; font-weight: 500; color: var(--foreground); } -.wp-kept__why { font-family: var(--font-mono); font-size: 10.5px; color: var(--muted-foreground); } -.wp-section--keep .wp-section__title { } -.wp-keepline { font-size: 13.5px; color: var(--muted-foreground); margin: 12px 0 0; max-width: 60ch; line-height: 1.6; } -.wp-keepline b { color: var(--foreground); font-weight: 600; } +.wp-kept__what { + font-size: 16px; + font-weight: 500; + color: var(--foreground); +} +.wp-kept__why { + font-family: var(--font-mono); + font-size: 10.5px; + color: var(--muted-foreground); +} +.wp-section--keep .wp-section__title { +} +.wp-keepline { + font-size: 13.5px; + color: var(--muted-foreground); + margin: 12px 0 0; + max-width: 60ch; + line-height: 1.6; +} +.wp-keepline b { + color: var(--foreground); + font-weight: 600; +} .wp-foot { margin-top: 40px; @@ -168,15 +288,22 @@ align-items: center; gap: 16px; } -.wp-copyline { font-family: var(--font-mono); font-size: 11px; color: var(--primary); letter-spacing: 0.04em; } +.wp-copyline { + font-family: var(--font-mono); + font-size: 11px; + color: var(--primary); + letter-spacing: 0.04em; +} /* theme toggle */ .wp-theme { position: fixed; - top: 18px; right: 22px; + top: 18px; + right: 22px; font-family: var(--font-mono); font-size: 14px; - width: 32px; height: 32px; + width: 32px; + height: 32px; background: var(--card); color: var(--foreground); border: 1px solid var(--border-strong); @@ -186,8 +313,17 @@ } @media (max-width: 900px) { - .wp { grid-template-columns: 1fr; } - .wp-who { border-right: 0; border-bottom: 1px solid var(--border); } - .wp-keep { grid-template-columns: 1fr; } - .wp-body { padding: 32px 28px 48px; } + .wp { + grid-template-columns: 1fr; + } + .wp-who { + border-right: 0; + border-bottom: 1px solid var(--border); + } + .wp-keep { + grid-template-columns: 1fr; + } + .wp-body { + padding: 32px 28px 48px; + } } diff --git a/docs/design-system/ink-carbon/project/reinvention/boards-envelope.jsx b/docs/design-system/ink-carbon/project/reinvention/boards-envelope.jsx index 168bb88e7..2e0640993 100644 --- a/docs/design-system/ink-carbon/project/reinvention/boards-envelope.jsx +++ b/docs/design-system/ink-carbon/project/reinvention/boards-envelope.jsx @@ -2,189 +2,250 @@ // SPDX-License-Identifier: Apache-2.0 /* Direction A — SECURITY TINT — boards */ -;(() => { - -function EnvIdentity() { - return ( -
-
-
-

PDPP

- Consent-class mail - Permit Nº 0001 +(() => { + function EnvIdentity() { + return ( +
+
+
+

PDPP

+ Consent-class mail + Permit Nº 0001 +
+

+ ref — security-tint envelopes, +
+ die-cut windows, permit indicia, +
+ certified-mail green cards +

-

- ref — security-tint envelopes,
die-cut windows, permit indicia,
certified-mail green cards -

-
-

- Your data travels sealed. The grant is the window. -

-

- A security tint has exactly one job: keeping personal data unreadable in transit. - A window envelope reveals exactly the named fields and nothing else. - PDPP already works this way — the brand just admits it. Granted is typed. Withheld is tinted. -

+

+ Your data travels sealed. The grant is the window. +

+

+ A security tint has exactly one job: keeping personal data unreadable in transit. A window envelope reveals + exactly the named fields and nothing else. PDPP already works this way — the brand just admits it.{" "} + Granted is typed. Withheld is tinted. +

-
-
-

The grammar

- one rule, applied everywhere -
-
-
- granted field - employer: Acme Co -
-
- granted field - net_pay: $3,508.12 +
+
+

The grammar

+ one rule, applied everywhere
-
- withheld field - +
+
+ granted field + employer: Acme Co +
+
+ granted field + net_pay: $3,508.12 +
+
+ withheld field + +
-
- -
-
-

House tints

- privacy, printed -
-
-
tint/hatch — withheld
-
tint/cross — sealed
-
tint/weave — archive
-
-
-
-
-

Palette

- postal, not pastel -
-
-
tint blue
-
paper
-
kraft
-
receipt green
-
return red
+
+
+

House tints

+ privacy, printed +
+
+
+
+ tint/hatch — withheld +
+
+
+ tint/cross — sealed +
+
+
+ tint/weave — archive +
+
-
-
-
-

Type

- Public Sans · Courier Prime -
-
-
- display / 700 - Granular access to personal data. -
-
- label / caps - Detach to revoke +
+
+

Palette

+ postal, not pastel
-
- data / typed - pay_statements.read — append only — 2y 1mo +
+
+
+ tint blue +
+
+
+ paper +
+
+
+ kraft +
+
+
+ receipt green +
+
+
+ return red +
-
-
- ); -} - -function EnvSurfaces() { - return ( -
-
-

Consent — the envelope

- windows show exactly what Longview gets -
-
-
-
- YOUR RESOURCE SERVER - rs.example.com · holder: you +
+
+

Type

+ Public Sans · Courier Prime
-
- PDPP
consent-class
Nº GRT-LONGVIEW01 +
+
+ display / 700 + + Granular access to personal data. + +
+
+ label / caps + + Detach to revoke + +
+
+ data / typed + + pay_statements.read — append only — 2y 1mo + +
-
- Deliver to - LONGVIEW PLANNING - purpose: long_term_financial_planning +
+ ); + } + + function EnvSurfaces() { + return ( +
+
+

Consent — the envelope

+ windows show exactly what Longview gets
-
-
- pay_statements.read - append only · 2y 1mo + +
+
+
+ YOUR RESOURCE SERVER + rs.example.com · holder: you +
+
+ PDPP +
+ consent-class +
+ Nº GRT-LONGVIEW01 +
-
- employment.read - current + 5y +
+ Deliver to + LONGVIEW PLANNING + purpose: long_term_financial_planning
-
- - tax_docs — sealed +
+
+ pay_statements.read + append only · 2y 1mo +
+
+ employment.read + current + 5y +
+
+ + tax_docs — sealed +
+
+ + identity — sealed +
-
- - identity — sealed +
+ Detach here to revoke — takes effect at the server + GRT-LONGVIEW01
-
- Detach here to revoke — takes effect at the server - GRT-LONGVIEW01 -
-
-
-

Grant record — the green card

- proof of consent, kept by the holder -
- -
-
-

Return receipt · consent recorded

- GRT-LONGVIEW01 +
+

Grant record — the green card

+ proof of consent, kept by the holder
-
-
GranteeLongview Planning
-
Modecontinuous
-
Issued2025-10-14 09:22Z
-
Expires2025-12-14 09:22Z
-
Scopes2 granted · 2 sealed
-
Purposelong_term_financial_planning
+ +
+
+

Return receipt · consent recorded

+ GRT-LONGVIEW01 +
+
+
+ Grantee + Longview Planning +
+
+ Mode + continuous +
+
+ Issued + 2025-10-14 09:22Z +
+
+ Expires + 2025-12-14 09:22Z +
+
+ Scopes + 2 granted · 2 sealed +
+
+ Purpose + long_term_financial_planning +
+
-
-
-

Endorsements & actions

- statuses are stamped, not badged -
+
+

Endorsements & actions

+ statuses are stamped, not badged +
-
- Active · first class - Expires Dec 14 - Sealed × 2 - Refused — returned to sender -
+
+ Active · first class + Expires Dec 14 + Sealed × 2 + Refused — returned to sender +
-
- - - +
+ + + +
-
- ); -} + ); + } -Object.assign(window, { EnvIdentity, EnvSurfaces }); + Object.assign(window, { EnvIdentity, EnvSurfaces }); })(); diff --git a/docs/design-system/ink-carbon/project/reinvention/boards-round2.jsx b/docs/design-system/ink-carbon/project/reinvention/boards-round2.jsx index 2f2f60b3a..fa9b6e812 100644 --- a/docs/design-system/ink-carbon/project/reinvention/boards-round2.jsx +++ b/docs/design-system/ink-carbon/project/reinvention/boards-round2.jsx @@ -2,280 +2,388 @@ // SPDX-License-Identifier: Apache-2.0 /* PDPP — Vibe studies round 2: six personalities, same content */ -;(() => { +(() => { + const SCOPES = [ + { name: "pay_statements.read", terms: "append only · 2y 1mo" }, + { name: "employment.read", terms: "current + 5y" }, + ]; -const SCOPES = [ - { name: "pay_statements.read", terms: "append only · 2y 1mo" }, - { name: "employment.read", terms: "current + 5y" }, -]; + function Chips({ items, labelStyle }) { + return ( +
+ {items.map(([c, n]) => ( +
+ {n} +
+ ))} +
+ ); + } -function Chips({ items, labelStyle }) { - return ( -
- {items.map(([c, n]) => ( -
- {n} + /* ── 1 · CARBON COPY ── */ + function VCarbon() { + return ( +
+
+
+

+ PDPP +

+ File copy — holder +
+ + ref — carbon paper, +
+ the duplicate you keep +
- ))} -
- ); -} - -/* ── 1 · CARBON COPY ── */ -function VCarbon() { - return ( -
-
-
-

PDPP

- File copy — holder +

+ Every grant writes two copies. Yours is the original. +

+
+
+
+ Longview Planning + long_term_financial_planning +
+ {SCOPES.map((s) => ( +
+ {s.name} + {s.terms} +
+ ))} +
+ Carbon · grt-longview01 +
+ + +
+
+
+
+
+ Recorded + Expires Dec 14 + + Revoked +
- ref — carbon paper,
the duplicate you keep
+
-

Every grant writes two copies. Yours is the original.

-
-
-
- Longview Planning - long_term_financial_planning + ); + } + + /* ── 2 · TWO-COLOR RIBBON ── */ + function VRibbon() { + return ( +
+
+
+

PDPP

+ black for the record · red for refusal +
+ + ref — two-color +
+ typewriter ribbon +
+
+

+ The record is typed in black. No is typed in red. +

+
Consent
+
+
+ grantee + Longview Planning +
+
+ purpose + long_term_financial_planning
{SCOPES.map((s) => ( -
- {s.name} - {s.terms} +
+ {s.name} + {s.terms}
))} -
- Carbon · grt-longview01 -
- - -
+
+ browsing.read + refused
-
-
- Recorded - Expires Dec 14 - Revoked -
- -
- ); -} - -/* ── 2 · TWO-COLOR RIBBON ── */ -function VRibbon() { - return ( -
-
-
-

PDPP

- black for the record · red for refusal +
+ [RECORDED] + [EXPIRES 12-14] + [REVOKED]
- ref — two-color
typewriter ribbon
-
-

The record is typed in black. No is typed in red.

-
Consent
-
-
granteeLongview Planning
-
purposelong_term_financial_planning
- {SCOPES.map((s) => ( -
{s.name}{s.terms}
- ))} -
browsing.readrefused
-
-
- [RECORDED] - [EXPIRES 12-14] - [REVOKED] -
-
- - -
- -
- ); -} - -/* ── 3 · CLARENDON LEDGER ── */ -function VClarendon() { - return ( -
-
-
-

PDPP

- est. rev 0.1.0 +
+ +
+
-

Consent, recorded — with the gravity of a ledger and none of the dust.

-
-
- Longview Planning - grt-longview01 + ); + } + + /* ── 3 · CLARENDON LEDGER ── */ + function VClarendon() { + return ( +
+
+
+

PDPP

+ est. rev 0.1.0 +
- {SCOPES.map((s, i) => ( -
- {String(i + 1).padStart(2, "0")} - {s.name} - {s.terms} +

+ Consent, recorded — with the gravity of a ledger and none of the dust. +

+
+
+ Longview Planning + grt-longview01 +
+ {SCOPES.map((s, i) => ( +
+ {String(i + 1).padStart(2, "0")} + {s.name} + {s.terms} +
+ ))} +
+ +
- ))} -
- -
-
-
- Recorded - Expires Dec 14 - Vacated -
- -
- ); -} - -/* ── 4 · PUNCH COLUMN ── */ -function VPunch() { - return ( -
-
-
-

PDPP

- col 01–80 · personal data +
+ Recorded + Expires Dec 14 + Vacated
- ref — tabulating cards,
a hole is a grant
+
-

A field is granted the way a card is punched: precisely, or not at all.

-
-
- Longview Planning - long_term_financial_planning + ); + } + + /* ── 4 · PUNCH COLUMN ── */ + function VPunch() { + return ( +
+
+
+

PDPP

+ col 01–80 · personal data +
+ + ref — tabulating cards, +
a hole is a grant +
- {SCOPES.map((s) => ( -
- - {s.name} - {s.terms} +

A field is granted the way a card is punched: precisely, or not at all.

+
+
+ Longview Planning + long_term_financial_planning +
+ {SCOPES.map((s) => ( +
+ + {s.name} + {s.terms} +
+ ))} +
+ + + tax_docs.read + + not punched +
+
+ 0 1 2 3 4 5 6 7 8 9 + col 12–18
- ))} -
- - tax_docs.read - not punched
-
0 1 2 3 4 5 6 7 8 9col 12–18
-
-
- Punched - Expires Dec 14 - Void -
-
- - -
- -
- ); -} - -/* ── 5 · CIVIC BOLD ── */ -function VCivic() { - return ( -
-
-
-

PDPP

- protocol, in public +
+ Punched + Expires Dec 14 + Void +
+
+ +
- ref — civic signage,
public notices
+
-

Your data. Your terms. In writing.

-
-
- Longview Planning - long_term_financial_planning + ); + } + + /* ── 5 · CIVIC BOLD ── */ + function VCivic() { + return ( +
+
+
+

PDPP

+ protocol, in public +
+ + ref — civic signage, +
+ public notices +
- {SCOPES.map((s) => ( -
- {s.name} - {s.terms} +

+ Your data. Your terms. In writing. +

+
+
+ Longview Planning + long_term_financial_planning +
+ {SCOPES.map((s) => ( +
+ {s.name} + {s.terms} +
+ ))} +
+ +
- ))} -
- -
+
+ Recorded + Expires Dec 14 + Revoked +
+
-
- Recorded - Expires Dec 14 - Revoked -
- -
- ); -} + ); + } -/* ── 6 · GREENBAR ── */ -function VGreenbar() { - return ( -
-
-
-
-
-

PDPP

- continuous form · holder's printout + /* ── 6 · GREENBAR ── */ + function VGreenbar() { + return ( +
+
+
+
+
+

PDPP

+ continuous form · holder's printout +
+ + ref — greenbar paper, +
+ tractor-feed printouts +
- ref — greenbar paper,
tractor-feed printouts
-
-

The server prints what crossed. Every band, accounted for.

-
-
- Longview Planning - long_term_financial_planning +

+ The server prints what crossed. Every band, accounted for. +

+
+
+ Longview Planning + long_term_financial_planning +
+ {SCOPES.map((s) => ( +
+ {s.name} + {s.terms} +
+ ))} +
+ queries this cycle + 14 · all inside grant +
+
+ last sync + 2025-11-04 11:05Z +
- {SCOPES.map((s) => ( -
{s.name}{s.terms}
- ))} -
queries this cycle14 · all inside grant
-
last sync2025-11-04 11:05Z
-
-
- Recorded - Expires Dec 14 - Revoked -
-
- - +
+ Recorded + Expires Dec 14 + Revoked +
+
+ + +
+
- -
- ); -} + ); + } -Object.assign(window, { VCarbon, VRibbon, VClarendon, VPunch, VCivic, VGreenbar }); + Object.assign(window, { VCarbon, VRibbon, VClarendon, VPunch, VCivic, VGreenbar }); })(); diff --git a/docs/design-system/ink-carbon/project/reinvention/boards-round3.jsx b/docs/design-system/ink-carbon/project/reinvention/boards-round3.jsx index 329489760..043663489 100644 --- a/docs/design-system/ink-carbon/project/reinvention/boards-round3.jsx +++ b/docs/design-system/ink-carbon/project/reinvention/boards-round3.jsx @@ -2,141 +2,210 @@ // SPDX-License-Identifier: Apache-2.0 /* PDPP round 3 — console + consent, written once, themed three ways */ -;(() => { - -/* ─── Hosted consent page (light) ─── */ -function Consent({ t }) { - const isT1 = t === "t1", isT2 = t === "t2", isT3 = t === "t3"; - const endorse = isT2 - ? (s) => `[${s.toUpperCase()}]` - : isT3 - ? (s) => (s === "active" ? "● " + s : s) - : (s) => s; - return ( -
-
- - PDPP - Northstar HR · provider -
-
-
- {isT2 ? "access request · staged via PAR" : "Access request"} -

Longview Planning asks to read 2 streams

-

- Purpose: long-term financial planning. Only the fields below cross. - Every response is projected to this grant — nothing else leaves Northstar HR. -

+(() => { + /* ─── Hosted consent page (light) ─── */ + function Consent({ t }) { + const isT1 = t === "t1", + isT2 = t === "t2", + isT3 = t === "t3"; + const endorse = isT2 ? (s) => `[${s.toUpperCase()}]` : isT3 ? (s) => (s === "active" ? "● " + s : s) : (s) => s; + return ( +
+
+ + PDPP + Northstar HR · provider
-
- {(isT1 || isT2) &&
} -
-
- Longview Planning - grant grt_lngvw_01 · client longview_planning_v1 -
-
- pay_statements.read - append only · 2 yrs - Employer, pay period, gross and net pay — 5 of 8 fields -
-
- employment.read - current + 5 yrs - Employers and dates. No salary history. -
-
- - {isT1 && "Carbon — your copy stays here"} - {isT2 && "DUPLICATE — OWNER'S FILE"} - {isT3 && "A copy stays on your server"} - - expires 2026-12-14 +
+
+ {isT2 ? "access request · staged via PAR" : "Access request"} +

Longview Planning asks to read 2 streams

+

+ Purpose: long-term financial planning. Only the fields below cross. Every response is projected to + this grant — nothing else leaves Northstar HR. +

+
+
+ {(isT1 || isT2) &&
} +
+
+ Longview Planning + grant grt_lngvw_01 · client longview_planning_v1 +
+
+ pay_statements.read + append only · 2 yrs + Employer, pay period, gross and net pay — 5 of 8 fields +
+
+ employment.read + current + 5 yrs + Employers and dates. No salary history. +
+
+ + {isT1 && "Carbon — your copy stays here"} + {isT2 && "DUPLICATE — OWNER'S FILE"} + {isT3 && "A copy stays on your server"} + + + expires 2026-12-14 + +
+
+ revocable at any time + takes effect at the server, not the app +
-
- revocable at any time - takes effect at the server, not the app +
+ + + You can revoke from your dashboard.
-
- - - You can revoke from your dashboard. -
-
- ); -} + ); + } -/* ─── Operator console overview (dark) ─── */ -const NAV = [ - ["Overview", "", true], ["Explore", ""], ["Sources", "7"], ["Traces", ""], - ["Grants", "4"], ["Runs", "12"], ["Schedules", "2"], ["Connect AI apps", ""], ["Deployment", ""], -]; -const GRANTS = [ - { client: "Longview Planning", id: "grt_lngvw_01", scopes: "pay_statements.read · employment.read", exp: "exp 2026-12-14", st: ["st-ok", "active"], dupe: true }, - { client: "Concert Recommendations", id: "grt_cncrt_02", scopes: "listening_history.read", exp: "continuous", st: ["st-pro", "continuous"] }, - { client: "pdpp CLI — owner export", id: "dev_cli_07", scopes: "* (owner device flow)", exp: "exp 2026-06-11", st: ["st-warn", "expiring 26h"] }, - { client: "Crosswise Ads", id: "grt_xwise_09", scopes: "browsing.read", exp: "2026-05-02", st: ["st-off", "revoked"], revoked: true }, -]; + /* ─── Operator console overview (dark) ─── */ + const NAV = [ + ["Overview", "", true], + ["Explore", ""], + ["Sources", "7"], + ["Traces", ""], + ["Grants", "4"], + ["Runs", "12"], + ["Schedules", "2"], + ["Connect AI apps", ""], + ["Deployment", ""], + ]; + const GRANTS = [ + { + client: "Longview Planning", + id: "grt_lngvw_01", + scopes: "pay_statements.read · employment.read", + exp: "exp 2026-12-14", + st: ["st-ok", "active"], + dupe: true, + }, + { + client: "Concert Recommendations", + id: "grt_cncrt_02", + scopes: "listening_history.read", + exp: "continuous", + st: ["st-pro", "continuous"], + }, + { + client: "pdpp CLI — owner export", + id: "dev_cli_07", + scopes: "* (owner device flow)", + exp: "exp 2026-06-11", + st: ["st-warn", "expiring 26h"], + }, + { + client: "Crosswise Ads", + id: "grt_xwise_09", + scopes: "browsing.read", + exp: "2026-05-02", + st: ["st-off", "revoked"], + revoked: true, + }, + ]; -function Console({ t }) { - const isT2 = t === "t2", isT3 = t === "t3"; - const fmt = (s) => (isT2 ? `[${s.toUpperCase()}]` : isT3 && s === "active" ? "● active" : isT3 && s === "revoked" ? "⊘ revoked" : isT3 && s.startsWith("expiring") ? "◐ " + s : isT3 ? "○ " + s : s); - return ( -
-
-
- - PDPP -
- -
- AS :7662 · RS :7663
composed @ localhost:3002
rev 668ecf811d47 -
-
-
-
-

Overview

- ⌘K — jump to grant, trace, run -
-
-
2connectors
-
7streams
-
48,112records
-
1.21 GBretained
-
-
-
-

{isT2 ? "Grants on file" : "Grants"}

- View all 4 → + function Console({ t }) { + const isT2 = t === "t2", + isT3 = t === "t3"; + const fmt = (s) => + isT2 + ? `[${s.toUpperCase()}]` + : isT3 && s === "active" + ? "● active" + : isT3 && s === "revoked" + ? "⊘ revoked" + : isT3 && s.startsWith("expiring") + ? "◐ " + s + : isT3 + ? "○ " + s + : s; + return ( +
+
+
+ + PDPP
-
- {GRANTS.map((g) => ( -
- - {g.client} - {g.id} - - {g.scopes} - {fmt(g.st[1])} - {g.exp} -
+ +
+ AS :7662 · RS :7663 +
+ composed @ localhost:3002 +
+ rev 668ecf811d47 +
+
+
+
+

Overview

+ ⌘K — jump to grant, trace, run +
+
+
+ 2 + connectors +
+
+ 7 + streams +
+
+ 48,112 + records +
+
+ 1.21 GB + retained +
+
+
+
+

{isT2 ? "Grants on file" : "Grants"}

+ View all 4 → +
+
+ {GRANTS.map((g) => ( +
+ + {g.client} + {g.id} + + {g.scopes} + {fmt(g.st[1])} + {g.exp} +
+ ))} +
-
- ); -} + ); + } -Object.assign(window, { R3Consent: Consent, R3Console: Console }); + Object.assign(window, { R3Consent: Consent, R3Console: Console }); })(); diff --git a/docs/design-system/ink-carbon/project/reinvention/design-canvas.jsx b/docs/design-system/ink-carbon/project/reinvention/design-canvas.jsx index 0906314ce..e057ccbb5 100644 --- a/docs/design-system/ink-carbon/project/reinvention/design-canvas.jsx +++ b/docs/design-system/ink-carbon/project/reinvention/design-canvas.jsx @@ -26,75 +26,75 @@ /* END USAGE */ const DC = { - bg: '#f0eee9', - grid: 'rgba(0,0,0,0.06)', - label: 'rgba(60,50,40,0.7)', - title: 'rgba(40,30,20,0.85)', - subtitle: 'rgba(60,50,40,0.6)', - postitBg: '#fef4a8', - postitText: '#5a4a2a', + bg: "#f0eee9", + grid: "rgba(0,0,0,0.06)", + label: "rgba(60,50,40,0.7)", + title: "rgba(40,30,20,0.85)", + subtitle: "rgba(60,50,40,0.6)", + postitBg: "#fef4a8", + postitText: "#5a4a2a", font: '-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif', }; // One-time CSS injection (classes are dc-prefixed so they don't collide with // the hosted design's own styles). -if (typeof document !== 'undefined' && !document.getElementById('dc-styles')) { - const s = document.createElement('style'); - s.id = 'dc-styles'; +if (typeof document !== "undefined" && !document.getElementById("dc-styles")) { + const s = document.createElement("style"); + s.id = "dc-styles"; s.textContent = [ - '.dc-editable{cursor:text;outline:none;white-space:nowrap;border-radius:3px;padding:0 2px;margin:0 -2px}', - '.dc-editable:focus{background:#fff;box-shadow:0 0 0 1.5px #c96442}', - '[data-dc-slot]{transition:transform .18s cubic-bezier(.2,.7,.3,1)}', - '[data-dc-slot].dc-dragging{transition:none;z-index:10;pointer-events:none}', - '[data-dc-slot].dc-dragging .dc-card{box-shadow:0 12px 40px rgba(0,0,0,.25),0 0 0 2px #c96442;transform:scale(1.02)}', + ".dc-editable{cursor:text;outline:none;white-space:nowrap;border-radius:3px;padding:0 2px;margin:0 -2px}", + ".dc-editable:focus{background:#fff;box-shadow:0 0 0 1.5px #c96442}", + "[data-dc-slot]{transition:transform .18s cubic-bezier(.2,.7,.3,1)}", + "[data-dc-slot].dc-dragging{transition:none;z-index:10;pointer-events:none}", + "[data-dc-slot].dc-dragging .dc-card{box-shadow:0 12px 40px rgba(0,0,0,.25),0 0 0 2px #c96442;transform:scale(1.02)}", // isolation:isolate contains artboard content's z-indexes so a // z-indexed child (sticky navbar etc.) can't paint over .dc-header or // the .dc-menu popover that drops into the top of the card. - '.dc-card{isolation:isolate;transition:box-shadow .15s,transform .15s}', - '.dc-card *{scrollbar-width:none}', - '.dc-card *::-webkit-scrollbar{display:none}', + ".dc-card{isolation:isolate;transition:box-shadow .15s,transform .15s}", + ".dc-card *{scrollbar-width:none}", + ".dc-card *::-webkit-scrollbar{display:none}", // Per-artboard header: grip + label on the left, delete/expand on the // right. Single flex row; when the artboard's on-screen width is too // narrow for both the label yields (ellipsis, then hidden entirely below // ~4ch via the container query) and the buttons stay on the row. - '.dc-header{position:absolute;bottom:100%;left:-4px;margin-bottom:calc(4px * var(--dc-inv-zoom,1));z-index:2;', - ' display:flex;align-items:center;container-type:inline-size}', - '.dc-labelrow{display:flex;align-items:center;gap:4px;height:24px;flex:1 1 auto;min-width:0}', - '.dc-grip{flex:0 0 auto;cursor:grab;display:flex;align-items:center;padding:5px 4px;border-radius:4px;transition:background .12s,opacity .12s}', - '.dc-grip:hover{background:rgba(0,0,0,.08)}', - '.dc-grip:active{cursor:grabbing}', - '.dc-labeltext{flex:1 1 auto;min-width:0;cursor:pointer;border-radius:4px;padding:3px 6px;', - ' display:flex;align-items:center;transition:background .12s;overflow:hidden}', + ".dc-header{position:absolute;bottom:100%;left:-4px;margin-bottom:calc(4px * var(--dc-inv-zoom,1));z-index:2;", + " display:flex;align-items:center;container-type:inline-size}", + ".dc-labelrow{display:flex;align-items:center;gap:4px;height:24px;flex:1 1 auto;min-width:0}", + ".dc-grip{flex:0 0 auto;cursor:grab;display:flex;align-items:center;padding:5px 4px;border-radius:4px;transition:background .12s,opacity .12s}", + ".dc-grip:hover{background:rgba(0,0,0,.08)}", + ".dc-grip:active{cursor:grabbing}", + ".dc-labeltext{flex:1 1 auto;min-width:0;cursor:pointer;border-radius:4px;padding:3px 6px;", + " display:flex;align-items:center;transition:background .12s;overflow:hidden}", // Below ~4ch of label room: hide the label entirely, and drop the grip to // hover-only (same reveal rule as .dc-btns) so a narrow header is clean // until the card is moused. - '@container (max-width: 110px){', - ' .dc-labeltext{display:none}', - ' .dc-grip{opacity:0}', - ' [data-dc-slot]:hover .dc-grip{opacity:1}', - '}', - '.dc-labeltext:hover{background:rgba(0,0,0,.05)}', - '.dc-labeltext .dc-editable{overflow:hidden;text-overflow:ellipsis;max-width:100%}', - '.dc-labeltext .dc-editable:focus{overflow:visible;text-overflow:clip}', - '.dc-btns{flex:0 0 auto;margin-left:auto;display:flex;gap:2px;opacity:0;transition:opacity .12s}', - '[data-dc-slot]:hover .dc-btns,.dc-btns:has(.dc-menu){opacity:1}', - '.dc-expand,.dc-kebab{width:22px;height:22px;border-radius:5px;border:none;cursor:pointer;padding:0;', - ' background:transparent;color:rgba(60,50,40,.7);display:flex;align-items:center;justify-content:center;', - ' font:inherit;transition:background .12s,color .12s}', - '.dc-expand:hover,.dc-kebab:hover{background:rgba(0,0,0,.06);color:#2a251f}', + "@container (max-width: 110px){", + " .dc-labeltext{display:none}", + " .dc-grip{opacity:0}", + " [data-dc-slot]:hover .dc-grip{opacity:1}", + "}", + ".dc-labeltext:hover{background:rgba(0,0,0,.05)}", + ".dc-labeltext .dc-editable{overflow:hidden;text-overflow:ellipsis;max-width:100%}", + ".dc-labeltext .dc-editable:focus{overflow:visible;text-overflow:clip}", + ".dc-btns{flex:0 0 auto;margin-left:auto;display:flex;gap:2px;opacity:0;transition:opacity .12s}", + "[data-dc-slot]:hover .dc-btns,.dc-btns:has(.dc-menu){opacity:1}", + ".dc-expand,.dc-kebab{width:22px;height:22px;border-radius:5px;border:none;cursor:pointer;padding:0;", + " background:transparent;color:rgba(60,50,40,.7);display:flex;align-items:center;justify-content:center;", + " font:inherit;transition:background .12s,color .12s}", + ".dc-expand:hover,.dc-kebab:hover{background:rgba(0,0,0,.06);color:#2a251f}", // Slot hosting an open menu floats above later siblings (which otherwise // paint on top — same z-index:auto, later DOM order) so the popup isn't // clipped by the next card. - '[data-dc-slot]:has(.dc-menu){z-index:10}', - '.dc-menu{position:absolute;top:100%;right:0;margin-top:4px;background:#fff;border-radius:8px;', - ' box-shadow:0 8px 28px rgba(0,0,0,.18),0 0 0 1px rgba(0,0,0,.05);padding:4px;min-width:160px;z-index:10}', - '.dc-menu button{display:block;width:100%;padding:7px 10px;border:0;background:transparent;', - ' border-radius:5px;font-family:inherit;font-size:13px;font-weight:500;line-height:1.2;', - ' color:#29261b;cursor:pointer;text-align:left;transition:background .12s;white-space:nowrap}', - '.dc-menu button:hover{background:rgba(0,0,0,.05)}', - '.dc-menu hr{border:0;border-top:1px solid rgba(0,0,0,.08);margin:4px 2px}', - '.dc-menu .dc-danger{color:#c96442}', - '.dc-menu .dc-danger:hover{background:rgba(201,100,66,.1)}', + "[data-dc-slot]:has(.dc-menu){z-index:10}", + ".dc-menu{position:absolute;top:100%;right:0;margin-top:4px;background:#fff;border-radius:8px;", + " box-shadow:0 8px 28px rgba(0,0,0,.18),0 0 0 1px rgba(0,0,0,.05);padding:4px;min-width:160px;z-index:10}", + ".dc-menu button{display:block;width:100%;padding:7px 10px;border:0;background:transparent;", + " border-radius:5px;font-family:inherit;font-size:13px;font-weight:500;line-height:1.2;", + " color:#29261b;cursor:pointer;text-align:left;transition:background .12s;white-space:nowrap}", + ".dc-menu button:hover{background:rgba(0,0,0,.05)}", + ".dc-menu hr{border:0;border-top:1px solid rgba(0,0,0,.08);margin:4px 2px}", + ".dc-menu .dc-danger{color:#c96442}", + ".dc-menu .dc-danger:hover{background:rgba(201,100,66,.1)}", // Chrome (titles / labels / buttons) counter-scales against the viewport // zoom so it stays a constant on-screen size. --dc-inv-zoom is set by // DCViewport on every transform update and inherits to all descendants — @@ -111,10 +111,10 @@ if (typeof document !== 'undefined' && !document.getElementById('dc-styles')) { // grows with the counter-scale, pushing the card row down — otherwise the // constant-screen-size title would overflow into the (shrinking) world- // space gap and overlap the artboard headers at low zoom. - '.dc-header{width:calc((100% + 4px) / var(--dc-inv-zoom,1));', - ' transform:scale(var(--dc-inv-zoom,1));transform-origin:bottom left}', - '.dc-sectionhead{zoom:var(--dc-inv-zoom,1)}', - ].join('\n'); + ".dc-header{width:calc((100% + 4px) / var(--dc-inv-zoom,1));", + " transform:scale(var(--dc-inv-zoom,1));transform-origin:bottom left}", + ".dc-sectionhead{zoom:var(--dc-inv-zoom,1)}", + ].join("\n"); document.head.appendChild(s); } @@ -142,7 +142,7 @@ function dcFlatten(children) { // host's window.omelette bridge — editing requires the omelette runtime. // Focus is ephemeral. // ───────────────────────────────────────────────────────────── -const DC_STATE_FILE = '.design-canvas.state.json'; +const DC_STATE_FILE = ".design-canvas.state.json"; function DesignCanvas({ children, minScale, maxScale, style }) { const [state, setState] = React.useState({ sections: {}, focus: null }); @@ -157,7 +157,7 @@ function DesignCanvas({ children, minScale, maxScale, style }) { React.useEffect(() => { let off = false; - fetch('./' + DC_STATE_FILE) + fetch("./" + DC_STATE_FILE) .then((r) => (r.ok ? r.json() : null)) .then((saved) => { if (off || !saved || !saved.sections) return; @@ -165,14 +165,25 @@ function DesignCanvas({ children, minScale, maxScale, style }) { setState((s) => ({ ...s, sections: saved.sections })); }) .catch(() => {}) - .finally(() => { didRead.current = true; if (!off) setReady(true); }); - const t = setTimeout(() => { if (!off) setReady(true); }, 150); - return () => { off = true; clearTimeout(t); }; + .finally(() => { + didRead.current = true; + if (!off) setReady(true); + }); + const t = setTimeout(() => { + if (!off) setReady(true); + }, 150); + return () => { + off = true; + clearTimeout(t); + }; }, []); React.useEffect(() => { if (!didRead.current) return; - if (skipNextWrite.current) { skipNextWrite.current = false; return; } + if (skipNextWrite.current) { + skipNextWrite.current = false; + return; + } const t = setTimeout(() => { window.omelette?.writeFile(DC_STATE_FILE, JSON.stringify({ sections: state.sections })).catch(() => {}); }, 250); @@ -182,8 +193,8 @@ function DesignCanvas({ children, minScale, maxScale, style }) { // Build registries synchronously from children so FocusOverlay can read // them in the same render. Fragments are flattened; wrapping in other // elements still opts out of focus/reorder. - const registry = {}; // slotId -> { sectionId, artboard } - const sectionMeta = {}; // sectionId -> { title, subtitle, slotIds[] } + const registry = {}; // slotId -> { sectionId, artboard } + const sectionMeta = {}; // sectionId -> { title, subtitle, slotIds[] } const sectionOrder = []; dcFlatten(children).forEach((sec) => { if (!sec || sec.type !== DCSection) return; @@ -199,8 +210,8 @@ function DesignCanvas({ children, minScale, maxScale, style }) { }); // hidden is scoped to one source revision — when the agent regenerates // (artboard-ID set changes), prior deletes don't apply to new content. - const srcKey = abs.map(([k]) => k).join('\x1f'); - const hidden = persisted.srcKey === srcKey ? (persisted.hidden || []) : []; + const srcKey = abs.map(([k]) => k).join("\x1f"); + const hidden = persisted.srcKey === srcKey ? persisted.hidden || [] : []; const srcIds = []; abs.forEach(([aid, ab]) => { if (hidden.includes(aid)) return; @@ -215,34 +226,45 @@ function DesignCanvas({ children, minScale, maxScale, style }) { }; }); - const api = React.useMemo(() => ({ - state, - section: (id) => state.sections[id] || {}, - patchSection: (id, p) => setState((s) => ({ - ...s, - sections: { ...s.sections, [id]: { ...s.sections[id], ...(typeof p === 'function' ? p(s.sections[id] || {}) : p) } }, - })), - setFocus: (slotId) => setState((s) => ({ ...s, focus: slotId })), - }), [state]); + const api = React.useMemo( + () => ({ + state, + section: (id) => state.sections[id] || {}, + patchSection: (id, p) => + setState((s) => ({ + ...s, + sections: { + ...s.sections, + [id]: { ...s.sections[id], ...(typeof p === "function" ? p(s.sections[id] || {}) : p) }, + }, + })), + setFocus: (slotId) => setState((s) => ({ ...s, focus: slotId })), + }), + [state] + ); // Esc exits focus; any outside pointerdown commits an in-progress rename. React.useEffect(() => { - const onKey = (e) => { if (e.key === 'Escape') api.setFocus(null); }; + const onKey = (e) => { + if (e.key === "Escape") api.setFocus(null); + }; const onPd = (e) => { const ae = document.activeElement; if (ae && ae.isContentEditable && !ae.contains(e.target)) ae.blur(); }; - document.addEventListener('keydown', onKey); - document.addEventListener('pointerdown', onPd, true); + document.addEventListener("keydown", onKey); + document.addEventListener("pointerdown", onPd, true); return () => { - document.removeEventListener('keydown', onKey); - document.removeEventListener('pointerdown', onPd, true); + document.removeEventListener("keydown", onKey); + document.removeEventListener("pointerdown", onPd, true); }; }, [api]); return ( - {ready && children} + + {ready && children} + {state.focus && registry[state.focus] && ( )} @@ -270,7 +292,7 @@ function DCViewport({ children, minScale = 0.1, maxScale = 8, style = {} }) { // Persist viewport across reloads so the user lands back where they were // after an agent edit or browser refresh. The sandbox origin is already // per-project; pathname keeps multiple canvas files in one project apart. - const tfKey = 'dc-viewport:' + location.pathname; + const tfKey = "dc-viewport:" + location.pathname; const saveT = React.useRef(0); const lastPostedScale = React.useRef(); @@ -280,26 +302,30 @@ function DCViewport({ children, minScale = 0.1, maxScale = 8, style = {} }) { if (!el) return; el.style.transform = `translate3d(${x}px, ${y}px, 0) scale(${scale})`; // Exposed for zoom-invariant chrome (labels, buttons, TweaksPanel). - el.style.setProperty('--dc-inv-zoom', String(1 / scale)); + el.style.setProperty("--dc-inv-zoom", String(1 / scale)); // Keep the host toolbar's % readout in sync with the canvas scale. Pan // ticks leave scale unchanged — skip the cross-frame post for those. if (lastPostedScale.current !== scale) { lastPostedScale.current = scale; - window.parent.postMessage({ type: '__dc_zoom', scale }, '*'); + window.parent.postMessage({ type: "__dc_zoom", scale }, "*"); } clearTimeout(saveT.current); saveT.current = setTimeout(() => { - try { localStorage.setItem(tfKey, JSON.stringify(tf.current)); } catch {} + try { + localStorage.setItem(tfKey, JSON.stringify(tf.current)); + } catch {} }, 200); }, [tfKey]); React.useLayoutEffect(() => { const flush = () => { clearTimeout(saveT.current); - try { localStorage.setItem(tfKey, JSON.stringify(tf.current)); } catch {} + try { + localStorage.setItem(tfKey, JSON.stringify(tf.current)); + } catch {} }; try { - const s = JSON.parse(localStorage.getItem(tfKey) || 'null'); + const s = JSON.parse(localStorage.getItem(tfKey) || "null"); if (s && Number.isFinite(s.x) && Number.isFinite(s.y) && Number.isFinite(s.scale)) { tf.current = { x: s.x, y: s.y, scale: Math.min(maxScale, Math.max(minScale, s.scale)) }; apply(); @@ -307,8 +333,11 @@ function DCViewport({ children, minScale = 0.1, maxScale = 8, style = {} }) { } catch {} // Flush on pagehide and unmount so a reload within the 200ms debounce // window doesn't drop the last pan/zoom. - window.addEventListener('pagehide', flush); - return () => { window.removeEventListener('pagehide', flush); flush(); }; + window.addEventListener("pagehide", flush); + return () => { + window.removeEventListener("pagehide", flush); + flush(); + }; }, []); React.useEffect(() => { @@ -317,7 +346,8 @@ function DCViewport({ children, minScale = 0.1, maxScale = 8, style = {} }) { const zoomAt = (cx, cy, factor) => { const r = vp.getBoundingClientRect(); - const px = cx - r.left, py = cy - r.top; + const px = cx - r.left, + py = cy - r.top; const t = tf.current; const next = Math.min(maxScale, Math.max(minScale, t.scale * factor)); const k = next / t.scale; @@ -328,10 +358,11 @@ function DCViewport({ children, minScale = 0.1, maxScale = 8, style = {} }) { // Anchor the DOM element under the cursor instead: record its screen Y, // apply the transform + --dc-inv-zoom, then cancel whatever vertical // drift the reflow introduced so it stays put on screen. - let marker = null, markerY0 = 0; + let marker = null, + markerY0 = 0; if (k !== 1) { const hit = document.elementFromPoint(cx, cy); - marker = hit && hit.closest ? hit.closest('[data-dc-slot],[data-dc-section]') : null; + marker = hit && hit.closest ? hit.closest("[data-dc-slot],[data-dc-section]") : null; if (marker) markerY0 = marker.getBoundingClientRect().top; } // keep the world point under the cursor fixed @@ -343,7 +374,10 @@ function DCViewport({ children, minScale = 0.1, maxScale = 8, style = {} }) { // A pure zoom around (cx, cy) maps screen Y → cy + (Y - cy) * k. Any // departure after the --dc-inv-zoom reflow is the layout drift. const drift = marker.getBoundingClientRect().top - (cy + (markerY0 - cy) * k); - if (Math.abs(drift) > 0.1) { t.y -= drift; apply(); } + if (Math.abs(drift) > 0.1) { + t.y -= drift; + apply(); + } } }; @@ -353,8 +387,7 @@ function DCViewport({ children, minScale = 0.1, maxScale = 8, style = {} }) { // two-finger scroll sends small/fractional pixel deltas, often with // non-zero deltaX. ctrlKey is set by the browser for trackpad pinch. const isMouseWheel = (e) => - e.deltaMode !== 0 || - (e.deltaX === 0 && Number.isInteger(e.deltaY) && Math.abs(e.deltaY) >= 40); + e.deltaMode !== 0 || (e.deltaX === 0 && Number.isInteger(e.deltaY) && Math.abs(e.deltaY) >= 40); const onWheel = (e) => { e.preventDefault(); @@ -381,57 +414,65 @@ function DCViewport({ children, minScale = 0.1, maxScale = 8, style = {} }) { // onWheel drop those entirely so they neither zoom nor pan. let gsBase = 1; let isGesturing = false; - const onGestureStart = (e) => { e.preventDefault(); isGesturing = true; gsBase = tf.current.scale; }; + const onGestureStart = (e) => { + e.preventDefault(); + isGesturing = true; + gsBase = tf.current.scale; + }; const onGestureChange = (e) => { e.preventDefault(); zoomAt(e.clientX, e.clientY, (gsBase * e.scale) / tf.current.scale); }; - const onGestureEnd = (e) => { e.preventDefault(); isGesturing = false; }; + const onGestureEnd = (e) => { + e.preventDefault(); + isGesturing = false; + }; // Drag-pan: middle button anywhere, or primary button on canvas // background (anything that isn't an artboard or an inline editor). let drag = null; const onPointerDown = (e) => { - const onBg = !e.target.closest('[data-dc-slot], .dc-editable'); + const onBg = !e.target.closest("[data-dc-slot], .dc-editable"); if (!(e.button === 1 || (e.button === 0 && onBg))) return; e.preventDefault(); vp.setPointerCapture(e.pointerId); drag = { id: e.pointerId, lx: e.clientX, ly: e.clientY }; - vp.style.cursor = 'grabbing'; + vp.style.cursor = "grabbing"; }; const onPointerMove = (e) => { if (!drag || e.pointerId !== drag.id) return; tf.current.x += e.clientX - drag.lx; tf.current.y += e.clientY - drag.ly; - drag.lx = e.clientX; drag.ly = e.clientY; + drag.lx = e.clientX; + drag.ly = e.clientY; apply(); }; const onPointerUp = (e) => { if (!drag || e.pointerId !== drag.id) return; vp.releasePointerCapture(e.pointerId); drag = null; - vp.style.cursor = ''; + vp.style.cursor = ""; }; // Host-driven zoom (toolbar % menu). Zooms around viewport centre so the // visible midpoint stays fixed — matching the host's iframe-zoom feel. const onHostMsg = (e) => { const d = e.data; - if (d && d.type === '__dc_set_zoom' && typeof d.scale === 'number') { + if (d && d.type === "__dc_set_zoom" && typeof d.scale === "number") { const r = vp.getBoundingClientRect(); zoomAt(r.left + r.width / 2, r.top + r.height / 2, d.scale / tf.current.scale); - } else if (d && d.type === '__dc_probe') { + } else if (d && d.type === "__dc_probe") { // Host's [readyGen] reset asks whether a canvas is present; it // fires on the iframe's native 'load', which for canvases with // images/fonts is after our mount-time announce, so re-announce. // Clear the pan-tick guard so apply() re-posts the current scale // even if it's unchanged — the host just reset dcScale to 1. - window.parent.postMessage({ type: '__dc_present' }, '*'); + window.parent.postMessage({ type: "__dc_present" }, "*"); lastPostedScale.current = undefined; apply(); } }; - window.addEventListener('message', onHostMsg); + window.addEventListener("message", onHostMsg); // Announce canvas mode so the host toolbar proxies its % control here // instead of scaling the iframe element (which would just shrink the // viewport window of an infinite canvas). The apply() that follows emits @@ -439,28 +480,28 @@ function DCViewport({ children, minScale = 0.1, maxScale = 8, style = {} }) { // lastPostedScale reset mirrors the __dc_probe handler: the layout // effect's restore-path apply() may already have posted the restored // scale (before __dc_present), so clear the guard to re-post it in order. - window.parent.postMessage({ type: '__dc_present' }, '*'); + window.parent.postMessage({ type: "__dc_present" }, "*"); lastPostedScale.current = undefined; apply(); - vp.addEventListener('wheel', onWheel, { passive: false }); - vp.addEventListener('gesturestart', onGestureStart, { passive: false }); - vp.addEventListener('gesturechange', onGestureChange, { passive: false }); - vp.addEventListener('gestureend', onGestureEnd, { passive: false }); - vp.addEventListener('pointerdown', onPointerDown); - vp.addEventListener('pointermove', onPointerMove); - vp.addEventListener('pointerup', onPointerUp); - vp.addEventListener('pointercancel', onPointerUp); + vp.addEventListener("wheel", onWheel, { passive: false }); + vp.addEventListener("gesturestart", onGestureStart, { passive: false }); + vp.addEventListener("gesturechange", onGestureChange, { passive: false }); + vp.addEventListener("gestureend", onGestureEnd, { passive: false }); + vp.addEventListener("pointerdown", onPointerDown); + vp.addEventListener("pointermove", onPointerMove); + vp.addEventListener("pointerup", onPointerUp); + vp.addEventListener("pointercancel", onPointerUp); return () => { - window.removeEventListener('message', onHostMsg); - vp.removeEventListener('wheel', onWheel); - vp.removeEventListener('gesturestart', onGestureStart); - vp.removeEventListener('gesturechange', onGestureChange); - vp.removeEventListener('gestureend', onGestureEnd); - vp.removeEventListener('pointerdown', onPointerDown); - vp.removeEventListener('pointermove', onPointerMove); - vp.removeEventListener('pointerup', onPointerUp); - vp.removeEventListener('pointercancel', onPointerUp); + window.removeEventListener("message", onHostMsg); + vp.removeEventListener("wheel", onWheel); + vp.removeEventListener("gesturestart", onGestureStart); + vp.removeEventListener("gesturechange", onGestureChange); + vp.removeEventListener("gestureend", onGestureEnd); + vp.removeEventListener("pointerdown", onPointerDown); + vp.removeEventListener("pointermove", onPointerMove); + vp.removeEventListener("pointerup", onPointerUp); + vp.removeEventListener("pointercancel", onPointerUp); }; }, [apply, minScale, maxScale]); @@ -470,29 +511,42 @@ function DCViewport({ children, minScale = 0.1, maxScale = 8, style = {} }) { ref={vpRef} className="design-canvas" style={{ - height: '100vh', width: '100vw', + height: "100vh", + width: "100vw", background: DC.bg, - overflow: 'hidden', - overscrollBehavior: 'none', - touchAction: 'none', - position: 'relative', + overflow: "hidden", + overscrollBehavior: "none", + touchAction: "none", + position: "relative", fontFamily: DC.font, - boxSizing: 'border-box', + boxSizing: "border-box", ...style, }} >
-
+
{children}
@@ -512,14 +566,14 @@ function DCSection({ id, title, subtitle, children, gap = 48 }) { // Must match DesignCanvas's srcKey computation exactly (it filters falsy // IDs), or onDelete persists a srcKey that DesignCanvas never recognizes. const allIds = artboards.map((a) => a.props.id ?? a.props.label).filter(Boolean); - const srcKey = allIds.join('\x1f'); - const hidden = sec.srcKey === srcKey ? (sec.hidden || []) : []; + const srcKey = allIds.join("\x1f"); + const hidden = sec.srcKey === srcKey ? sec.hidden || [] : []; const srcOrder = allIds.filter((k) => !hidden.includes(k)); const order = React.useMemo(() => { const kept = (sec.order || []).filter((k) => srcOrder.includes(k)); return [...kept, ...srcOrder.filter((k) => !kept.includes(k))]; - }, [sec.order, srcOrder.join('|')]); + }, [sec.order, srcOrder.join("|")]); const byId = Object.fromEntries(artboards.map((a) => [a.props.id ?? a.props.label, a])); @@ -530,27 +584,44 @@ function DCSection({ id, title, subtitle, children, gap = 48 }) { // the 24px artboard-header (abs-positioned above each card) plus ~8px, so // the title sits tight against its own row at every zoom. return ( -
-
+
+
- ctx && sid && ctx.patchSection(sid, { title: v })} - style={{ fontSize: 28, fontWeight: 600, color: DC.title, letterSpacing: -0.4, marginBottom: 6, display: 'inline-block' }} /> + style={{ + fontSize: 28, + fontWeight: 600, + color: DC.title, + letterSpacing: -0.4, + marginBottom: 6, + display: "inline-block", + }} + /> {subtitle &&
{subtitle}
}
-
+
{order.map((k) => ( - ctx && ctx.patchSection(sid, (x) => ({ labels: { ...x.labels, [k]: v } }))} onReorder={(next) => ctx && ctx.patchSection(sid, { order: next })} - onDelete={() => ctx && ctx.patchSection(sid, (x) => ({ - hidden: [...(x.srcKey === srcKey ? (x.hidden || []) : []), k], - srcKey, - }))} - onFocus={() => ctx && ctx.setFocus(`${sid}/${k}`)} /> + onDelete={() => + ctx && + ctx.patchSection(sid, (x) => ({ + hidden: [...(x.srcKey === srcKey ? x.hidden || [] : []), k], + srcKey, + })) + } + onFocus={() => ctx && ctx.setFocus(`${sid}/${k}`)} + /> ))}
{rest} @@ -559,7 +630,9 @@ function DCSection({ id, title, subtitle, children, gap = 48 }) { } // DCArtboard — marker; rendered by DCArtboardFrame via DCSection. -function DCArtboard() { return null; } +function DCArtboard() { + return null; +} // Per-artboard export (kind: 'png' | 'html'). Both paths share the same // self-contained clone: computed styles baked in, @font-face / / @@ -568,79 +641,132 @@ function DCArtboard() { return null; } // (same pipeline the host uses for page captures); HTML wraps it in a // minimal standalone document. Both are independent of viewport zoom. async function dcExport(node, w, h, name, kind) { - try { await document.fonts.ready; } catch {} - const toDataURL = (url) => fetch(url).then((r) => r.blob()).then((b) => new Promise((res) => { - const fr = new FileReader(); fr.onload = () => res(fr.result); fr.onerror = () => res(url); fr.readAsDataURL(b); - })).catch(() => url); + try { + await document.fonts.ready; + } catch {} + const toDataURL = (url) => + fetch(url) + .then((r) => r.blob()) + .then( + (b) => + new Promise((res) => { + const fr = new FileReader(); + fr.onload = () => res(fr.result); + fr.onerror = () => res(url); + fr.readAsDataURL(b); + }) + ) + .catch(() => url); // Collect @font-face rules. ss.cssRules throws SecurityError on // cross-origin sheets (e.g. fonts.googleapis.com) — in that case fetch // the CSS text directly (those endpoints send ACAO:*) and regex-extract // the blocks. @import and @media/@supports are walked so nested // @font-face rules aren't missed. - const fontRules = [], pending = [], seen = new Set(); + const fontRules = [], + pending = [], + seen = new Set(); const scrapeCss = (href) => { - if (seen.has(href)) return; seen.add(href); - pending.push(fetch(href).then((r) => r.text()).then((css) => { - for (const m of css.match(/@font-face\s*{[^}]*}/g) || []) fontRules.push({ css: m, base: href }); - for (const m of css.matchAll(/@import\s+(?:url\()?['"]?([^'")\s;]+)/g)) - scrapeCss(new URL(m[1], href).href); - }).catch(() => {})); + if (seen.has(href)) return; + seen.add(href); + pending.push( + fetch(href) + .then((r) => r.text()) + .then((css) => { + for (const m of css.match(/@font-face\s*{[^}]*}/g) || []) fontRules.push({ css: m, base: href }); + for (const m of css.matchAll(/@import\s+(?:url\()?['"]?([^'")\s;]+)/g)) scrapeCss(new URL(m[1], href).href); + }) + .catch(() => {}) + ); }; const walk = (rules, base) => { for (const r of rules) { if (r.type === CSSRule.FONT_FACE_RULE) fontRules.push({ css: r.cssText, base }); else if (r.type === CSSRule.IMPORT_RULE && r.styleSheet) { const ibase = r.styleSheet.href || base; - try { walk(r.styleSheet.cssRules, ibase); } catch { scrapeCss(ibase); } + try { + walk(r.styleSheet.cssRules, ibase); + } catch { + scrapeCss(ibase); + } } else if (r.cssRules) walk(r.cssRules, base); } }; for (const ss of document.styleSheets) { const base = ss.href || location.href; - try { walk(ss.cssRules, base); } catch { if (ss.href) scrapeCss(ss.href); } + try { + walk(ss.cssRules, base); + } catch { + if (ss.href) scrapeCss(ss.href); + } } while (pending.length) await pending.shift(); - const fontCss = (await Promise.all(fontRules.map(async (rule) => { - let out = rule.css, m; const re = /url\((['"]?)([^'")]+)\1\)/g; - while ((m = re.exec(rule.css))) { - if (m[2].indexOf('data:') === 0) continue; - let abs; try { abs = new URL(m[2], rule.base).href; } catch { continue; } - out = out.split(m[0]).join('url("' + await toDataURL(abs) + '")'); - } - return out; - }))).join('\n'); + const fontCss = ( + await Promise.all( + fontRules.map(async (rule) => { + let out = rule.css, + m; + const re = /url\((['"]?)([^'")]+)\1\)/g; + while ((m = re.exec(rule.css))) { + if (m[2].indexOf("data:") === 0) continue; + let abs; + try { + abs = new URL(m[2], rule.base).href; + } catch { + continue; + } + out = out.split(m[0]).join('url("' + (await toDataURL(abs)) + '")'); + } + return out; + }) + ) + ).join("\n"); const cloneStyled = (src) => { - if (src.nodeType === 8 || (src.nodeType === 1 && src.tagName === 'SCRIPT')) return document.createTextNode(''); + if (src.nodeType === 8 || (src.nodeType === 1 && src.tagName === "SCRIPT")) return document.createTextNode(""); const dst = src.cloneNode(false); if (src.nodeType === 1) { - const cs = getComputedStyle(src); let txt = ''; - for (let i = 0; i < cs.length; i++) txt += cs[i] + ':' + cs.getPropertyValue(cs[i]) + ';'; - dst.setAttribute('style', txt + 'animation:none;transition:none;'); - if (src.tagName === 'CANVAS') try { const im = document.createElement('img'); im.src = src.toDataURL(); im.setAttribute('style', txt); return im; } catch {} + const cs = getComputedStyle(src); + let txt = ""; + for (let i = 0; i < cs.length; i++) txt += cs[i] + ":" + cs.getPropertyValue(cs[i]) + ";"; + dst.setAttribute("style", txt + "animation:none;transition:none;"); + if (src.tagName === "CANVAS") + try { + const im = document.createElement("img"); + im.src = src.toDataURL(); + im.setAttribute("style", txt); + return im; + } catch {} } for (let c = src.firstChild; c; c = c.nextSibling) dst.appendChild(cloneStyled(c)); return dst; }; const clone = cloneStyled(node); - clone.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml'); + clone.setAttribute("xmlns", "http://www.w3.org/1999/xhtml"); // Drop the card's own shadow/radius so the export is a flush w×h rect; // the artboard's own background (if any) is already in the computed style. - clone.style.boxShadow = 'none'; clone.style.borderRadius = '0'; + clone.style.boxShadow = "none"; + clone.style.borderRadius = "0"; const jobs = []; - clone.querySelectorAll('img').forEach((el) => { - const s = el.getAttribute('src'); - if (s && s.indexOf('data:') !== 0) jobs.push(toDataURL(el.src).then((d) => el.setAttribute('src', d))); + clone.querySelectorAll("img").forEach((el) => { + const s = el.getAttribute("src"); + if (s && s.indexOf("data:") !== 0) jobs.push(toDataURL(el.src).then((d) => el.setAttribute("src", d))); }); - [clone, ...clone.querySelectorAll('*')].forEach((el) => { - const bg = el.style.backgroundImage; if (!bg) return; - let m; const re = /url\(["']?([^"')]+)["']?\)/g; + [clone, ...clone.querySelectorAll("*")].forEach((el) => { + const bg = el.style.backgroundImage; + if (!bg) return; + let m; + const re = /url\(["']?([^"')]+)["']?\)/g; while ((m = re.exec(bg))) { - const tok = m[0], url = m[1]; - if (url.indexOf('data:') === 0) continue; - jobs.push(toDataURL(url).then((d) => { el.style.backgroundImage = el.style.backgroundImage.split(tok).join('url("' + d + '")'); })); + const tok = m[0], + url = m[1]; + if (url.indexOf("data:") === 0) continue; + jobs.push( + toDataURL(url).then((d) => { + el.style.backgroundImage = el.style.backgroundImage.split(tok).join('url("' + d + '")'); + }) + ); } }); await Promise.all(jobs); @@ -648,16 +774,23 @@ async function dcExport(node, w, h, name, kind) { const xml = new XMLSerializer().serializeToString(clone); const save = (blob, ext) => { if (!blob) return; - const a = document.createElement('a'); - a.href = URL.createObjectURL(blob); a.download = name + '.' + ext; a.click(); + const a = document.createElement("a"); + a.href = URL.createObjectURL(blob); + a.download = name + "." + ext; + a.click(); setTimeout(() => URL.revokeObjectURL(a.href), 1000); }; - if (kind === 'html') { - const html = '' + name + '' + - (fontCss ? '' : '') + - '' + xml + ''; - return save(new Blob([html], { type: 'text/html' }), 'html'); + if (kind === "html") { + const html = + '' + + name + + "" + + (fontCss ? "" : "") + + '' + + xml + + ""; + return save(new Blob([html], { type: "text/html" }), "html"); } // PNG: the SVG's own width/height must be the output resolution — an @@ -666,18 +799,34 @@ async function dcExport(node, w, h, name, kind) { // w×h foreignObject onto the px·w × px·h SVG canvas so the browser renders // the HTML at full resolution. const px = 3; - const svg = '' + - (fontCss ? '' : '') + xml + ''; + const svg = + '' + + (fontCss ? "" : "") + + xml + + ""; const img = new Image(); await new Promise((res, rej) => { - img.onload = res; img.onerror = () => rej(new Error('svg load failed')); - img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg); + img.onload = res; + img.onerror = () => rej(new Error("svg load failed")); + img.src = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(svg); }); - const cv = document.createElement('canvas'); - cv.width = w * px; cv.height = h * px; - cv.getContext('2d').drawImage(img, 0, 0); - cv.toBlob((blob) => save(blob, 'png'), 'image/png'); + const cv = document.createElement("canvas"); + cv.width = w * px; + cv.height = h * px; + cv.getContext("2d").drawImage(img, 0, 0); + cv.toBlob((blob) => save(blob, "png"), "image/png"); } function DCArtboardFrame({ sectionId, artboard, label, order, onRename, onReorder, onFocus, onDelete }) { @@ -692,25 +841,32 @@ function DCArtboardFrame({ sectionId, artboard, label, order, onRename, onReorde // ⋯ menu: close on any outside pointerdown. Two-click delete lives inside // the menu — first click arms the row, second commits; closing disarms. React.useEffect(() => { - if (!menuOpen) { setConfirming(false); return; } - const off = (e) => { if (!menuRef.current || !menuRef.current.contains(e.target)) setMenuOpen(false); }; - document.addEventListener('pointerdown', off, true); - return () => document.removeEventListener('pointerdown', off, true); + if (!menuOpen) { + setConfirming(false); + return; + } + const off = (e) => { + if (!menuRef.current || !menuRef.current.contains(e.target)) setMenuOpen(false); + }; + document.addEventListener("pointerdown", off, true); + return () => document.removeEventListener("pointerdown", off, true); }, [menuOpen]); const doExport = (kind) => { setMenuOpen(false); if (!cardRef.current) return; - const name = String(label || id || 'artboard').replace(/[^\w\s.-]+/g, '_'); - dcExport(cardRef.current, width, height, name, kind) - .catch((e) => console.error('[design-canvas] export failed:', e)); + const name = String(label || id || "artboard").replace(/[^\w\s.-]+/g, "_"); + dcExport(cardRef.current, width, height, name, kind).catch((e) => + console.error("[design-canvas] export failed:", e) + ); }; // Live drag-reorder: dragged card sticks to cursor; siblings slide into // their would-be slots in real time via transforms. DOM order only // changes on drop. const onGripDown = (e) => { - e.preventDefault(); e.stopPropagation(); + e.preventDefault(); + e.stopPropagation(); const me = ref.current; // translateX is applied in local (pre-scale) space but pointer deltas and // getBoundingClientRect().left are screen-space — divide by the viewport's @@ -722,7 +878,7 @@ function DCArtboardFrame({ sectionId, artboard, label, order, onRename, onReorde const startIdx = order.indexOf(id); const startX = e.clientX; let liveOrder = order.slice(); - me.classList.add('dc-dragging'); + me.classList.add("dc-dragging"); const layout = () => { for (const h of homes) { @@ -736,10 +892,14 @@ function DCArtboardFrame({ sectionId, artboard, label, order, onRename, onReorde const dx = ev.clientX - startX; me.style.transform = `translateX(${dx / scale}px)`; const cur = homes[startIdx].x + dx; - let nearest = 0, best = Infinity; + let nearest = 0, + best = Infinity; for (let i = 0; i < slotXs.length; i++) { const d = Math.abs(slotXs[i] - cur); - if (d < best) { best = d; nearest = i; } + if (d < best) { + best = d; + nearest = i; + } } if (liveOrder.indexOf(id) !== nearest) { liveOrder = order.filter((k) => k !== id); @@ -749,77 +909,155 @@ function DCArtboardFrame({ sectionId, artboard, label, order, onRename, onReorde }; const up = () => { - document.removeEventListener('pointermove', move); - document.removeEventListener('pointerup', up); + document.removeEventListener("pointermove", move); + document.removeEventListener("pointerup", up); const finalSlot = liveOrder.indexOf(id); - me.classList.remove('dc-dragging'); + me.classList.remove("dc-dragging"); me.style.transform = `translateX(${(slotXs[finalSlot] - homes[startIdx].x) / scale}px)`; // After the settle transition, kill transitions + clear transforms + // commit the reorder in the same frame so there's no visual snap-back. setTimeout(() => { - for (const h of homes) { h.el.style.transition = 'none'; h.el.style.transform = ''; } - if (liveOrder.join('|') !== order.join('|')) onReorder(liveOrder); - requestAnimationFrame(() => requestAnimationFrame(() => { - for (const h of homes) h.el.style.transition = ''; - })); + for (const h of homes) { + h.el.style.transition = "none"; + h.el.style.transform = ""; + } + if (liveOrder.join("|") !== order.join("|")) onReorder(liveOrder); + requestAnimationFrame(() => + requestAnimationFrame(() => { + for (const h of homes) h.el.style.transition = ""; + }) + ); }, 180); }; - document.addEventListener('pointermove', move); - document.addEventListener('pointerup', up); + document.addEventListener("pointermove", move); + document.addEventListener("pointerup", up); }; return ( -
-
e.stopPropagation()}> +
+
e.stopPropagation()} + >
- + + + + + + + +
- e.stopPropagation()} - style={{ fontSize: 15, fontWeight: 500, color: DC.label, lineHeight: 1 }} /> + e.stopPropagation()} + style={{ fontSize: 15, fontWeight: 500, color: DC.label, lineHeight: 1 }} + />
-
+
{menuOpen && (
e.stopPropagation()}> - - + +
-
)}
-
- {children ||
{id}
} +
+ {children || ( +
+ {id} +
+ )}
); } // Inline rename — commits on blur or Enter. -function DCEditable({ value, onChange, style, tag = 'span', onClick }) { +function DCEditable({ value, onChange, style, tag = "span", onClick }) { const T = tag; return ( - e.stopPropagation()} onBlur={(e) => onChange && onChange(e.currentTarget.textContent)} - onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); e.currentTarget.blur(); } }} - style={style}>{value} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); + e.currentTarget.blur(); + } + }} + style={style} + > + {value} + ); } @@ -837,7 +1075,10 @@ function DCFocusOverlay({ entry, sectionMeta, sectionOrder }) { const idx = peers.indexOf(aid); const secIdx = sectionOrder.indexOf(sectionId); - const go = (d) => { const n = peers[(idx + d + peers.length) % peers.length]; if (n) ctx.setFocus(`${sectionId}/${n}`); }; + const go = (d) => { + const n = peers[(idx + d + peers.length) % peers.length]; + if (n) ctx.setFocus(`${sectionId}/${n}`); + }; const goSection = (d) => { // Sections whose artboards are all deleted have slotIds:[] — step past // them to the next non-empty section so ↑/↓ doesn't dead-end. @@ -845,97 +1086,277 @@ function DCFocusOverlay({ entry, sectionMeta, sectionOrder }) { for (let i = 1; i < n; i++) { const ns = sectionOrder[(((secIdx + d * i) % n) + n) % n]; const first = sectionMeta[ns] && sectionMeta[ns].slotIds[0]; - if (first) { ctx.setFocus(`${ns}/${first}`); return; } + if (first) { + ctx.setFocus(`${ns}/${first}`); + return; + } } }; React.useEffect(() => { const k = (e) => { - if (e.key === 'ArrowLeft') { e.preventDefault(); go(-1); } - if (e.key === 'ArrowRight') { e.preventDefault(); go(1); } - if (e.key === 'ArrowUp') { e.preventDefault(); goSection(-1); } - if (e.key === 'ArrowDown') { e.preventDefault(); goSection(1); } + if (e.key === "ArrowLeft") { + e.preventDefault(); + go(-1); + } + if (e.key === "ArrowRight") { + e.preventDefault(); + go(1); + } + if (e.key === "ArrowUp") { + e.preventDefault(); + goSection(-1); + } + if (e.key === "ArrowDown") { + e.preventDefault(); + goSection(1); + } }; - document.addEventListener('keydown', k); - return () => document.removeEventListener('keydown', k); + document.addEventListener("keydown", k); + return () => document.removeEventListener("keydown", k); }); const { width = 260, height = 480, children } = artboard.props; const [vp, setVp] = React.useState({ w: window.innerWidth, h: window.innerHeight }); - React.useEffect(() => { const r = () => setVp({ w: window.innerWidth, h: window.innerHeight }); window.addEventListener('resize', r); return () => window.removeEventListener('resize', r); }, []); + React.useEffect(() => { + const r = () => setVp({ w: window.innerWidth, h: window.innerHeight }); + window.addEventListener("resize", r); + return () => window.removeEventListener("resize", r); + }, []); const scale = Math.max(0.1, Math.min((vp.w - 200) / width, (vp.h - 260) / height, 2)); const [ddOpen, setDd] = React.useState(false); const Arrow = ({ dir, onClick }) => ( - ); // Portal to body so position:fixed is the real viewport regardless of any // transform on DesignCanvas's ancestors (including the canvas zoom itself). return ReactDOM.createPortal( -
ctx.setFocus(null)} +
ctx.setFocus(null)} onWheel={(e) => e.preventDefault()} - style={{ position: 'fixed', inset: 0, zIndex: 100, background: 'rgba(24,20,16,.6)', backdropFilter: 'blur(14px)', - fontFamily: DC.font, color: '#fff' }}> - + style={{ + position: "fixed", + inset: 0, + zIndex: 100, + background: "rgba(24,20,16,.6)", + backdropFilter: "blur(14px)", + fontFamily: DC.font, + color: "#fff", + }} + > {/* top bar: section dropdown (left) · close (right) */} -
e.stopPropagation()} - style={{ position: 'absolute', top: 0, left: 0, right: 0, height: 72, display: 'flex', alignItems: 'flex-start', padding: '16px 20px 0', gap: 16 }}> -
- {ddOpen && ( -
- {sectionOrder.filter((sid) => sectionMeta[sid].slotIds.length).map((sid) => ( - - ))} +
+ {sectionOrder + .filter((sid) => sectionMeta[sid].slotIds.length) + .map((sid) => ( + + ))}
)}
- +
{/* card centered, label + index below — only the card itself stops propagation so any backdrop click (including the margins around the card) exits focus */}
-
e.stopPropagation()} style={{ width: width * scale, height: height * scale, position: 'relative' }}> -
- {children ||
{aid}
} + style={{ + position: "absolute", + top: 64, + bottom: 56, + left: 100, + right: 100, + display: "flex", + flexDirection: "column", + alignItems: "center", + justifyContent: "center", + gap: 16, + }} + > +
e.stopPropagation()} + style={{ width: width * scale, height: height * scale, position: "relative" }} + > +
+ {children || ( +
+ {aid} +
+ )}
-
e.stopPropagation()} style={{ fontSize: 14, fontWeight: 500, opacity: .85, textAlign: 'center' }}> +
e.stopPropagation()} + style={{ fontSize: 14, fontWeight: 500, opacity: 0.85, textAlign: "center" }} + > {(sec.labels || {})[aid] ?? artboard.props.label} - {idx + 1} / {peers.length} + + {idx + 1} / {peers.length} +
@@ -943,16 +1364,35 @@ function DCFocusOverlay({ entry, sectionMeta, sectionOrder }) { go(1)} /> {/* dots */} -
e.stopPropagation()} - style={{ position: 'absolute', bottom: 20, left: '50%', transform: 'translateX(-50%)', display: 'flex', gap: 8 }}> +
e.stopPropagation()} + style={{ + position: "absolute", + bottom: 20, + left: "50%", + transform: "translateX(-50%)", + display: "flex", + gap: 8, + }} + > {peers.map((p, i) => ( -
, - document.body, + document.body ); } @@ -961,17 +1401,28 @@ function DCFocusOverlay({ entry, sectionMeta, sectionOrder }) { // ───────────────────────────────────────────────────────────── function DCPostIt({ children, top, left, right, bottom, rotate = -2, width = 180 }) { return ( -
{children}
+
+ {children} +
); } Object.assign(window, { DesignCanvas, DCSection, DCArtboard, DCPostIt }); - diff --git a/docs/design-system/ink-carbon/project/reinvention/envelope.css b/docs/design-system/ink-carbon/project/reinvention/envelope.css index 5a95c9579..072bdcffc 100644 --- a/docs/design-system/ink-carbon/project/reinvention/envelope.css +++ b/docs/design-system/ink-carbon/project/reinvention/envelope.css @@ -13,34 +13,39 @@ grant id = permit indicia · revocation = perforation proof of consent = the green card ═══════════════════════════════════════════════════════════════════ */ -@import url('https://fonts.googleapis.com/css2?family=Public+Sans:wght@400;600;700;800&family=Courier+Prime:ital,wght@0,400;0,700;1,400&display=swap'); +@import url("https://fonts.googleapis.com/css2?family=Public+Sans:wght@400;600;700;800&family=Courier+Prime:ital,wght@0,400;0,700;1,400&display=swap"); .env { --env-paper: oklch(0.975 0.005 95); --env-ink: oklch(0.24 0.01 270); - --env-faint: oklch(0.50 0.01 270); - --env-tint: oklch(0.42 0.09 265); /* security-tint blue */ + --env-faint: oklch(0.5 0.01 270); + --env-tint: oklch(0.42 0.09 265); /* security-tint blue */ --env-tint-ground: oklch(0.93 0.018 265); --env-kraft: oklch(0.85 0.035 85); --env-kraft-deep: oklch(0.76 0.045 80); - --env-receipt: oklch(0.55 0.09 155); /* certified-mail green */ + --env-receipt: oklch(0.55 0.09 155); /* certified-mail green */ --env-receipt-ground: oklch(0.93 0.03 155); - --env-return: oklch(0.52 0.17 27); /* return-to-sender red */ + --env-return: oklch(0.52 0.17 27); /* return-to-sender red */ --env-hairline: oklch(0.84 0.008 270); --env-sans: "Public Sans", system-ui, sans-serif; --env-type: "Courier Prime", "Courier New", monospace; /* the three house tints */ - --tint-hatch: repeating-linear-gradient(45deg, - var(--env-tint) 0 1px, transparent 1px 5px); - --tint-cross: repeating-linear-gradient(45deg, - var(--env-tint) 0 1px, transparent 1px 6px), - repeating-linear-gradient(-45deg, - var(--env-tint) 0 1px, transparent 1px 6px); - --tint-weave: repeating-linear-gradient(0deg, - var(--env-tint) 0 1px, transparent 1px 4px), - repeating-linear-gradient(90deg, - var(--env-tint) 0 1px, transparent 1px 4px); + --tint-hatch: repeating-linear-gradient( + 45deg, + var(--env-tint) 0 1px, + transparent 1px 5px + ); + --tint-cross: + repeating-linear-gradient(45deg, var(--env-tint) 0 1px, transparent 1px 6px), + repeating-linear-gradient( + -45deg, + var(--env-tint) 0 1px, + transparent 1px 6px + ); + --tint-weave: + repeating-linear-gradient(0deg, var(--env-tint) 0 1px, transparent 1px 4px), + repeating-linear-gradient(90deg, var(--env-tint) 0 1px, transparent 1px 4px); background: var(--env-paper); color: var(--env-ink); @@ -53,7 +58,9 @@ flex-direction: column; gap: 30px; } -.env * { box-sizing: border-box; } +.env * { + box-sizing: border-box; +} /* ─── Indicia (the brand lockup) ─── */ .env-indicia { @@ -82,8 +89,14 @@ text-transform: uppercase; color: var(--env-ink); } -.env-indicia--tint { border-color: var(--env-tint); outline-color: var(--env-tint); color: var(--env-tint); } -.env-indicia--tint .env-indicia__line { color: var(--env-tint); } +.env-indicia--tint { + border-color: var(--env-tint); + outline-color: var(--env-tint); + color: var(--env-tint); +} +.env-indicia--tint .env-indicia__line { + color: var(--env-tint); +} /* ─── Display statement ─── */ .env-statement { @@ -95,43 +108,99 @@ max-width: 17ch; text-wrap: balance; } -.env-statement .t { color: var(--env-tint); } -.env-body { font-size: 15px; max-width: 54ch; margin: 0; color: var(--env-faint); } -.env-body b { color: var(--env-ink); font-weight: 600; } +.env-statement .t { + color: var(--env-tint); +} +.env-body { + font-size: 15px; + max-width: 54ch; + margin: 0; + color: var(--env-faint); +} +.env-body b { + color: var(--env-ink); + font-weight: 600; +} /* ─── Section caption ─── */ .env-cap { - display: flex; align-items: baseline; gap: 10px; + display: flex; + align-items: baseline; + gap: 10px; border-top: 2px solid var(--env-ink); padding-top: 8px; } .env-cap__name { - font-size: 11px; font-weight: 700; letter-spacing: 0.18em; - text-transform: uppercase; margin: 0; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.18em; + text-transform: uppercase; + margin: 0; +} +.env-cap__sub { + font-family: var(--env-type); + font-size: 11.5px; + color: var(--env-faint); + white-space: nowrap; } -.env-cap__sub { font-family: var(--env-type); font-size: 11.5px; color: var(--env-faint); white-space: nowrap; } /* ─── Tint specimen row ─── */ -.env-tints { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; } -.env-tint-card { display: flex; flex-direction: column; gap: 8px; } +.env-tints { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 16px; +} +.env-tint-card { + display: flex; + flex-direction: column; + gap: 8px; +} .env-tint-card__chip { height: 76px; background-color: var(--env-tint-ground); border: 1px solid var(--env-tint); } -.env-tint-card__chip.is-hatch { background-image: var(--tint-hatch); } -.env-tint-card__chip.is-cross { background-image: var(--tint-cross); } -.env-tint-card__chip.is-weave { background-image: var(--tint-weave); } -.env-tint-card__name { font-family: var(--env-type); font-size: 11.5px; color: var(--env-faint); } +.env-tint-card__chip.is-hatch { + background-image: var(--tint-hatch); +} +.env-tint-card__chip.is-cross { + background-image: var(--tint-cross); +} +.env-tint-card__chip.is-weave { + background-image: var(--tint-weave); +} +.env-tint-card__name { + font-family: var(--env-type); + font-size: 11.5px; + color: var(--env-faint); +} /* ─── Palette ─── */ -.env-palette { display: grid; grid-template-columns: repeat(5, 1fr); gap: 16px; } -.env-swatch { display: flex; flex-direction: column; gap: 8px; } -.env-swatch__chip { height: 56px; border: 1px solid var(--env-hairline); } -.env-swatch__name { font-family: var(--env-type); font-size: 11px; color: var(--env-faint); } +.env-palette { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 16px; +} +.env-swatch { + display: flex; + flex-direction: column; + gap: 8px; +} +.env-swatch__chip { + height: 56px; + border: 1px solid var(--env-hairline); +} +.env-swatch__name { + font-family: var(--env-type); + font-size: 11px; + color: var(--env-faint); +} /* ─── The grammar demo: granted vs withheld ─── */ -.env-grammar { display: flex; flex-direction: column; } +.env-grammar { + display: flex; + flex-direction: column; +} .env-field { display: grid; grid-template-columns: 150px 1fr; @@ -140,21 +209,28 @@ padding: 10px 0; border-bottom: 1px solid var(--env-hairline); } -.env-field:last-child { border-bottom: 0; } +.env-field:last-child { + border-bottom: 0; +} .env-field__k { - font-size: 10.5px; font-weight: 700; letter-spacing: 0.14em; - text-transform: uppercase; color: var(--env-faint); + font-size: 10.5px; + font-weight: 700; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--env-faint); } .env-field__window { background: white; border: 1px solid var(--env-hairline); - border-radius: 6px; /* envelope windows are round-cornered */ + border-radius: 6px; /* envelope windows are round-cornered */ padding: 7px 12px; font-family: var(--env-type); font-size: 14px; width: max-content; min-width: 220px; - box-shadow: inset 0 0 0 1px oklch(1 0 0), inset 0 1px 3px oklch(0 0 0 / 0.07); + box-shadow: + inset 0 0 0 1px oklch(1 0 0), + inset 0 1px 3px oklch(0 0 0 / 0.07); } .env-field__bar { height: 30px; @@ -166,13 +242,26 @@ } /* ─── Type ramp ─── */ -.env-ramp { display: flex; flex-direction: column; } +.env-ramp { + display: flex; + flex-direction: column; +} .env-ramp__row { - display: grid; grid-template-columns: 150px 1fr; gap: 18px; align-items: baseline; - padding: 9px 0; border-bottom: 1px solid var(--env-hairline); + display: grid; + grid-template-columns: 150px 1fr; + gap: 18px; + align-items: baseline; + padding: 9px 0; + border-bottom: 1px solid var(--env-hairline); +} +.env-ramp__row:last-child { + border-bottom: 0; +} +.env-ramp__tag { + font-family: var(--env-type); + font-size: 11px; + color: var(--env-faint); } -.env-ramp__row:last-child { border-bottom: 0; } -.env-ramp__tag { font-family: var(--env-type); font-size: 11px; color: var(--env-faint); } /* ═══ SURFACES ═══ */ @@ -186,29 +275,71 @@ gap: 18px; position: relative; } -.env-envelope__row { display: flex; justify-content: space-between; align-items: flex-start; gap: 20px; } -.env-envelope__from { display: flex; flex-direction: column; gap: 2px; } -.env-envelope__from-name { font-size: 13px; font-weight: 700; letter-spacing: 0.06em; } -.env-envelope__from-sub { font-family: var(--env-type); font-size: 11.5px; color: var(--env-faint); white-space: nowrap; } +.env-envelope__row { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 20px; +} +.env-envelope__from { + display: flex; + flex-direction: column; + gap: 2px; +} +.env-envelope__from-name { + font-size: 13px; + font-weight: 700; + letter-spacing: 0.06em; +} +.env-envelope__from-sub { + font-family: var(--env-type); + font-size: 11.5px; + color: var(--env-faint); + white-space: nowrap; +} .env-envelope__indicia { border: 1.5px solid var(--env-ink); padding: 7px 10px; text-align: center; - font-size: 8.5px; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; + font-size: 8.5px; + font-weight: 700; + letter-spacing: 0.14em; + text-transform: uppercase; line-height: 1.6; flex-shrink: 0; } -.env-envelope__to { display: flex; flex-direction: column; gap: 3px; padding-left: 10px; } -.env-envelope__to-label { font-size: 9.5px; font-weight: 700; letter-spacing: 0.16em; text-transform: uppercase; color: var(--env-faint); } -.env-envelope__to-name { font-family: var(--env-type); font-size: 17px; font-weight: 700; } -.env-envelope__to-purpose { font-family: var(--env-type); font-size: 13px; color: var(--env-ink); } +.env-envelope__to { + display: flex; + flex-direction: column; + gap: 3px; + padding-left: 10px; +} +.env-envelope__to-label { + font-size: 9.5px; + font-weight: 700; + letter-spacing: 0.16em; + text-transform: uppercase; + color: var(--env-faint); +} +.env-envelope__to-name { + font-family: var(--env-type); + font-size: 17px; + font-weight: 700; +} +.env-envelope__to-purpose { + font-family: var(--env-type); + font-size: 13px; + color: var(--env-ink); +} /* The die-cut window */ .env-window { background: white; border-radius: 8px; padding: 14px 18px; - box-shadow: inset 0 0 0 1px oklch(0 0 0 / 0.06), inset 0 2px 5px oklch(0 0 0 / 0.08); + box-shadow: + inset 0 0 0 1px oklch(0 0 0 / 0.06), + inset 0 2px 5px oklch(0 0 0 / 0.08); display: flex; flex-direction: column; } @@ -220,9 +351,21 @@ padding: 6px 0; border-bottom: 1px dotted var(--env-hairline); } -.env-window__row:last-child { border-bottom: 0; } -.env-window__field { font-family: var(--env-type); font-size: 14.5px; font-weight: 700; } -.env-window__terms { font-family: var(--env-type); font-size: 11.5px; color: var(--env-faint); text-align: right; white-space: nowrap; } +.env-window__row:last-child { + border-bottom: 0; +} +.env-window__field { + font-family: var(--env-type); + font-size: 14.5px; + font-weight: 700; +} +.env-window__terms { + font-family: var(--env-type); + font-size: 11.5px; + color: var(--env-faint); + text-align: right; + white-space: nowrap; +} .env-window__bar { height: 18px; background-color: var(--env-tint-ground); @@ -231,7 +374,13 @@ width: 100%; max-width: 300px; } -.env-window__bar-tag { font-family: var(--env-type); font-size: 11.5px; color: var(--env-tint); text-align: right; white-space: nowrap; } +.env-window__bar-tag { + font-family: var(--env-type); + font-size: 11.5px; + color: var(--env-tint); + text-align: right; + white-space: nowrap; +} /* Perforated revoke edge */ .env-perf { @@ -243,8 +392,19 @@ gap: 16px; padding: 10px 26px 14px; } -.env-perf__hint { font-size: 9.5px; font-weight: 700; letter-spacing: 0.16em; text-transform: uppercase; color: var(--env-faint); } -.env-perf__no { font-family: var(--env-type); font-size: 11.5px; color: var(--env-faint); white-space: nowrap; } +.env-perf__hint { + font-size: 9.5px; + font-weight: 700; + letter-spacing: 0.16em; + text-transform: uppercase; + color: var(--env-faint); +} +.env-perf__no { + font-family: var(--env-type); + font-size: 11.5px; + color: var(--env-faint); + white-space: nowrap; +} /* ─── The green card (grant record / proof of consent) ─── */ .env-receipt { @@ -255,9 +415,26 @@ flex-direction: column; gap: 12px; } -.env-receipt__head { display: flex; justify-content: space-between; align-items: baseline; gap: 16px; } -.env-receipt__title { font-size: 11px; font-weight: 800; letter-spacing: 0.18em; text-transform: uppercase; color: var(--env-receipt); margin: 0; } -.env-receipt__no { font-family: var(--env-type); font-size: 12.5px; font-weight: 700; white-space: nowrap; } +.env-receipt__head { + display: flex; + justify-content: space-between; + align-items: baseline; + gap: 16px; +} +.env-receipt__title { + font-size: 11px; + font-weight: 800; + letter-spacing: 0.18em; + text-transform: uppercase; + color: var(--env-receipt); + margin: 0; +} +.env-receipt__no { + font-family: var(--env-type); + font-size: 12.5px; + font-weight: 700; + white-space: nowrap; +} .env-receipt__grid { display: grid; grid-template-columns: 1fr 1fr; @@ -265,40 +442,86 @@ border-top: 1px solid var(--env-receipt); } .env-receipt__cell { - display: flex; flex-direction: column; gap: 1px; + display: flex; + flex-direction: column; + gap: 1px; padding: 8px 0 9px; border-bottom: 1px solid oklch(0.55 0.09 155 / 0.35); } -.env-receipt__k { font-size: 9px; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; color: var(--env-receipt); } -.env-receipt__v { font-family: var(--env-type); font-size: 13.5px; } +.env-receipt__k { + font-size: 9px; + font-weight: 700; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--env-receipt); +} +.env-receipt__v { + font-family: var(--env-type); + font-size: 13.5px; +} /* ─── Endorsements (statuses) ─── */ -.env-endorse { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; } +.env-endorse { + display: flex; + flex-wrap: wrap; + gap: 10px; + align-items: center; +} .env-tag { - font-size: 10px; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; border: 1.5px solid var(--env-ink); padding: 4px 9px; white-space: nowrap; } -.env-tag--receipt { border-color: var(--env-receipt); color: var(--env-receipt); } -.env-tag--tint { border-color: var(--env-tint); color: var(--env-tint); } +.env-tag--receipt { + border-color: var(--env-receipt); + color: var(--env-receipt); +} +.env-tag--tint { + border-color: var(--env-tint); + color: var(--env-tint); +} .env-tag--return { - border-color: var(--env-return); color: var(--env-return); + border-color: var(--env-return); + color: var(--env-return); transform: rotate(-3deg); } /* ─── Buttons ─── */ -.env-actions { display: flex; gap: 12px; align-items: center; } +.env-actions { + display: flex; + gap: 12px; + align-items: center; +} .env-btn { font-family: var(--env-sans); - font-size: 11.5px; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; - background: var(--env-ink); color: var(--env-paper); + font-size: 11.5px; + font-weight: 700; + letter-spacing: 0.1em; + text-transform: uppercase; + background: var(--env-ink); + color: var(--env-paper); border: 1.5px solid var(--env-ink); padding: 10px 18px; cursor: pointer; white-space: nowrap; } -.env-btn--ghost { background: transparent; color: var(--env-ink); } -.env-btn--return { background: transparent; color: var(--env-return); border-color: var(--env-return); } +.env-btn--ghost { + background: transparent; + color: var(--env-ink); +} +.env-btn--return { + background: transparent; + color: var(--env-return); + border-color: var(--env-return); +} -.env-note { font-family: var(--env-type); font-size: 11.5px; color: var(--env-faint); margin: 0; } +.env-note { + font-family: var(--env-type); + font-size: 11.5px; + color: var(--env-faint); + margin: 0; +} diff --git a/docs/design-system/ink-carbon/project/reinvention/round2.css b/docs/design-system/ink-carbon/project/reinvention/round2.css index 0a02c29c6..aa291cef3 100644 --- a/docs/design-system/ink-carbon/project/reinvention/round2.css +++ b/docs/design-system/ink-carbon/project/reinvention/round2.css @@ -9,180 +9,797 @@ typed data, official hardware. No tint, no postal costume. Shared skeleton classes (.vs-*) + one prefix per study. ═══════════════════════════════════════════════════════════════════ */ -@import url('https://fonts.googleapis.com/css2?family=Familjen+Grotesk:ital,wght@0,400..700;1,400&family=Fragment+Mono:ital@0;1&family=Besley:ital,wght@0,400..900;1,400..700&family=Archivo:wght@400;500;700;900&family=Martian+Mono:wght@400;700&family=IBM+Plex+Mono:wght@400;500;600&family=Spline+Sans+Mono:wght@400;500;600&display=swap'); +@import url("https://fonts.googleapis.com/css2?family=Familjen+Grotesk:ital,wght@0,400..700;1,400&family=Fragment+Mono:ital@0;1&family=Besley:ital,wght@0,400..900;1,400..700&family=Archivo:wght@400;500;700;900&family=Martian+Mono:wght@400;700&family=IBM+Plex+Mono:wght@400;500;600&family=Spline+Sans+Mono:wght@400;500;600&display=swap"); /* ─── shared skeleton ─── */ -.vs { height: 100%; padding: 34px 36px; display: flex; flex-direction: column; gap: 22px; } -.vs * { box-sizing: border-box; } -.vs-row { display: flex; justify-content: space-between; align-items: flex-start; gap: 18px; } -.vs-chips { display: flex; gap: 10px; } -.vs-chip { width: 54px; height: 40px; } -.vs-chip span { display: block; margin-top: 46px; font-size: 9px; white-space: nowrap; } -.vs-tags { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; } -.vs-actions { display: flex; gap: 10px; align-items: center; } +.vs { + height: 100%; + padding: 34px 36px; + display: flex; + flex-direction: column; + gap: 22px; +} +.vs * { + box-sizing: border-box; +} +.vs-row { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 18px; +} +.vs-chips { + display: flex; + gap: 10px; +} +.vs-chip { + width: 54px; + height: 40px; +} +.vs-chip span { + display: block; + margin-top: 46px; + font-size: 9px; + white-space: nowrap; +} +.vs-tags { + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: center; +} +.vs-actions { + display: flex; + gap: 10px; + align-items: center; +} /* ═══ 1 · CARBON COPY ═══ */ .cc { - --p: oklch(0.99 0.003 250); --ink: oklch(0.21 0.012 270); - --vio: oklch(0.46 0.15 295); --vio-soft: oklch(0.46 0.15 295 / 0.10); + --p: oklch(0.99 0.003 250); + --ink: oklch(0.21 0.012 270); + --vio: oklch(0.46 0.15 295); + --vio-soft: oklch(0.46 0.15 295 / 0.1); --line: oklch(0.87 0.005 270); - background: var(--p); color: var(--ink); - font-family: "Familjen Grotesk", system-ui, sans-serif; font-size: 14px; line-height: 1.5; -} -.cc-mono { font-family: "Fragment Mono", monospace; } -.cc-lockup { display: flex; flex-direction: column; gap: 2px; } -.cc-lockup__name { font-size: 30px; font-weight: 700; letter-spacing: -0.02em; margin: 0; line-height: 1; } -.cc-lockup__name em { font-style: normal; color: var(--vio); } -.cc-lockup__sub { font-family: "Fragment Mono", monospace; font-size: 10px; color: var(--vio); letter-spacing: 0.1em; text-transform: uppercase; } -.cc-statement { font-size: 27px; font-weight: 600; letter-spacing: -0.02em; line-height: 1.15; margin: 0; text-wrap: balance; } -.cc-statement em { font-style: italic; color: var(--vio); } -.cc-dupe { position: relative; } + background: var(--p); + color: var(--ink); + font-family: "Familjen Grotesk", system-ui, sans-serif; + font-size: 14px; + line-height: 1.5; +} +.cc-mono { + font-family: "Fragment Mono", monospace; +} +.cc-lockup { + display: flex; + flex-direction: column; + gap: 2px; +} +.cc-lockup__name { + font-size: 30px; + font-weight: 700; + letter-spacing: -0.02em; + margin: 0; + line-height: 1; +} +.cc-lockup__name em { + font-style: normal; + color: var(--vio); +} +.cc-lockup__sub { + font-family: "Fragment Mono", monospace; + font-size: 10px; + color: var(--vio); + letter-spacing: 0.1em; + text-transform: uppercase; +} +.cc-statement { + font-size: 27px; + font-weight: 600; + letter-spacing: -0.02em; + line-height: 1.15; + margin: 0; + text-wrap: balance; +} +.cc-statement em { + font-style: italic; + color: var(--vio); +} +.cc-dupe { + position: relative; +} .cc-dupe__shadow { - position: absolute; inset: 0; + position: absolute; + inset: 0; transform: translate(10px, 10px); border: 1.5px solid oklch(0.46 0.15 295 / 0.45); background: var(--vio-soft); } -.cc-card { position: relative; border: 1.5px solid var(--ink); background: var(--p); padding: 16px 18px; display: flex; flex-direction: column; gap: 10px; } -.cc-card__head { display: flex; flex-direction: column; gap: 2px; } -.cc-card__client { font-size: 16px; font-weight: 700; } -.cc-card__purpose { font-family: "Fragment Mono", monospace; font-size: 10.5px; color: var(--vio); } -.cc-scope { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; padding: 7px 0; border-top: 1px solid var(--line); } -.cc-scope__name { font-family: "Fragment Mono", monospace; font-size: 13px; color: var(--vio); } -.cc-scope__terms { font-family: "Fragment Mono", monospace; font-size: 10px; color: oklch(0.5 0.01 270); white-space: nowrap; } -.cc-card__foot { border-top: 1px solid var(--line); padding-top: 10px; display: flex; justify-content: space-between; align-items: center; gap: 10px; } -.cc-copytag { font-family: "Fragment Mono", monospace; font-size: 9px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--vio); } -.cc-btn { font-family: "Familjen Grotesk", sans-serif; font-size: 12.5px; font-weight: 700; background: var(--ink); color: var(--p); border: 1.5px solid var(--ink); padding: 7px 14px; cursor: pointer; white-space: nowrap; } -.cc-btn--ghost { background: transparent; color: var(--ink); } -.cc-tag { font-family: "Fragment Mono", monospace; font-size: 9.5px; letter-spacing: 0.08em; text-transform: uppercase; border: 1px solid var(--ink); padding: 4px 8px; white-space: nowrap; } -.cc-tag--vio { border-color: var(--vio); color: var(--vio); } +.cc-card { + position: relative; + border: 1.5px solid var(--ink); + background: var(--p); + padding: 16px 18px; + display: flex; + flex-direction: column; + gap: 10px; +} +.cc-card__head { + display: flex; + flex-direction: column; + gap: 2px; +} +.cc-card__client { + font-size: 16px; + font-weight: 700; +} +.cc-card__purpose { + font-family: "Fragment Mono", monospace; + font-size: 10.5px; + color: var(--vio); +} +.cc-scope { + display: flex; + justify-content: space-between; + align-items: baseline; + gap: 12px; + padding: 7px 0; + border-top: 1px solid var(--line); +} +.cc-scope__name { + font-family: "Fragment Mono", monospace; + font-size: 13px; + color: var(--vio); +} +.cc-scope__terms { + font-family: "Fragment Mono", monospace; + font-size: 10px; + color: oklch(0.5 0.01 270); + white-space: nowrap; +} +.cc-card__foot { + border-top: 1px solid var(--line); + padding-top: 10px; + display: flex; + justify-content: space-between; + align-items: center; + gap: 10px; +} +.cc-copytag { + font-family: "Fragment Mono", monospace; + font-size: 9px; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--vio); +} +.cc-btn { + font-family: "Familjen Grotesk", sans-serif; + font-size: 12.5px; + font-weight: 700; + background: var(--ink); + color: var(--p); + border: 1.5px solid var(--ink); + padding: 7px 14px; + cursor: pointer; + white-space: nowrap; +} +.cc-btn--ghost { + background: transparent; + color: var(--ink); +} +.cc-tag { + font-family: "Fragment Mono", monospace; + font-size: 9.5px; + letter-spacing: 0.08em; + text-transform: uppercase; + border: 1px solid var(--ink); + padding: 4px 8px; + white-space: nowrap; +} +.cc-tag--vio { + border-color: var(--vio); + color: var(--vio); +} /* ═══ 2 · TWO-COLOR RIBBON ═══ */ .rb { - --p: oklch(0.962 0.01 92); --ink: oklch(0.25 0.015 75); - --red: oklch(0.54 0.19 27); --line: oklch(0.82 0.015 85); - background: var(--p); color: var(--ink); - font-family: "IBM Plex Mono", monospace; font-size: 13px; line-height: 1.6; -} -.rb-lockup__name { font-size: 24px; font-weight: 600; letter-spacing: 0.3em; text-indent: 0.3em; margin: 0; } -.rb-lockup__sub { font-size: 10px; color: var(--red); letter-spacing: 0.1em; } -.rb-statement { font-size: 19px; font-weight: 600; line-height: 1.45; margin: 0; max-width: 30ch; } -.rb-statement b { color: var(--red); font-weight: 600; } -.rb-h { font-size: 11px; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase; display: flex; align-items: center; gap: 10px; } -.rb-h::before, .rb-h::after { content: ""; height: 1px; background: var(--ink); flex: 0 0 24px; } -.rb-h::after { flex: 1; } -.rb-card { border: 1px solid var(--ink); padding: 14px 16px; display: flex; flex-direction: column; gap: 6px; } -.rb-line { display: grid; grid-template-columns: 96px 1fr; gap: 12px; font-size: 12px; } -.rb-line__k { color: oklch(0.5 0.02 75); } -.rb-line--scope { display: grid; grid-template-columns: 1fr auto; border-top: 1px dotted var(--line); padding-top: 6px; } -.rb-line--scope .rb-line__k { white-space: nowrap; } -.rb-revoked { color: var(--red); text-decoration: line-through; text-decoration-color: var(--red); } -.rb-tag { font-size: 10.5px; white-space: nowrap; } -.rb-tag--red { color: var(--red); } -.rb-btn { font-family: inherit; font-size: 11.5px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; background: var(--ink); color: var(--p); border: 1px solid var(--ink); padding: 8px 14px; cursor: pointer; white-space: nowrap; } -.rb-btn--red { background: transparent; color: var(--red); border-color: var(--red); } + --p: oklch(0.962 0.01 92); + --ink: oklch(0.25 0.015 75); + --red: oklch(0.54 0.19 27); + --line: oklch(0.82 0.015 85); + background: var(--p); + color: var(--ink); + font-family: "IBM Plex Mono", monospace; + font-size: 13px; + line-height: 1.6; +} +.rb-lockup__name { + font-size: 24px; + font-weight: 600; + letter-spacing: 0.3em; + text-indent: 0.3em; + margin: 0; +} +.rb-lockup__sub { + font-size: 10px; + color: var(--red); + letter-spacing: 0.1em; +} +.rb-statement { + font-size: 19px; + font-weight: 600; + line-height: 1.45; + margin: 0; + max-width: 30ch; +} +.rb-statement b { + color: var(--red); + font-weight: 600; +} +.rb-h { + font-size: 11px; + font-weight: 600; + letter-spacing: 0.14em; + text-transform: uppercase; + display: flex; + align-items: center; + gap: 10px; +} +.rb-h::before, +.rb-h::after { + content: ""; + height: 1px; + background: var(--ink); + flex: 0 0 24px; +} +.rb-h::after { + flex: 1; +} +.rb-card { + border: 1px solid var(--ink); + padding: 14px 16px; + display: flex; + flex-direction: column; + gap: 6px; +} +.rb-line { + display: grid; + grid-template-columns: 96px 1fr; + gap: 12px; + font-size: 12px; +} +.rb-line__k { + color: oklch(0.5 0.02 75); +} +.rb-line--scope { + display: grid; + grid-template-columns: 1fr auto; + border-top: 1px dotted var(--line); + padding-top: 6px; +} +.rb-line--scope .rb-line__k { + white-space: nowrap; +} +.rb-revoked { + color: var(--red); + text-decoration: line-through; + text-decoration-color: var(--red); +} +.rb-tag { + font-size: 10.5px; + white-space: nowrap; +} +.rb-tag--red { + color: var(--red); +} +.rb-btn { + font-family: inherit; + font-size: 11.5px; + font-weight: 600; + letter-spacing: 0.06em; + text-transform: uppercase; + background: var(--ink); + color: var(--p); + border: 1px solid var(--ink); + padding: 8px 14px; + cursor: pointer; + white-space: nowrap; +} +.rb-btn--red { + background: transparent; + color: var(--red); + border-color: var(--red); +} /* ═══ 3 · CLARENDON LEDGER ═══ */ .cl { - --p: oklch(0.972 0.009 85); --ink: oklch(0.24 0.02 60); - --green: oklch(0.43 0.07 165); --line: oklch(0.8 0.02 85); - background: var(--p); color: var(--ink); - font-family: "Besley", Georgia, serif; font-size: 14.5px; line-height: 1.55; -} -.cl-mono { font-family: "Spline Sans Mono", monospace; } -.cl-frame { border-top: 3px solid var(--ink); position: relative; padding-top: 3px; } -.cl-frame::before { content: ""; display: block; border-top: 1px solid var(--ink); } -.cl-lockup__name { font-size: 32px; font-weight: 800; letter-spacing: 0.02em; margin: 6px 0 0; line-height: 1; } -.cl-lockup__sub { font-family: "Spline Sans Mono", monospace; font-size: 10px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--green); } -.cl-statement { font-size: 25px; font-weight: 500; font-style: italic; line-height: 1.25; margin: 0; text-wrap: balance; } -.cl-statement b { font-style: normal; font-weight: 800; color: var(--green); } -.cl-card { border: 1px solid var(--ink); box-shadow: 0 0 0 1px var(--p), 0 0 0 2px var(--ink); margin: 2px; padding: 16px 18px; display: flex; flex-direction: column; gap: 9px; background: var(--p); } -.cl-card__head { display: flex; flex-direction: column; gap: 1px; } -.cl-card__client { font-size: 17px; font-weight: 700; } -.cl-card__no { font-family: "Spline Sans Mono", monospace; font-size: 10px; color: var(--green); } -.cl-scope { display: grid; grid-template-columns: 26px 1fr auto; gap: 10px; align-items: baseline; padding: 7px 0; border-top: 1px solid var(--line); } -.cl-scope__no { font-family: "Spline Sans Mono", monospace; font-size: 10px; color: oklch(0.55 0.02 60); } -.cl-scope__name { font-family: "Spline Sans Mono", monospace; font-size: 13px; font-weight: 600; } -.cl-scope__terms { font-family: "Spline Sans Mono", monospace; font-size: 10px; color: oklch(0.55 0.02 60); white-space: nowrap; } -.cl-tag { font-family: "Spline Sans Mono", monospace; font-size: 9.5px; letter-spacing: 0.1em; text-transform: uppercase; border: 1px solid var(--ink); padding: 4px 9px; white-space: nowrap; } -.cl-tag--green { border-color: var(--green); color: var(--green); } -.cl-btn { font-family: "Besley", serif; font-size: 13.5px; font-weight: 700; background: var(--ink); color: var(--p); border: 1px solid var(--ink); padding: 8px 16px; cursor: pointer; white-space: nowrap; } -.cl-btn--ghost { background: transparent; color: var(--ink); } + --p: oklch(0.972 0.009 85); + --ink: oklch(0.24 0.02 60); + --green: oklch(0.43 0.07 165); + --line: oklch(0.8 0.02 85); + background: var(--p); + color: var(--ink); + font-family: "Besley", Georgia, serif; + font-size: 14.5px; + line-height: 1.55; +} +.cl-mono { + font-family: "Spline Sans Mono", monospace; +} +.cl-frame { + border-top: 3px solid var(--ink); + position: relative; + padding-top: 3px; +} +.cl-frame::before { + content: ""; + display: block; + border-top: 1px solid var(--ink); +} +.cl-lockup__name { + font-size: 32px; + font-weight: 800; + letter-spacing: 0.02em; + margin: 6px 0 0; + line-height: 1; +} +.cl-lockup__sub { + font-family: "Spline Sans Mono", monospace; + font-size: 10px; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--green); +} +.cl-statement { + font-size: 25px; + font-weight: 500; + font-style: italic; + line-height: 1.25; + margin: 0; + text-wrap: balance; +} +.cl-statement b { + font-style: normal; + font-weight: 800; + color: var(--green); +} +.cl-card { + border: 1px solid var(--ink); + box-shadow: + 0 0 0 1px var(--p), + 0 0 0 2px var(--ink); + margin: 2px; + padding: 16px 18px; + display: flex; + flex-direction: column; + gap: 9px; + background: var(--p); +} +.cl-card__head { + display: flex; + flex-direction: column; + gap: 1px; +} +.cl-card__client { + font-size: 17px; + font-weight: 700; +} +.cl-card__no { + font-family: "Spline Sans Mono", monospace; + font-size: 10px; + color: var(--green); +} +.cl-scope { + display: grid; + grid-template-columns: 26px 1fr auto; + gap: 10px; + align-items: baseline; + padding: 7px 0; + border-top: 1px solid var(--line); +} +.cl-scope__no { + font-family: "Spline Sans Mono", monospace; + font-size: 10px; + color: oklch(0.55 0.02 60); +} +.cl-scope__name { + font-family: "Spline Sans Mono", monospace; + font-size: 13px; + font-weight: 600; +} +.cl-scope__terms { + font-family: "Spline Sans Mono", monospace; + font-size: 10px; + color: oklch(0.55 0.02 60); + white-space: nowrap; +} +.cl-tag { + font-family: "Spline Sans Mono", monospace; + font-size: 9.5px; + letter-spacing: 0.1em; + text-transform: uppercase; + border: 1px solid var(--ink); + padding: 4px 9px; + white-space: nowrap; +} +.cl-tag--green { + border-color: var(--green); + color: var(--green); +} +.cl-btn { + font-family: "Besley", serif; + font-size: 13.5px; + font-weight: 700; + background: var(--ink); + color: var(--p); + border: 1px solid var(--ink); + padding: 8px 16px; + cursor: pointer; + white-space: nowrap; +} +.cl-btn--ghost { + background: transparent; + color: var(--ink); +} /* ═══ 4 · PUNCH COLUMN ═══ */ .pc { - --p: oklch(0.93 0.028 92); --ink: oklch(0.27 0.02 75); - --rule: oklch(0.6 0.1 40 / 0.55); --slot: oklch(0.27 0.02 75); - background: var(--p); color: var(--ink); - font-family: "Archivo", system-ui, sans-serif; font-size: 13.5px; line-height: 1.5; -} -.pc-mono { font-family: "Martian Mono", monospace; } -.pc-lockup__name { font-family: "Martian Mono", monospace; font-size: 25px; font-weight: 700; letter-spacing: 0.06em; margin: 0; line-height: 1.1; } -.pc-lockup__sub { font-family: "Martian Mono", monospace; font-size: 8.5px; letter-spacing: 0.14em; text-transform: uppercase; color: oklch(0.5 0.06 40); } -.pc-statement { font-size: 24px; font-weight: 700; letter-spacing: -0.015em; line-height: 1.2; margin: 0; text-wrap: balance; } -.pc-card { border: 1.5px solid var(--ink); background: oklch(0.945 0.025 92); display: flex; flex-direction: column; } -.pc-card__head { display: flex; flex-direction: column; gap: 2px; padding: 12px 16px 8px; } -.pc-card__client { font-weight: 700; font-size: 15.5px; } -.pc-card__purpose { font-family: "Martian Mono", monospace; font-size: 8.5px; color: oklch(0.5 0.06 40); } -.pc-scope { display: grid; grid-template-columns: 34px 1fr auto; gap: 12px; align-items: center; padding: 8px 16px; border-top: 1px solid var(--rule); } -.pc-slot { width: 22px; height: 9px; border: 1.5px solid var(--ink); border-radius: 4.5px; } -.pc-slot--punched { background: var(--slot); } -.pc-scope__name { font-family: "Martian Mono", monospace; font-size: 11px; font-weight: 700; } -.pc-scope__terms { font-family: "Martian Mono", monospace; font-size: 8.5px; color: oklch(0.5 0.04 75); white-space: nowrap; } -.pc-index { display: flex; justify-content: space-between; padding: 6px 16px 8px; border-top: 1px solid var(--rule); font-family: "Martian Mono", monospace; font-size: 7.5px; color: var(--rule); letter-spacing: 0.2em; } -.pc-tag { font-family: "Martian Mono", monospace; font-size: 8.5px; letter-spacing: 0.08em; text-transform: uppercase; border: 1.5px solid var(--ink); padding: 4px 8px; white-space: nowrap; } -.pc-tag--punched { background: var(--ink); color: var(--p); } -.pc-btn { font-family: "Archivo", sans-serif; font-size: 12px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; background: var(--ink); color: var(--p); border: 1.5px solid var(--ink); padding: 8px 14px; cursor: pointer; white-space: nowrap; } -.pc-btn--ghost { background: transparent; color: var(--ink); } + --p: oklch(0.93 0.028 92); + --ink: oklch(0.27 0.02 75); + --rule: oklch(0.6 0.1 40 / 0.55); + --slot: oklch(0.27 0.02 75); + background: var(--p); + color: var(--ink); + font-family: "Archivo", system-ui, sans-serif; + font-size: 13.5px; + line-height: 1.5; +} +.pc-mono { + font-family: "Martian Mono", monospace; +} +.pc-lockup__name { + font-family: "Martian Mono", monospace; + font-size: 25px; + font-weight: 700; + letter-spacing: 0.06em; + margin: 0; + line-height: 1.1; +} +.pc-lockup__sub { + font-family: "Martian Mono", monospace; + font-size: 8.5px; + letter-spacing: 0.14em; + text-transform: uppercase; + color: oklch(0.5 0.06 40); +} +.pc-statement { + font-size: 24px; + font-weight: 700; + letter-spacing: -0.015em; + line-height: 1.2; + margin: 0; + text-wrap: balance; +} +.pc-card { + border: 1.5px solid var(--ink); + background: oklch(0.945 0.025 92); + display: flex; + flex-direction: column; +} +.pc-card__head { + display: flex; + flex-direction: column; + gap: 2px; + padding: 12px 16px 8px; +} +.pc-card__client { + font-weight: 700; + font-size: 15.5px; +} +.pc-card__purpose { + font-family: "Martian Mono", monospace; + font-size: 8.5px; + color: oklch(0.5 0.06 40); +} +.pc-scope { + display: grid; + grid-template-columns: 34px 1fr auto; + gap: 12px; + align-items: center; + padding: 8px 16px; + border-top: 1px solid var(--rule); +} +.pc-slot { + width: 22px; + height: 9px; + border: 1.5px solid var(--ink); + border-radius: 4.5px; +} +.pc-slot--punched { + background: var(--slot); +} +.pc-scope__name { + font-family: "Martian Mono", monospace; + font-size: 11px; + font-weight: 700; +} +.pc-scope__terms { + font-family: "Martian Mono", monospace; + font-size: 8.5px; + color: oklch(0.5 0.04 75); + white-space: nowrap; +} +.pc-index { + display: flex; + justify-content: space-between; + padding: 6px 16px 8px; + border-top: 1px solid var(--rule); + font-family: "Martian Mono", monospace; + font-size: 7.5px; + color: var(--rule); + letter-spacing: 0.2em; +} +.pc-tag { + font-family: "Martian Mono", monospace; + font-size: 8.5px; + letter-spacing: 0.08em; + text-transform: uppercase; + border: 1.5px solid var(--ink); + padding: 4px 8px; + white-space: nowrap; +} +.pc-tag--punched { + background: var(--ink); + color: var(--p); +} +.pc-btn { + font-family: "Archivo", sans-serif; + font-size: 12px; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; + background: var(--ink); + color: var(--p); + border: 1.5px solid var(--ink); + padding: 8px 14px; + cursor: pointer; + white-space: nowrap; +} +.pc-btn--ghost { + background: transparent; + color: var(--ink); +} /* ═══ 5 · CIVIC BOLD ═══ */ .cb { - --p: oklch(0.985 0.002 95); --ink: oklch(0.16 0 0); - --blue: oklch(0.45 0.12 262); --line: oklch(0.88 0.002 95); - background: var(--p); color: var(--ink); - font-family: "Archivo", system-ui, sans-serif; font-size: 14px; line-height: 1.5; -} -.cb-mono { font-family: "Fragment Mono", monospace; } -.cb-lockup__name { font-size: 40px; font-weight: 900; letter-spacing: -0.03em; margin: 0; line-height: 0.95; } -.cb-lockup__sub { font-family: "Fragment Mono", monospace; font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--blue); } -.cb-statement { font-size: 31px; font-weight: 900; letter-spacing: -0.025em; line-height: 1.04; margin: 0; text-transform: uppercase; max-width: 15ch; } -.cb-statement b { color: var(--blue); } -.cb-card { border: 2px solid var(--ink); padding: 14px 16px; display: flex; flex-direction: column; gap: 8px; } -.cb-card__head { display: flex; flex-direction: column; gap: 2px; } -.cb-card__client { font-size: 17px; font-weight: 900; letter-spacing: -0.01em; } -.cb-card__purpose { font-family: "Fragment Mono", monospace; font-size: 10px; color: var(--blue); } -.cb-scope { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; padding: 7px 0; border-top: 1px solid var(--line); } -.cb-scope__name { font-family: "Fragment Mono", monospace; font-size: 12.5px; font-weight: 400; } -.cb-scope__terms { font-family: "Fragment Mono", monospace; font-size: 10px; color: oklch(0.45 0 0); white-space: nowrap; } -.cb-tag { font-size: 10.5px; font-weight: 900; letter-spacing: 0.08em; text-transform: uppercase; background: var(--ink); color: var(--p); padding: 5px 9px; white-space: nowrap; } -.cb-tag--blue { background: var(--blue); } -.cb-tag--ghost { background: transparent; color: var(--ink); box-shadow: inset 0 0 0 2px var(--ink); } -.cb-btn { font-family: "Archivo", sans-serif; font-size: 12.5px; font-weight: 900; letter-spacing: 0.06em; text-transform: uppercase; background: var(--ink); color: var(--p); border: 2px solid var(--ink); padding: 9px 16px; cursor: pointer; white-space: nowrap; } -.cb-btn--ghost { background: transparent; color: var(--ink); } + --p: oklch(0.985 0.002 95); + --ink: oklch(0.16 0 0); + --blue: oklch(0.45 0.12 262); + --line: oklch(0.88 0.002 95); + background: var(--p); + color: var(--ink); + font-family: "Archivo", system-ui, sans-serif; + font-size: 14px; + line-height: 1.5; +} +.cb-mono { + font-family: "Fragment Mono", monospace; +} +.cb-lockup__name { + font-size: 40px; + font-weight: 900; + letter-spacing: -0.03em; + margin: 0; + line-height: 0.95; +} +.cb-lockup__sub { + font-family: "Fragment Mono", monospace; + font-size: 10px; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--blue); +} +.cb-statement { + font-size: 31px; + font-weight: 900; + letter-spacing: -0.025em; + line-height: 1.04; + margin: 0; + text-transform: uppercase; + max-width: 15ch; +} +.cb-statement b { + color: var(--blue); +} +.cb-card { + border: 2px solid var(--ink); + padding: 14px 16px; + display: flex; + flex-direction: column; + gap: 8px; +} +.cb-card__head { + display: flex; + flex-direction: column; + gap: 2px; +} +.cb-card__client { + font-size: 17px; + font-weight: 900; + letter-spacing: -0.01em; +} +.cb-card__purpose { + font-family: "Fragment Mono", monospace; + font-size: 10px; + color: var(--blue); +} +.cb-scope { + display: flex; + justify-content: space-between; + align-items: baseline; + gap: 12px; + padding: 7px 0; + border-top: 1px solid var(--line); +} +.cb-scope__name { + font-family: "Fragment Mono", monospace; + font-size: 12.5px; + font-weight: 400; +} +.cb-scope__terms { + font-family: "Fragment Mono", monospace; + font-size: 10px; + color: oklch(0.45 0 0); + white-space: nowrap; +} +.cb-tag { + font-size: 10.5px; + font-weight: 900; + letter-spacing: 0.08em; + text-transform: uppercase; + background: var(--ink); + color: var(--p); + padding: 5px 9px; + white-space: nowrap; +} +.cb-tag--blue { + background: var(--blue); +} +.cb-tag--ghost { + background: transparent; + color: var(--ink); + box-shadow: inset 0 0 0 2px var(--ink); +} +.cb-btn { + font-family: "Archivo", sans-serif; + font-size: 12.5px; + font-weight: 900; + letter-spacing: 0.06em; + text-transform: uppercase; + background: var(--ink); + color: var(--p); + border: 2px solid var(--ink); + padding: 9px 16px; + cursor: pointer; + white-space: nowrap; +} +.cb-btn--ghost { + background: transparent; + color: var(--ink); +} /* ═══ 6 · GREENBAR ═══ */ .gb { - --p: oklch(0.99 0.004 140); --band: oklch(0.945 0.028 162); - --ink: oklch(0.24 0.012 250); --red: oklch(0.55 0.18 27); + --p: oklch(0.99 0.004 140); + --band: oklch(0.945 0.028 162); + --ink: oklch(0.24 0.012 250); + --red: oklch(0.55 0.18 27); --green-deep: oklch(0.42 0.06 165); - background: var(--p); color: var(--ink); - font-family: "IBM Plex Mono", monospace; font-size: 12.5px; line-height: 1.6; + background: var(--p); + color: var(--ink); + font-family: "IBM Plex Mono", monospace; + font-size: 12.5px; + line-height: 1.6; position: relative; } -.gb-lockup__name { font-size: 23px; font-weight: 600; letter-spacing: 0.18em; text-indent: 0.18em; margin: 0; } -.gb-lockup__sub { font-size: 9.5px; color: var(--green-deep); letter-spacing: 0.12em; text-transform: uppercase; } -.gb-statement { font-family: "Archivo", sans-serif; font-size: 25px; font-weight: 700; letter-spacing: -0.015em; line-height: 1.18; margin: 0; text-wrap: balance; } -.gb-statement b { color: var(--green-deep); } -.gb-card { border: 1px solid var(--green-deep); display: flex; flex-direction: column; overflow: hidden; } -.gb-card__head { display: flex; flex-direction: column; gap: 1px; padding: 9px 14px; background: var(--band); } -.gb-card__client { font-family: "Archivo", sans-serif; font-size: 14.5px; font-weight: 700; } -.gb-card__purpose { font-size: 9.5px; color: var(--green-deep); } -.gb-row { display: grid; grid-template-columns: 1fr auto; align-items: baseline; gap: 12px; padding: 7px 14px; font-size: 11.5px; } -.gb-row > span:first-child { white-space: nowrap; } -.gb-row:nth-child(odd) { background: var(--band); } -.gb-row__terms { color: oklch(0.5 0.02 250); white-space: nowrap; font-size: 10px; } -.gb-tag { font-size: 9.5px; letter-spacing: 0.08em; text-transform: uppercase; border: 1px solid var(--green-deep); color: var(--green-deep); padding: 4px 8px; white-space: nowrap; background: var(--p); } -.gb-tag--solid { background: var(--green-deep); color: var(--p); } -.gb-tag--red { border-color: var(--red); color: var(--red); } -.gb-btn { font-family: "IBM Plex Mono", monospace; font-size: 11px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; background: var(--ink); color: var(--p); border: 1px solid var(--ink); padding: 8px 14px; cursor: pointer; white-space: nowrap; } -.gb-btn--ghost { background: transparent; color: var(--ink); } +.gb-lockup__name { + font-size: 23px; + font-weight: 600; + letter-spacing: 0.18em; + text-indent: 0.18em; + margin: 0; +} +.gb-lockup__sub { + font-size: 9.5px; + color: var(--green-deep); + letter-spacing: 0.12em; + text-transform: uppercase; +} +.gb-statement { + font-family: "Archivo", sans-serif; + font-size: 25px; + font-weight: 700; + letter-spacing: -0.015em; + line-height: 1.18; + margin: 0; + text-wrap: balance; +} +.gb-statement b { + color: var(--green-deep); +} +.gb-card { + border: 1px solid var(--green-deep); + display: flex; + flex-direction: column; + overflow: hidden; +} +.gb-card__head { + display: flex; + flex-direction: column; + gap: 1px; + padding: 9px 14px; + background: var(--band); +} +.gb-card__client { + font-family: "Archivo", sans-serif; + font-size: 14.5px; + font-weight: 700; +} +.gb-card__purpose { + font-size: 9.5px; + color: var(--green-deep); +} +.gb-row { + display: grid; + grid-template-columns: 1fr auto; + align-items: baseline; + gap: 12px; + padding: 7px 14px; + font-size: 11.5px; +} +.gb-row > span:first-child { + white-space: nowrap; +} +.gb-row:nth-child(odd) { + background: var(--band); +} +.gb-row__terms { + color: oklch(0.5 0.02 250); + white-space: nowrap; + font-size: 10px; +} +.gb-tag { + font-size: 9.5px; + letter-spacing: 0.08em; + text-transform: uppercase; + border: 1px solid var(--green-deep); + color: var(--green-deep); + padding: 4px 8px; + white-space: nowrap; + background: var(--p); +} +.gb-tag--solid { + background: var(--green-deep); + color: var(--p); +} +.gb-tag--red { + border-color: var(--red); + color: var(--red); +} +.gb-btn { + font-family: "IBM Plex Mono", monospace; + font-size: 11px; + font-weight: 600; + letter-spacing: 0.06em; + text-transform: uppercase; + background: var(--ink); + color: var(--p); + border: 1px solid var(--ink); + padding: 8px 14px; + cursor: pointer; + white-space: nowrap; +} +.gb-btn--ghost { + background: transparent; + color: var(--ink); +} diff --git a/docs/design-system/ink-carbon/project/reinvention/round3.css b/docs/design-system/ink-carbon/project/reinvention/round3.css index 6e2c0120a..c2e073c7e 100644 --- a/docs/design-system/ink-carbon/project/reinvention/round3.css +++ b/docs/design-system/ink-carbon/project/reinvention/round3.css @@ -13,7 +13,7 @@ t3 CIVIC INK — Hanken Grotesk · near-monochrome; one working blue reserved for actions. Statuses are ink glyphs. ═══════════════════════════════════════════════════════════════════ */ -@import url('https://fonts.googleapis.com/css2?family=Schibsted+Grotesk:ital,wght@0,400..900;1,400..700&family=JetBrains+Mono:ital,wght@0,400;0,500;0,600;1,400&family=Archivo:wght@500;600;700&family=IBM+Plex+Mono:ital,wght@0,400;0,500;0,600;1,400&family=Hanken+Grotesk:ital,wght@0,400;0,500;0,600;0,700;0,800;1,400&family=Fragment+Mono:ital@0;1&display=swap'); +@import url("https://fonts.googleapis.com/css2?family=Schibsted+Grotesk:ital,wght@0,400..900;1,400..700&family=JetBrains+Mono:ital,wght@0,400;0,500;0,600;1,400&family=Archivo:wght@500;600;700&family=IBM+Plex+Mono:ital,wght@0,400;0,500;0,600;1,400&family=Hanken+Grotesk:ital,wght@0,400;0,500;0,600;0,700;0,800;1,400&family=Fragment+Mono:ital@0;1&display=swap"); /* ─── Variant tokens ─── */ .t1 { @@ -23,22 +23,22 @@ --pro-dk: oklch(0.74 0.13 255); --hum: oklch(0.55 0.11 45); --hum-dk: oklch(0.76 0.12 45); - --ok: oklch(0.50 0.11 158); + --ok: oklch(0.5 0.11 158); --ok-dk: oklch(0.76 0.13 158); --bad: oklch(0.52 0.16 27); - --bad-dk: oklch(0.70 0.16 27); + --bad-dk: oklch(0.7 0.16 27); --dupe-mech: 1; } .t2 { --fs: "Archivo", system-ui, sans-serif; --fm: "IBM Plex Mono", ui-monospace, monospace; --pro: oklch(0.45 0.08 195); - --pro-dk: oklch(0.78 0.10 185); + --pro-dk: oklch(0.78 0.1 185); --hum: oklch(0.34 0.02 80); --hum-dk: oklch(0.88 0.02 85); --ok: oklch(0.48 0.09 175); - --ok-dk: oklch(0.78 0.10 175); - --bad: oklch(0.30 0.01 80); + --ok-dk: oklch(0.78 0.1 175); + --bad: oklch(0.3 0.01 80); --bad-dk: oklch(0.85 0.01 85); --mono-led: 1; } @@ -49,9 +49,9 @@ --pro-dk: oklch(0.72 0.15 262); --hum: oklch(0.25 0 0); --hum-dk: oklch(0.92 0 0); - --ok: oklch(0.30 0 0); + --ok: oklch(0.3 0 0); --ok-dk: oklch(0.88 0 0); - --bad: oklch(0.30 0 0); + --bad: oklch(0.3 0 0); --bad-dk: oklch(0.88 0 0); } @@ -61,9 +61,12 @@ --card: oklch(1 0 0); --fg: oklch(0.18 0.005 270); --mut: oklch(0.48 0.008 270); - --line: oklch(0.90 0.005 270); - --line-strong: oklch(0.80 0.006 270); - --p: var(--pro); --h: var(--hum); --g: var(--ok); --r: var(--bad); + --line: oklch(0.9 0.005 270); + --line-strong: oklch(0.8 0.006 270); + --p: var(--pro); + --h: var(--hum); + --g: var(--ok); + --r: var(--bad); } .dark { --bg: oklch(0.17 0.006 262); @@ -71,114 +74,535 @@ --fg: oklch(0.95 0.005 262); --mut: oklch(0.66 0.01 262); --line: oklch(0.29 0.008 262); - --line-strong: oklch(0.40 0.008 262); - --p: var(--pro-dk); --h: var(--hum-dk); --g: var(--ok-dk); --r: var(--bad-dk); + --line-strong: oklch(0.4 0.008 262); + --p: var(--pro-dk); + --h: var(--hum-dk); + --g: var(--ok-dk); + --r: var(--bad-dk); +} +.t2.lite { + --bg: oklch(0.97 0.008 95); + --card: oklch(0.985 0.006 95); } -.t2.lite { --bg: oklch(0.97 0.008 95); --card: oklch(0.985 0.006 95); } /* ═══ Shared skeleton ═══ */ -.s3 { height: 100%; background: var(--bg); color: var(--fg); font-family: var(--fs); font-size: 14px; line-height: 1.5; } -.s3 * { box-sizing: border-box; } -.s3 b { font-weight: 600; } -.mono { font-family: var(--fm); } +.s3 { + height: 100%; + background: var(--bg); + color: var(--fg); + font-family: var(--fs); + font-size: 14px; + line-height: 1.5; +} +.s3 * { + box-sizing: border-box; +} +.s3 b { + font-weight: 600; +} +.mono { + font-family: var(--fm); +} /* ═══ HOSTED CONSENT (light) ═══ */ -.hc { display: flex; flex-direction: column; padding: 0; } -.hc-head { display: flex; align-items: center; gap: 10px; padding: 18px 34px; border-bottom: 1px solid var(--line); } -.hc-mark { width: 22px; height: 22px; background: var(--fg); position: relative; flex-shrink: 0; } -.hc-mark::after { content: ""; position: absolute; inset: 5px 5px 9px 9px; background: var(--bg); } -.t3 .hc-mark { background: var(--p); } -.hc-word { font-weight: 700; font-size: 14px; letter-spacing: 0.01em; } -.t2 .hc-word { font-family: var(--fm); font-weight: 600; letter-spacing: 0.12em; } -.hc-prov { margin-left: auto; font-family: var(--fm); font-size: 11px; color: var(--mut); } -.hc-main { flex: 1; display: flex; flex-direction: column; gap: 20px; padding: 28px 34px 30px; } -.hc-eyebrow { font-family: var(--fm); font-size: 10.5px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--p); } -.hc-title { font-size: 24px; font-weight: 700; letter-spacing: -0.02em; line-height: 1.2; margin: 6px 0 0; } -.t2 .hc-title { font-family: var(--fs); font-weight: 600; letter-spacing: -0.01em; font-size: 21px; } -.t3 .hc-title { font-weight: 800; letter-spacing: -0.025em; } -.hc-lede { font-size: 14px; color: var(--mut); margin: 8px 0 0; max-width: 46ch; } -.hc-lede b { color: var(--fg); } +.hc { + display: flex; + flex-direction: column; + padding: 0; +} +.hc-head { + display: flex; + align-items: center; + gap: 10px; + padding: 18px 34px; + border-bottom: 1px solid var(--line); +} +.hc-mark { + width: 22px; + height: 22px; + background: var(--fg); + position: relative; + flex-shrink: 0; +} +.hc-mark::after { + content: ""; + position: absolute; + inset: 5px 5px 9px 9px; + background: var(--bg); +} +.t3 .hc-mark { + background: var(--p); +} +.hc-word { + font-weight: 700; + font-size: 14px; + letter-spacing: 0.01em; +} +.t2 .hc-word { + font-family: var(--fm); + font-weight: 600; + letter-spacing: 0.12em; +} +.hc-prov { + margin-left: auto; + font-family: var(--fm); + font-size: 11px; + color: var(--mut); +} +.hc-main { + flex: 1; + display: flex; + flex-direction: column; + gap: 20px; + padding: 28px 34px 30px; +} +.hc-eyebrow { + font-family: var(--fm); + font-size: 10.5px; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--p); +} +.hc-title { + font-size: 24px; + font-weight: 700; + letter-spacing: -0.02em; + line-height: 1.2; + margin: 6px 0 0; +} +.t2 .hc-title { + font-family: var(--fs); + font-weight: 600; + letter-spacing: -0.01em; + font-size: 21px; +} +.t3 .hc-title { + font-weight: 800; + letter-spacing: -0.025em; +} +.hc-lede { + font-size: 14px; + color: var(--mut); + margin: 8px 0 0; + max-width: 46ch; +} +.hc-lede b { + color: var(--fg); +} /* the request sheet (+ carbon mechanics per variant) */ -.hc-sheetwrap { position: relative; } -.hc-carbon { position: absolute; inset: 0; transform: translate(9px, 9px); background: color-mix(in oklab, var(--p) 9%, transparent); border: 1px solid color-mix(in oklab, var(--p) 35%, transparent); } -.hc-sheet { position: relative; background: var(--card); border: 1px solid var(--line-strong); display: flex; flex-direction: column; } -.t3 .hc-sheet { border: 2px solid var(--fg); } -.hc-sheet__head { display: flex; justify-content: space-between; align-items: baseline; gap: 14px; padding: 13px 18px; border-bottom: 1px solid var(--line); } -.hc-sheet__client { font-size: 16px; font-weight: 700; } -.hc-sheet__serial { font-family: var(--fm); font-size: 10.5px; color: var(--mut); white-space: nowrap; } -.hc-scope { display: grid; grid-template-columns: 1fr auto; gap: 4px 14px; padding: 11px 18px; border-bottom: 1px solid var(--line); } -.hc-scope:last-of-type { border-bottom: 0; } -.hc-scope__name { font-family: var(--fm); font-size: 13px; font-weight: 500; color: var(--fg); } -.hc-scope__terms { font-family: var(--fm); font-size: 10.5px; color: var(--mut); white-space: nowrap; align-self: center; } -.hc-scope__desc { font-size: 12px; color: var(--mut); grid-column: 1 / -1; } -.hc-sheet__foot { display: flex; justify-content: space-between; align-items: center; gap: 12px; padding: 11px 18px; border-top: 1px solid var(--line-strong); } -.hc-copyline { font-family: var(--fm); font-size: 9.5px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--p); white-space: nowrap; } -.hc-sheet__foot .hc-meta { white-space: nowrap; } -.hc-meta { display: flex; gap: 16px; font-family: var(--fm); font-size: 10.5px; color: var(--mut); } -.hc-foot { display: flex; gap: 10px; padding: 16px 34px 20px; border-top: 1px solid var(--line); } -.hc-btn { font-family: var(--fs); font-size: 13.5px; font-weight: 700; padding: 10px 20px; cursor: pointer; border: 1.5px solid var(--fg); background: var(--fg); color: var(--bg); white-space: nowrap; } -.hc-btn--go { background: var(--h); border-color: var(--h); color: oklch(0.99 0.005 90); } -.t2 .hc-btn { font-family: var(--fm); font-weight: 600; font-size: 12px; letter-spacing: 0.04em; } -.t3 .hc-btn--go { background: var(--p); border-color: var(--p); } -.hc-btn--ghost { background: transparent; color: var(--fg); border-color: var(--line-strong); font-weight: 600; } -.hc-revnote { margin-left: auto; align-self: center; font-size: 11.5px; color: var(--mut); } +.hc-sheetwrap { + position: relative; +} +.hc-carbon { + position: absolute; + inset: 0; + transform: translate(9px, 9px); + background: color-mix(in oklab, var(--p) 9%, transparent); + border: 1px solid color-mix(in oklab, var(--p) 35%, transparent); +} +.hc-sheet { + position: relative; + background: var(--card); + border: 1px solid var(--line-strong); + display: flex; + flex-direction: column; +} +.t3 .hc-sheet { + border: 2px solid var(--fg); +} +.hc-sheet__head { + display: flex; + justify-content: space-between; + align-items: baseline; + gap: 14px; + padding: 13px 18px; + border-bottom: 1px solid var(--line); +} +.hc-sheet__client { + font-size: 16px; + font-weight: 700; +} +.hc-sheet__serial { + font-family: var(--fm); + font-size: 10.5px; + color: var(--mut); + white-space: nowrap; +} +.hc-scope { + display: grid; + grid-template-columns: 1fr auto; + gap: 4px 14px; + padding: 11px 18px; + border-bottom: 1px solid var(--line); +} +.hc-scope:last-of-type { + border-bottom: 0; +} +.hc-scope__name { + font-family: var(--fm); + font-size: 13px; + font-weight: 500; + color: var(--fg); +} +.hc-scope__terms { + font-family: var(--fm); + font-size: 10.5px; + color: var(--mut); + white-space: nowrap; + align-self: center; +} +.hc-scope__desc { + font-size: 12px; + color: var(--mut); + grid-column: 1 / -1; +} +.hc-sheet__foot { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + padding: 11px 18px; + border-top: 1px solid var(--line-strong); +} +.hc-copyline { + font-family: var(--fm); + font-size: 9.5px; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--p); + white-space: nowrap; +} +.hc-sheet__foot .hc-meta { + white-space: nowrap; +} +.hc-meta { + display: flex; + gap: 16px; + font-family: var(--fm); + font-size: 10.5px; + color: var(--mut); +} +.hc-foot { + display: flex; + gap: 10px; + padding: 16px 34px 20px; + border-top: 1px solid var(--line); +} +.hc-btn { + font-family: var(--fs); + font-size: 13.5px; + font-weight: 700; + padding: 10px 20px; + cursor: pointer; + border: 1.5px solid var(--fg); + background: var(--fg); + color: var(--bg); + white-space: nowrap; +} +.hc-btn--go { + background: var(--h); + border-color: var(--h); + color: oklch(0.99 0.005 90); +} +.t2 .hc-btn { + font-family: var(--fm); + font-weight: 600; + font-size: 12px; + letter-spacing: 0.04em; +} +.t3 .hc-btn--go { + background: var(--p); + border-color: var(--p); +} +.hc-btn--ghost { + background: transparent; + color: var(--fg); + border-color: var(--line-strong); + font-weight: 600; +} +.hc-revnote { + margin-left: auto; + align-self: center; + font-size: 11.5px; + color: var(--mut); +} /* ═══ OPERATOR CONSOLE (dark) ═══ */ -.oc { display: grid; grid-template-columns: 184px 1fr; } -.oc-side { border-right: 1px solid var(--line); padding: 16px 0 14px; display: flex; flex-direction: column; gap: 2px; } -.oc-side__brand { display: flex; align-items: center; gap: 8px; padding: 0 16px 14px; } -.oc-side__brand .hc-mark { width: 18px; height: 18px; } -.oc-side__brand .hc-mark::after { inset: 4px 4px 7px 7px; } -.oc-side__word { font-weight: 700; font-size: 13px; } -.t2 .oc-side__word { font-family: var(--fm); letter-spacing: 0.1em; font-weight: 600; } -.oc-nav { display: flex; flex-direction: column; } -.oc-nav__item { display: flex; justify-content: space-between; align-items: center; gap: 8px; padding: 6px 16px; font-size: 12.5px; font-weight: 500; color: var(--mut); white-space: nowrap; } -.oc-nav__item.is-on { color: var(--fg); background: color-mix(in oklab, var(--p) 14%, transparent); box-shadow: inset 2px 0 0 var(--p); } -.t3 .oc-nav__item.is-on { background: transparent; box-shadow: inset 3px 0 0 var(--p); } -.oc-nav__count { font-family: var(--fm); font-size: 10px; color: var(--mut); } -.oc-side__foot { margin-top: auto; padding: 12px 16px 0; font-family: var(--fm); font-size: 9px; color: var(--mut); line-height: 1.7; border-top: 1px solid var(--line); } -.oc-main { padding: 20px 24px 22px; display: flex; flex-direction: column; gap: 18px; min-width: 0; } -.oc-top { display: flex; justify-content: space-between; align-items: baseline; gap: 14px; } -.oc-h1 { font-size: 19px; font-weight: 700; letter-spacing: -0.015em; margin: 0; } -.t3 .oc-h1 { font-weight: 800; } -.oc-kbd { font-family: var(--fm); font-size: 10px; color: var(--mut); border: 1px solid var(--line-strong); padding: 3px 8px; white-space: nowrap; } +.oc { + display: grid; + grid-template-columns: 184px 1fr; +} +.oc-side { + border-right: 1px solid var(--line); + padding: 16px 0 14px; + display: flex; + flex-direction: column; + gap: 2px; +} +.oc-side__brand { + display: flex; + align-items: center; + gap: 8px; + padding: 0 16px 14px; +} +.oc-side__brand .hc-mark { + width: 18px; + height: 18px; +} +.oc-side__brand .hc-mark::after { + inset: 4px 4px 7px 7px; +} +.oc-side__word { + font-weight: 700; + font-size: 13px; +} +.t2 .oc-side__word { + font-family: var(--fm); + letter-spacing: 0.1em; + font-weight: 600; +} +.oc-nav { + display: flex; + flex-direction: column; +} +.oc-nav__item { + display: flex; + justify-content: space-between; + align-items: center; + gap: 8px; + padding: 6px 16px; + font-size: 12.5px; + font-weight: 500; + color: var(--mut); + white-space: nowrap; +} +.oc-nav__item.is-on { + color: var(--fg); + background: color-mix(in oklab, var(--p) 14%, transparent); + box-shadow: inset 2px 0 0 var(--p); +} +.t3 .oc-nav__item.is-on { + background: transparent; + box-shadow: inset 3px 0 0 var(--p); +} +.oc-nav__count { + font-family: var(--fm); + font-size: 10px; + color: var(--mut); +} +.oc-side__foot { + margin-top: auto; + padding: 12px 16px 0; + font-family: var(--fm); + font-size: 9px; + color: var(--mut); + line-height: 1.7; + border-top: 1px solid var(--line); +} +.oc-main { + padding: 20px 24px 22px; + display: flex; + flex-direction: column; + gap: 18px; + min-width: 0; +} +.oc-top { + display: flex; + justify-content: space-between; + align-items: baseline; + gap: 14px; +} +.oc-h1 { + font-size: 19px; + font-weight: 700; + letter-spacing: -0.015em; + margin: 0; +} +.t3 .oc-h1 { + font-weight: 800; +} +.oc-kbd { + font-family: var(--fm); + font-size: 10px; + color: var(--mut); + border: 1px solid var(--line-strong); + padding: 3px 8px; + white-space: nowrap; +} /* dataset summary band */ -.oc-band { display: grid; grid-template-columns: repeat(4, 1fr); border: 1px solid var(--line); } -.oc-cell { padding: 12px 14px; border-left: 1px solid var(--line); display: flex; flex-direction: column; gap: 1px; } -.oc-cell:first-child { border-left: 0; } -.oc-cell__v { font-family: var(--fm); font-size: 19px; font-weight: 600; font-variant-numeric: tabular-nums; letter-spacing: -0.01em; } -.oc-cell__k { font-size: 10.5px; color: var(--mut); letter-spacing: 0.04em; text-transform: uppercase; font-weight: 500; } -.t2 .oc-cell__k { font-family: var(--fm); font-size: 9.5px; letter-spacing: 0.1em; } +.oc-band { + display: grid; + grid-template-columns: repeat(4, 1fr); + border: 1px solid var(--line); +} +.oc-cell { + padding: 12px 14px; + border-left: 1px solid var(--line); + display: flex; + flex-direction: column; + gap: 1px; +} +.oc-cell:first-child { + border-left: 0; +} +.oc-cell__v { + font-family: var(--fm); + font-size: 19px; + font-weight: 600; + font-variant-numeric: tabular-nums; + letter-spacing: -0.01em; +} +.oc-cell__k { + font-size: 10.5px; + color: var(--mut); + letter-spacing: 0.04em; + text-transform: uppercase; + font-weight: 500; +} +.t2 .oc-cell__k { + font-family: var(--fm); + font-size: 9.5px; + letter-spacing: 0.1em; +} /* grants data list */ -.oc-listhead { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; margin-top: 2px; } -.oc-h2 { font-size: 13px; font-weight: 700; letter-spacing: 0.01em; margin: 0; } -.t2 .oc-h2 { font-family: var(--fm); font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; font-size: 11px; } -.oc-link { font-size: 11.5px; color: var(--p); font-weight: 600; } -.oc-list { border-top: 1px solid var(--line-strong); } -.oc-row { display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(0, 1.6fr) auto auto; gap: 14px; align-items: center; padding: 10px 2px; border-bottom: 1px solid var(--line); position: relative; } -.oc-row__who { display: flex; flex-direction: column; gap: 1px; min-width: 0; } -.oc-row__client { font-size: 13.5px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.oc-row__id { font-family: var(--fm); font-size: 9.5px; color: var(--mut); white-space: nowrap; } -.oc-row__scopes { font-family: var(--fm); font-size: 11px; color: var(--mut); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.oc-row__exp { font-family: var(--fm); font-size: 10.5px; color: var(--mut); white-space: nowrap; font-variant-numeric: tabular-nums; } -.oc-row--dupe::after { content: ""; position: absolute; left: 4px; right: -4px; top: 4px; bottom: -4px; border: 1px solid color-mix(in oklab, var(--p) 30%, transparent); pointer-events: none; } +.oc-listhead { + display: flex; + justify-content: space-between; + align-items: baseline; + gap: 12px; + margin-top: 2px; +} +.oc-h2 { + font-size: 13px; + font-weight: 700; + letter-spacing: 0.01em; + margin: 0; +} +.t2 .oc-h2 { + font-family: var(--fm); + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + font-size: 11px; +} +.oc-link { + font-size: 11.5px; + color: var(--p); + font-weight: 600; +} +.oc-list { + border-top: 1px solid var(--line-strong); +} +.oc-row { + display: grid; + grid-template-columns: minmax(0, 1.25fr) minmax(0, 1.6fr) auto auto; + gap: 14px; + align-items: center; + padding: 10px 2px; + border-bottom: 1px solid var(--line); + position: relative; +} +.oc-row__who { + display: flex; + flex-direction: column; + gap: 1px; + min-width: 0; +} +.oc-row__client { + font-size: 13.5px; + font-weight: 600; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.oc-row__id { + font-family: var(--fm); + font-size: 9.5px; + color: var(--mut); + white-space: nowrap; +} +.oc-row__scopes { + font-family: var(--fm); + font-size: 11px; + color: var(--mut); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.oc-row__exp { + font-family: var(--fm); + font-size: 10.5px; + color: var(--mut); + white-space: nowrap; + font-variant-numeric: tabular-nums; +} +.oc-row--dupe::after { + content: ""; + position: absolute; + left: 4px; + right: -4px; + top: 4px; + bottom: -4px; + border: 1px solid color-mix(in oklab, var(--p) 30%, transparent); + pointer-events: none; +} .oc-row--revoked .oc-row__client, -.oc-row--revoked .oc-row__scopes { text-decoration: line-through; text-decoration-thickness: 1px; color: var(--mut); } +.oc-row--revoked .oc-row__scopes { + text-decoration: line-through; + text-decoration-thickness: 1px; + color: var(--mut); +} /* status endorsements — variant-differentiated */ -.st { font-family: var(--fm); font-size: 10px; white-space: nowrap; letter-spacing: 0.06em; } -.t1 .st { padding: 3px 8px; box-shadow: inset 0 0 0 1px color-mix(in oklab, currentColor 45%, transparent); background: color-mix(in oklab, currentColor 12%, transparent); } -.t1 .st-ok { color: var(--g); } -.t1 .st-pro { color: var(--p); } -.t1 .st-warn { color: var(--h); } -.t1 .st-off { color: var(--mut); box-shadow: inset 0 0 0 1px var(--line-strong); background: transparent; } -.t2 .st-ok { color: var(--g); } -.t2 .st-pro { color: var(--p); } -.t2 .st-warn { color: var(--h); } -.t2 .st-off { color: var(--mut); } -.t3 .st { letter-spacing: 0.02em; font-size: 10.5px; } -.t3 .st-ok, .t3 .st-pro, .t3 .st-warn { color: var(--fg); } -.t3 .st-off { color: var(--mut); } +.st { + font-family: var(--fm); + font-size: 10px; + white-space: nowrap; + letter-spacing: 0.06em; +} +.t1 .st { + padding: 3px 8px; + box-shadow: inset 0 0 0 1px color-mix(in oklab, currentColor 45%, transparent); + background: color-mix(in oklab, currentColor 12%, transparent); +} +.t1 .st-ok { + color: var(--g); +} +.t1 .st-pro { + color: var(--p); +} +.t1 .st-warn { + color: var(--h); +} +.t1 .st-off { + color: var(--mut); + box-shadow: inset 0 0 0 1px var(--line-strong); + background: transparent; +} +.t2 .st-ok { + color: var(--g); +} +.t2 .st-pro { + color: var(--p); +} +.t2 .st-warn { + color: var(--h); +} +.t2 .st-off { + color: var(--mut); +} +.t3 .st { + letter-spacing: 0.02em; + font-size: 10.5px; +} +.t3 .st-ok, +.t3 .st-pro, +.t3 .st-warn { + color: var(--fg); +} +.t3 .st-off { + color: var(--mut); +} diff --git a/docs/design-system/ink-carbon/project/ui_kits/web/ConsentCard.jsx b/docs/design-system/ink-carbon/project/ui_kits/web/ConsentCard.jsx index b7e82e864..da6e3fd62 100644 --- a/docs/design-system/ink-carbon/project/ui_kits/web/ConsentCard.jsx +++ b/docs/design-system/ink-carbon/project/ui_kits/web/ConsentCard.jsx @@ -4,64 +4,137 @@ // ConsentCard — the user-facing grant-approval surface. Human-temperature. const DEFAULT_SCOPES = [ - { id: 'pay', title: 'Pay statements', sub: 'Employer, pay period, gross & net pay.', scope: 'pay_statements.read', tag: 'append only', retention: '2y 1mo', on: true }, - { id: 'emp', title: 'Employment', sub: 'Current and previous employers with dates.', scope: 'employment.read', tag: 'mutable state', retention: 'current + 5y', on: true }, - { id: 'tax', title: 'Tax documents', sub: 'W-2 and 1099 forms issued to you.', scope: 'tax_docs.read', tag: 'append only', retention: '3y history', on: false }, + { + id: "pay", + title: "Pay statements", + sub: "Employer, pay period, gross & net pay.", + scope: "pay_statements.read", + tag: "append only", + retention: "2y 1mo", + on: true, + }, + { + id: "emp", + title: "Employment", + sub: "Current and previous employers with dates.", + scope: "employment.read", + tag: "mutable state", + retention: "current + 5y", + on: true, + }, + { + id: "tax", + title: "Tax documents", + sub: "W-2 and 1099 forms issued to you.", + scope: "tax_docs.read", + tag: "append only", + retention: "3y history", + on: false, + }, ]; const ConsentCard = () => { const [scopes, setScopes] = React.useState(DEFAULT_SCOPES); - const toggle = id => setScopes(s => s.map(x => x.id === id ? { ...x, on: !x.on } : x)); - const anyOn = scopes.some(s => s.on); + const toggle = (id) => setScopes((s) => s.map((x) => (x.id === id ? { ...x, on: !x.on } : x))); + const anyOn = scopes.some((s) => s.on); return ( -
-
-
CONSENT · SECTION 3
+
+
+
+ CONSENT · SECTION 3 +
Longview Planning wants access to your data
-

- They’ll use it for long-term financial planning. You can revoke at any time. +

+ They’ll use it for{" "} + + long-term financial planning + + . You can revoke at any time.

-
+
{scopes.map((s, i) => ( -
toggle(s.id)} style={{ - display: 'grid', gridTemplateColumns: '24px 1fr auto', gap: 12, - padding: '14px 24px', borderTop: i > 0 ? '1px solid var(--border)' : 'none', - cursor: 'pointer', alignItems: 'flex-start', - }}> -
- {s.on && - - } +
toggle(s.id)} + style={{ + display: "grid", + gridTemplateColumns: "24px 1fr auto", + gap: 12, + padding: "14px 24px", + borderTop: i > 0 ? "1px solid var(--border)" : "none", + cursor: "pointer", + alignItems: "flex-start", + }} + > +
+ {s.on && ( + + + + )}
{s.title}
-
{s.sub}
-
+
+ {s.sub} +
+
{s.tag} - {s.scope} + + {s.scope} +
- {s.retention} + + {s.retention} +
))}
-
-
- +
+
+ These are their commitments, not enforced by your server. -
- - +
+ +
diff --git a/docs/design-system/ink-carbon/project/ui_kits/web/GrantInspector.jsx b/docs/design-system/ink-carbon/project/ui_kits/web/GrantInspector.jsx index 32dba0762..050bbcfec 100644 --- a/docs/design-system/ink-carbon/project/ui_kits/web/GrantInspector.jsx +++ b/docs/design-system/ink-carbon/project/ui_kits/web/GrantInspector.jsx @@ -4,50 +4,81 @@ // GrantInspector — the protocol-temperature companion to ConsentCard. // Shows a grant "as issued" in machine terms. -const GrantInspector = ({ grantId = 'grt_longview01' }) => ( -
-
-
+const GrantInspector = ({ grantId = "grt_longview01" }) => ( +
+
+
-
GRANT · ISSUED
-
+
+ GRANT · ISSUED +
+
{grantId}
- active + + + active +
-
-
+
+
{[ - ['purpose', long_term_financial_planning], - ['mode', 'continuous'], - ['scopes', pay_statements.read · employment.read], - ['fields', 'employer, pay_period, gross_pay, net_pay'], - ['time_range', 'last 2y 1mo'], - ['issued', '2025-10-14T09:22:07Z'], - ['expires', '2025-12-14T09:22:07Z'], + ["purpose", long_term_financial_planning], + ["mode", "continuous"], + ["scopes", pay_statements.read · employment.read], + ["fields", "employer, pay_period, gross_pay, net_pay"], + ["time_range", "last 2y 1mo"], + ["issued", "2025-10-14T09:22:07Z"], + ["expires", "2025-12-14T09:22:07Z"], ].map(([k, v], i) => ( -
{k}
-
{v}
+
+ {k} +
+
+ {v} +
))}
-
-
- +
+
+ The grant is the artifact. Collection is a companion mechanism. -
- - +
+ +
diff --git a/docs/design-system/ink-carbon/project/ui_kits/web/GrantsList.jsx b/docs/design-system/ink-carbon/project/ui_kits/web/GrantsList.jsx index 429adf428..d0a688420 100644 --- a/docs/design-system/ink-carbon/project/ui_kits/web/GrantsList.jsx +++ b/docs/design-system/ink-carbon/project/ui_kits/web/GrantsList.jsx @@ -4,53 +4,128 @@ // GrantsList — the owner's dashboard of active grants. const GRANTS = [ - { id: 'grt_longview01', client: 'Longview Planning', monogram: 'LV', purpose: 'long_term_financial_planning', status: 'active', scopes: ['pay_statements.read', 'employment.read'], issued: 'Oct 14, 2025', expires: 'Dec 14, 2025' }, - { id: 'grt_acme_kyc_02', client: 'Acme KYC', monogram: 'AK', purpose: 'identity_verification', status: 'active', scopes: ['identity.read'], issued: 'Nov 02, 2025', expires: 'Nov 03, 2025' }, - { id: 'grt_forecast_17', client: 'Forecast Mortgage',monogram: 'FM', purpose: 'underwriting_review', status: 'expiring', scopes: ['pay_statements.read', 'tax_docs.read', 'employment.read'], issued: 'Sep 28, 2025', expires: 'in 2 days' }, - { id: 'grt_oldmedical', client: 'Old Medical LLC', monogram: 'OM', purpose: 'insurance_claim', status: 'revoked', scopes: ['identity.read'], issued: 'Aug 05, 2025', expires: '—' }, + { + id: "grt_longview01", + client: "Longview Planning", + monogram: "LV", + purpose: "long_term_financial_planning", + status: "active", + scopes: ["pay_statements.read", "employment.read"], + issued: "Oct 14, 2025", + expires: "Dec 14, 2025", + }, + { + id: "grt_acme_kyc_02", + client: "Acme KYC", + monogram: "AK", + purpose: "identity_verification", + status: "active", + scopes: ["identity.read"], + issued: "Nov 02, 2025", + expires: "Nov 03, 2025", + }, + { + id: "grt_forecast_17", + client: "Forecast Mortgage", + monogram: "FM", + purpose: "underwriting_review", + status: "expiring", + scopes: ["pay_statements.read", "tax_docs.read", "employment.read"], + issued: "Sep 28, 2025", + expires: "in 2 days", + }, + { + id: "grt_oldmedical", + client: "Old Medical LLC", + monogram: "OM", + purpose: "insurance_claim", + status: "revoked", + scopes: ["identity.read"], + issued: "Aug 05, 2025", + expires: "—", + }, ]; -const STATUS_CLASS = { active: 'pdpp-badge-success', expiring: 'pdpp-badge-warning', revoked: 'pdpp-badge-destructive' }; +const STATUS_CLASS = { + active: "pdpp-badge-success", + expiring: "pdpp-badge-warning", + revoked: "pdpp-badge-destructive", +}; const GrantsList = ({ onOpen }) => ( -
-
+
+
Your grants
-
+
4 grants · 2 active · 1 expiring · 1 revoked
-
- - +
+ +
- {GRANTS.map(g => ( -
( +
onOpen && onOpen(g)} - className={g.status === 'revoked' ? 'pdpp-surface-neutral' : 'pdpp-surface-protocol'} - style={{ padding: '14px 16px', cursor: 'pointer', opacity: g.status === 'revoked' ? 0.65 : 1 }}> -
-
-
{g.monogram}
+ className={g.status === "revoked" ? "pdpp-surface-neutral" : "pdpp-surface-protocol"} + style={{ padding: "14px 16px", cursor: "pointer", opacity: g.status === "revoked" ? 0.65 : 1 }} + > +
+
+
+ {g.monogram} +
{g.client}
-
// {g.purpose}
+
+ // {g.purpose} +
- {g.status} + + + {g.status} +
-
- {g.scopes.map(sc => {sc})} +
+ {g.scopes.map((sc) => ( + + {sc} + + ))}
-
+
{g.id} issued {g.issued} expires {g.expires} diff --git a/docs/design-system/ink-carbon/project/ui_kits/web/Hero.jsx b/docs/design-system/ink-carbon/project/ui_kits/web/Hero.jsx index c0bac9d2d..ca95e8a6a 100644 --- a/docs/design-system/ink-carbon/project/ui_kits/web/Hero.jsx +++ b/docs/design-system/ink-carbon/project/ui_kits/web/Hero.jsx @@ -4,48 +4,96 @@ // Hero — the cross-quadrant layout from apps/web. Copper left rule on content column. const Hero = () => ( -
-
+
+
{/* empty quadrant aligned with sidebar */} -
-
PDPP · v0.1.0 draft
+
+
+ PDPP · v0.1.0 draft +

- Granular access to personal data. + Granular access to personal data.

-

- An open specification for how personal user data flows through the digital economy. - Clients request named records and fields. Every response stays inside the grant. +

+ An open specification for how personal user data flows through the digital economy. Clients request named + records and fields. Every response stays inside the grant.

-
- - - + RFC status · draft 3
-
+
{[ - { k: 'GRANT', t: 'The portable artifact', b: 'A grant names resources, fields, purpose, duration, and mode.' }, - { k: 'STREAM', t: 'Named data shapes', b: 'Pay statements, employment, tax docs — declared, typed, versioned.' }, - { k: 'ENFORCE', t: 'Server-side boundary', b: 'Only the granted fields come back. Purpose is declared, not enforced.' }, + { + k: "GRANT", + t: "The portable artifact", + b: "A grant names resources, fields, purpose, duration, and mode.", + }, + { + k: "STREAM", + t: "Named data shapes", + b: "Pay statements, employment, tax docs — declared, typed, versioned.", + }, + { + k: "ENFORCE", + t: "Server-side boundary", + b: "Only the granted fields come back. Purpose is declared, not enforced.", + }, ].map((c, i) => ( -
-
{c.k}
-
{c.t}
-

{c.b}

+
+
+ {c.k} +
+
+ {c.t} +
+

+ {c.b} +

))}
diff --git a/docs/design-system/ink-carbon/project/ui_kits/web/SiteHeader.jsx b/docs/design-system/ink-carbon/project/ui_kits/web/SiteHeader.jsx index 0a1bda644..d678e7da9 100644 --- a/docs/design-system/ink-carbon/project/ui_kits/web/SiteHeader.jsx +++ b/docs/design-system/ink-carbon/project/ui_kits/web/SiteHeader.jsx @@ -2,40 +2,74 @@ // SPDX-License-Identifier: Apache-2.0 // Site header — nav with logo, docs/spec/palette links, GitHub CTA -const SiteHeader = ({ active = 'home', onNav }) => ( -
-
-
-
P
- PDPP - v0.1.0 +const SiteHeader = ({ active = "home", onNav }) => ( +
+
+
+
+ P +
+ PDPP + + v0.1.0 +
- diff --git a/docs/design-system/ink-carbon/project/ui_kits/web/StreamInventory.jsx b/docs/design-system/ink-carbon/project/ui_kits/web/StreamInventory.jsx index f98dc7a7e..69ac09b46 100644 --- a/docs/design-system/ink-carbon/project/ui_kits/web/StreamInventory.jsx +++ b/docs/design-system/ink-carbon/project/ui_kits/web/StreamInventory.jsx @@ -4,48 +4,77 @@ // StreamInventory — a list of declared streams with their shapes and access mode. const STREAMS = [ - { id: 'pay_statements', title: 'Pay statements', mode: 'append only', fields: 6, recent: '2 days ago', granted: 3 }, - { id: 'employment', title: 'Employment', mode: 'mutable state', fields: 4, recent: '1 month ago', granted: 2 }, - { id: 'tax_docs', title: 'Tax documents', mode: 'append only', fields: 5, recent: '3 months ago', granted: 1 }, - { id: 'identity', title: 'Identity', mode: 'mutable state', fields: 3, recent: 'never', granted: 0 }, - { id: 'transactions', title: 'Transactions', mode: 'append only', fields: 8, recent: 'today', granted: 4 }, + { id: "pay_statements", title: "Pay statements", mode: "append only", fields: 6, recent: "2 days ago", granted: 3 }, + { id: "employment", title: "Employment", mode: "mutable state", fields: 4, recent: "1 month ago", granted: 2 }, + { id: "tax_docs", title: "Tax documents", mode: "append only", fields: 5, recent: "3 months ago", granted: 1 }, + { id: "identity", title: "Identity", mode: "mutable state", fields: 3, recent: "never", granted: 0 }, + { id: "transactions", title: "Transactions", mode: "append only", fields: 8, recent: "today", granted: 4 }, ]; const StreamInventory = () => ( -
-
+
+
Streams
-
+
5 declared · 3 sharing with at least one grant
- +
-
- streammodefieldslast recordgrants +
+ stream + mode + fields + last record + grants
{STREAMS.map((s, i) => ( -
0 ? '1px solid var(--border)' : 'none', - }}> +
0 ? "1px solid var(--border)" : "none", + }} + >
{s.title}
-
{s.id}
+
{s.id}
- {s.mode} - {s.fields} - {s.recent} - - {s.granted || '—'} + + {s.mode} + + {s.fields} + + {s.recent} + + + {s.granted || "—"}
))} diff --git a/docs/design-system/ink-carbon/project/ui_kits/web/Teaching.jsx b/docs/design-system/ink-carbon/project/ui_kits/web/Teaching.jsx index 99ed40147..58a06701e 100644 --- a/docs/design-system/ink-carbon/project/ui_kits/web/Teaching.jsx +++ b/docs/design-system/ink-carbon/project/ui_kits/web/Teaching.jsx @@ -4,39 +4,78 @@ // CodeBlock + FlowDiagram — teaching units used throughout the site/docs. const CodeBlock = ({ children, caption }) => ( -
-
{children}
- {caption &&
{caption}
} +
+
+      {children}
+    
+ {caption && ( +
+ {caption} +
+ )}
); const Node = ({ kind, eyebrow, title, sub }) => ( -
-
{eyebrow}
-
{title}
-
{sub}
+
+
+ {eyebrow} +
+
+ {title} +
+
+ {sub} +
); const Arrow = ({ label }) => ( -
- {label} -
- +
+ {label} +
+
); const FlowDiagram = () => ( -
+
diff --git a/docs/design-system/ink-carbon/project/ui_kits/web/index.html b/docs/design-system/ink-carbon/project/ui_kits/web/index.html index d0abdcf14..5e63c9086 100644 --- a/docs/design-system/ink-carbon/project/ui_kits/web/index.html +++ b/docs/design-system/ink-carbon/project/ui_kits/web/index.html @@ -1,49 +1,133 @@ - - -PDPP · Web UI Kit - - - - - - -
+ + + PDPP · Web UI Kit + + + + + + +
- - - + + + - - - - - - - + + + + + + + - - + diff --git a/docs/design-system/ink-carbon/project/ui_kits/web/kit.css b/docs/design-system/ink-carbon/project/ui_kits/web/kit.css index 69e8a78f8..eff360e2e 100644 --- a/docs/design-system/ink-carbon/project/ui_kits/web/kit.css +++ b/docs/design-system/ink-carbon/project/ui_kits/web/kit.css @@ -34,7 +34,9 @@ color: var(--muted-foreground); white-space: nowrap; } -.pdpp-sheet__body { padding: 14px 18px; } +.pdpp-sheet__body { + padding: 14px 18px; +} .pdpp-sheet__foot { display: flex; flex-wrap: wrap; @@ -64,7 +66,9 @@ padding: 11px 18px; border-bottom: 1px solid var(--border); } -.pdpp-scope:last-child { border-bottom: 0; } +.pdpp-scope:last-child { + border-bottom: 0; +} .pdpp-scope__name { font-family: var(--font-mono); font-size: 0.8125rem; @@ -94,7 +98,9 @@ Every list shares ONE column template via --cols on the wrapper, so every cell in a column sits on the same axis in every row. Headers are mandatory: a column a user must infer is a column mislabeled. */ -.pdpp-table { width: 100%; } +.pdpp-table { + width: 100%; +} .pdpp-table__hrow, .pdpp-table__row { display: grid; @@ -116,28 +122,48 @@ color: var(--muted-foreground); white-space: nowrap; } -.pdpp-table__row { border-bottom: 1px solid var(--border); position: relative; } -.pdpp-table .u-r { text-align: right; justify-self: stretch; } +.pdpp-table__row { + border-bottom: 1px solid var(--border); + position: relative; +} +.pdpp-table .u-r { + text-align: right; + justify-self: stretch; +} /* ─── Focus policy — rings only for keyboards ─────────────────── */ -.pdpp-sheet button:focus, .pdpp-table button:focus, .pdpp-btn:focus { outline: none; } +.pdpp-sheet button:focus, +.pdpp-table button:focus, +.pdpp-btn:focus { + outline: none; +} :is(button, a, input, [tabindex]):focus-visible { outline: 2px solid var(--ring); outline-offset: 1px; } /* ─── Data list — dense rows for the console ───────────────────── */ -.pdpp-data-list { width: 100%; } +.pdpp-data-list { + width: 100%; +} .pdpp-data-row { display: grid; - grid-template-columns: var(--cols, minmax(0, 1.25fr) minmax(0, 1.6fr) 112px 128px); + grid-template-columns: var( + --cols, + minmax(0, 1.25fr) minmax(0, 1.6fr) 112px 128px + ); gap: 0 18px; align-items: center; padding: 10px 0; border-bottom: 1px solid var(--border); position: relative; } -.pdpp-data-row__who { display: flex; flex-direction: column; gap: 1px; min-width: 0; } +.pdpp-data-row__who { + display: flex; + flex-direction: column; + gap: 1px; + min-width: 0; +} .pdpp-data-row__title { font-size: 0.875rem; font-weight: 600; @@ -183,7 +209,9 @@ background: var(--card); box-shadow: inset 0 0 0 1px var(--border-strong); } -.pdpp-data-row--revoked .pdpp-monogram { opacity: 0.5; } +.pdpp-data-row--revoked .pdpp-monogram { + opacity: 0.5; +} /* A revoked row is struck, not erased — the record survives revocation. */ .pdpp-data-row--revoked .pdpp-data-row__title, @@ -204,15 +232,23 @@ background: color-mix(in oklab, currentColor 11%, transparent); box-shadow: inset 0 0 0 1px color-mix(in oklab, currentColor 42%, transparent); } -.pdpp-endorse--active { color: var(--success); } -.pdpp-endorse--continuous { color: var(--primary); } -.pdpp-endorse--expiring { color: var(--warning); } +.pdpp-endorse--active { + color: var(--success); +} +.pdpp-endorse--continuous { + color: var(--primary); +} +.pdpp-endorse--expiring { + color: var(--warning); +} .pdpp-endorse--revoked { color: var(--muted-foreground); background: transparent; box-shadow: inset 0 0 0 1px var(--border-strong); } -.pdpp-endorse--denied { color: var(--destructive); } +.pdpp-endorse--denied { + color: var(--destructive); +} /* Taxonomy tag — typed, neutral, no color spent. */ .pdpp-tag { @@ -236,30 +272,44 @@ color: var(--background); border-radius: var(--radius-control); white-space: nowrap; - transition: background var(--motion-state), border-color var(--motion-state); + transition: + background var(--motion-state), + border-color var(--motion-state); +} +.pdpp-btn:hover { + background: color-mix(in oklab, var(--foreground) 86%, var(--background)); } -.pdpp-btn:hover { background: color-mix(in oklab, var(--foreground) 86%, var(--background)); } /* The owner's act — the one warm element on a surface. */ .pdpp-btn--human { background: var(--human); border-color: var(--human); color: var(--human-foreground); } -.pdpp-btn--human:hover { background: color-mix(in oklab, var(--human) 88%, var(--background)); } +.pdpp-btn--human:hover { + background: color-mix(in oklab, var(--human) 88%, var(--background)); +} .pdpp-btn--ghost { background: transparent; color: var(--foreground); border-color: var(--input); font-weight: 600; } -.pdpp-btn--ghost:hover { background: var(--muted); border-color: var(--border-strong); } +.pdpp-btn--ghost:hover { + background: var(--muted); + border-color: var(--border-strong); +} .pdpp-btn--destructive { background: transparent; color: var(--destructive); border-color: var(--destructive); } -.pdpp-btn--destructive:hover { background: var(--destructive-tint); } -.pdpp-btn--sm { font-size: 0.75rem; padding: 6px 12px; } +.pdpp-btn--destructive:hover { + background: var(--destructive-tint); +} +.pdpp-btn--sm { + font-size: 0.75rem; + padding: 6px 12px; +} /* ─── Inputs ───────────────────────────────────────────────────── */ .pdpp-input { @@ -271,14 +321,23 @@ border: 1px solid var(--input); border-radius: var(--radius-control); outline: none; - transition: border-color var(--motion-state), box-shadow var(--motion-state); + transition: + border-color var(--motion-state), + box-shadow var(--motion-state); } .pdpp-input:focus { border-color: var(--ring); box-shadow: 0 0 0 2px var(--primary-tint); } -.pdpp-input::placeholder { color: var(--muted-foreground); font-family: var(--font-sans); } -.pdpp-field { display: flex; flex-direction: column; gap: 5px; } +.pdpp-input::placeholder { + color: var(--muted-foreground); + font-family: var(--font-sans); +} +.pdpp-field { + display: flex; + flex-direction: column; + gap: 5px; +} .pdpp-field__label { font-family: var(--font-sans); font-size: 0.75rem; @@ -291,7 +350,10 @@ } /* ─── Key/value — typed record block ───────────────────────────── */ -.pdpp-kv { display: flex; flex-direction: column; } +.pdpp-kv { + display: flex; + flex-direction: column; +} .pdpp-kv__row { display: grid; grid-template-columns: 96px 1fr; @@ -326,7 +388,9 @@ flex-direction: column; gap: 1px; } -.pdpp-band__cell:first-child { border-left: 0; } +.pdpp-band__cell:first-child { + border-left: 0; +} .pdpp-band__v { font-family: var(--font-mono); font-size: 1.2rem; diff --git a/docs/explorer/uat/harness/capture.mjs b/docs/explorer/uat/harness/capture.mjs index cc2690236..c7d7f3925 100644 --- a/docs/explorer/uat/harness/capture.mjs +++ b/docs/explorer/uat/harness/capture.mjs @@ -26,10 +26,7 @@ import { join } from "node:path"; // root, so this is a dynamic, intentionally-untyped import. harness lives at // docs/explorer/uat/harness/ → repo root is four levels up. const { chromium } = await import( - new URL( - "../../../../packages/polyfill-connectors/node_modules/patchright/index.mjs", - import.meta.url - ) + new URL("../../../../packages/polyfill-connectors/node_modules/patchright/index.mjs", import.meta.url) ); const DASH_URL = process.env.DASH_URL || "http://localhost:3300/explore"; diff --git a/docs/explorer/uat/harness/seed-and-serve.mjs b/docs/explorer/uat/harness/seed-and-serve.mjs index 3042693f0..67f03d0d4 100644 --- a/docs/explorer/uat/harness/seed-and-serve.mjs +++ b/docs/explorer/uat/harness/seed-and-serve.mjs @@ -24,12 +24,7 @@ import { readFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; -import { - CHASE_CONNECTOR_ID, - CHASE_TRANSACTIONS, - GMAIL_CONNECTOR_ID, - GMAIL_MESSAGES, -} from "./fixtures.mjs"; +import { CHASE_CONNECTOR_ID, CHASE_TRANSACTIONS, GMAIL_CONNECTOR_ID, GMAIL_MESSAGES } from "./fixtures.mjs"; const __dir = dirname(fileURLToPath(import.meta.url)); // harness lives at docs/explorer/uat/harness/ — repo root is four levels up. @@ -101,9 +96,7 @@ async function registerManifest(manifest) { } async function ingest(token, connectorId, stream, records, timeField) { - const lines = records - .map((r) => JSON.stringify({ key: r.id, data: r, emitted_at: r[timeField] })) - .join("\n"); + const lines = records.map((r) => JSON.stringify({ key: r.id, data: r, emitted_at: r[timeField] })).join("\n"); const { status, body } = await jsonFetch( `${RS_URL}/v1/ingest/${encodeURIComponent(stream)}?connector_id=${encodeURIComponent(connectorId)}`, { diff --git a/packages/cli/bin/pdpp.js b/packages/cli/bin/pdpp.js index cc1330c43..8d6cfe9b8 100755 --- a/packages/cli/bin/pdpp.js +++ b/packages/cli/bin/pdpp.js @@ -2,7 +2,7 @@ // Copyright The PDP-Connect Contributors // SPDX-License-Identifier: Apache-2.0 -import { runCli } from '../src/index.js'; +import { runCli } from "../src/index.js"; const exitCode = await runCli(process.argv.slice(2), { stderr: process.stderr, diff --git a/packages/cli/src/cache-layout.js b/packages/cli/src/cache-layout.js index 956fbefe0..5a2612256 100644 --- a/packages/cli/src/cache-layout.js +++ b/packages/cli/src/cache-layout.js @@ -1,21 +1,21 @@ // Copyright The PDP-Connect Contributors // SPDX-License-Identifier: Apache-2.0 -import { mkdirSync, statSync, writeFileSync } from 'node:fs'; -import { dirname, join } from 'node:path'; +import { mkdirSync, statSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; -export function getPdppCacheLayout(cacheRoot = '.pdpp') { +export function getPdppCacheLayout(cacheRoot = ".pdpp") { return { root: cacheRoot, - clientsDir: join(cacheRoot, 'clients'), - gitignoreFile: join(cacheRoot, '.gitignore'), - credentialFile: (providerUrl) => join(cacheRoot, 'clients', `${providerCacheKey(providerUrl)}.json`), + clientsDir: join(cacheRoot, "clients"), + gitignoreFile: join(cacheRoot, ".gitignore"), + credentialFile: (providerUrl) => join(cacheRoot, "clients", `${providerCacheKey(providerUrl)}.json`), }; } function providerCacheKey(providerUrl) { - const host = providerUrl.includes('://') ? new URL(providerUrl).host : providerUrl; - return host.replace(/[^a-zA-Z0-9.-]/g, '_'); + const host = providerUrl.includes("://") ? new URL(providerUrl).host : providerUrl; + return host.replace(/[^a-zA-Z0-9.-]/g, "_"); } export function writePdppSecretFile(path, value) { diff --git a/packages/cli/src/collector/commands.js b/packages/cli/src/collector/commands.js index 51eff36e2..a7f52d7b1 100644 --- a/packages/cli/src/collector/commands.js +++ b/packages/cli/src/collector/commands.js @@ -1,9 +1,9 @@ // Copyright The PDP-Connect Contributors // SPDX-License-Identifier: Apache-2.0 -import { PDPP_CLI_BIN_NAME } from '../package-info.js'; -import { CollectorUsageError } from './errors.js'; -import { spawnCollectorRunner } from './runner.js'; +import { PDPP_CLI_BIN_NAME } from "../package-info.js"; +import { CollectorUsageError } from "./errors.js"; +import { spawnCollectorRunner } from "./runner.js"; const COLLECTOR_HELP = `Local collector runner (reference operator surface). @@ -68,12 +68,12 @@ Notes: PDPP_RUN_ID. PDPP_SOURCE_INSTANCE_ID remains a compatibility alias. `; -const SUBCOMMANDS = new Set(['advertise', 'enroll', 'run']); +const SUBCOMMANDS = new Set(["advertise", "enroll", "run"]); export async function runCollector(argv, io) { const [sub, ...rest] = argv; - if (!sub || sub === '--help' || sub === '-h' || sub === 'help') { + if (!sub || sub === "--help" || sub === "-h" || sub === "help") { io.stdout.write(COLLECTOR_HELP); return 0; } diff --git a/packages/cli/src/collector/errors.js b/packages/cli/src/collector/errors.js index 6e4cba99b..55d42dde9 100644 --- a/packages/cli/src/collector/errors.js +++ b/packages/cli/src/collector/errors.js @@ -4,7 +4,7 @@ export class CollectorUsageError extends Error { constructor(message, { exitCode = 64 } = {}) { super(message); - this.name = 'CollectorUsageError'; + this.name = "CollectorUsageError"; this.exitCode = exitCode; } } diff --git a/packages/cli/src/collector/runner.js b/packages/cli/src/collector/runner.js index d334cea0e..8e0478ae8 100644 --- a/packages/cli/src/collector/runner.js +++ b/packages/cli/src/collector/runner.js @@ -1,13 +1,13 @@ // Copyright The PDP-Connect Contributors // SPDX-License-Identifier: Apache-2.0 -import { spawn } from 'node:child_process'; -import { createRequire } from 'node:module'; -import { existsSync, readFileSync } from 'node:fs'; -import { dirname, join, resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; +import { spawn } from "node:child_process"; +import { createRequire } from "node:module"; +import { existsSync, readFileSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; -import { CollectorUsageError } from './errors.js'; +import { CollectorUsageError } from "./errors.js"; /** * Resolve the published `@pdpp/local-collector` package, if installed. @@ -26,8 +26,8 @@ export function resolveLocalCollectorPackage(startDir = dirname(fileURLToPath(im // npm install where @pdpp/local-collector is alongside @pdpp/cli in the // same node_modules tree. try { - const require = createRequire(join(startDir, '_')); - const manifestPath = require.resolve('@pdpp/local-collector/package.json'); + const require = createRequire(join(startDir, "_")); + const manifestPath = require.resolve("@pdpp/local-collector/package.json"); return { manifestPath, packageDir: dirname(manifestPath) }; } catch { // Continue to workspace fallback. @@ -40,7 +40,7 @@ export function resolveLocalCollectorPackage(startDir = dirname(fileURLToPath(im const seen = new Set(); while (!seen.has(cursor)) { seen.add(cursor); - const candidate = join(cursor, 'packages', 'local-collector', 'package.json'); + const candidate = join(cursor, "packages", "local-collector", "package.json"); if (existsSync(candidate)) { return { manifestPath: candidate, packageDir: dirname(candidate) }; } @@ -74,7 +74,7 @@ export function resolveCollectorRunnerScript(startDir = dirname(fileURLToPath(im const seen = new Set(); while (!seen.has(cursor)) { seen.add(cursor); - const candidate = join(cursor, 'packages', 'polyfill-connectors', 'bin', 'collector-runner.ts'); + const candidate = join(cursor, "packages", "polyfill-connectors", "bin", "collector-runner.ts"); if (existsSync(candidate)) { return candidate; } @@ -92,7 +92,7 @@ export function resolveTsxBinary(startDir = dirname(fileURLToPath(import.meta.ur const seen = new Set(); while (!seen.has(cursor)) { seen.add(cursor); - const candidate = join(cursor, 'node_modules', '.bin', 'tsx'); + const candidate = join(cursor, "node_modules", ".bin", "tsx"); if (existsSync(candidate)) { return candidate; } @@ -110,12 +110,12 @@ export function resolveTsxBinary(startDir = dirname(fileURLToPath(import.meta.ur * installed `@pdpp/local-collector` can be found. */ const RUNNER_MISSING_MESSAGE = - 'pdpp collector requires @pdpp/local-collector. Install once with ' + + "pdpp collector requires @pdpp/local-collector. Install once with " + '"npm i -g @pdpp/local-collector" or run "npx -y @pdpp/local-collector ...". ' + - 'See openspec/changes/publish-pdpp-local-collector/design.md.'; + "See openspec/changes/publish-pdpp-local-collector/design.md."; const TSX_MISSING_MESSAGE = - 'Could not locate tsx alongside the collector runner. Install ' + + "Could not locate tsx alongside the collector runner. Install " + '@pdpp/local-collector with "npm i -g @pdpp/local-collector" or run ' + '"pnpm install" at the monorepo root.'; @@ -138,8 +138,8 @@ export async function spawnCollectorRunner( localCollector = resolveLocalCollectorPackage(), tsxBinary = resolveTsxBinary(), spawnFn = spawn, - stdio = 'inherit', - } = {}, + stdio = "inherit", + } = {} ) { if (runnerScript) { if (!tsxBinary) { @@ -153,10 +153,10 @@ export async function spawnCollectorRunner( if (!existsSync(binPath)) { throw new CollectorUsageError( `@pdpp/local-collector is installed at ${localCollector.packageDir} but is missing its bin entrypoint. ` + - 'Reinstall the package or report this on https://github.com/PDP-Connect/pdpp/issues.', + "Reinstall the package or report this on https://github.com/PDP-Connect/pdpp/issues." ); } - if (binPath.endsWith('.ts')) { + if (binPath.endsWith(".ts")) { if (!tsxBinary) { throw new CollectorUsageError(TSX_MISSING_MESSAGE); } @@ -170,22 +170,22 @@ export async function spawnCollectorRunner( function resolveLocalCollectorBin(packageDir) { try { - const manifest = JSON.parse(readFileSync(join(packageDir, 'package.json'), 'utf8')); - const bin = manifest?.bin?.['pdpp-local-collector']; - if (typeof bin === 'string' && bin.trim()) { + const manifest = JSON.parse(readFileSync(join(packageDir, "package.json"), "utf8")); + const bin = manifest?.bin?.["pdpp-local-collector"]; + if (typeof bin === "string" && bin.trim()) { return join(packageDir, bin); } } catch {} - const publishedBin = join(packageDir, 'dist', 'local-collector', 'bin', 'pdpp-local-collector.js'); + const publishedBin = join(packageDir, "dist", "local-collector", "bin", "pdpp-local-collector.js"); if (existsSync(publishedBin)) return publishedBin; - return join(packageDir, 'bin', 'pdpp-local-collector.ts'); + return join(packageDir, "bin", "pdpp-local-collector.ts"); } function runSubprocess(spawnFn, binary, args, { env, stdio }) { return new Promise((resolvePromise, rejectPromise) => { const child = spawnFn(binary, args, { env, stdio }); - child.on('error', rejectPromise); - child.on('exit', (code, signal) => { + child.on("error", rejectPromise); + child.on("exit", (code, signal) => { if (signal) { rejectPromise(new Error(`collector-runner terminated by signal ${signal}`)); return; diff --git a/packages/cli/src/connect/flow.js b/packages/cli/src/connect/flow.js index 48afa1283..7f9177141 100644 --- a/packages/cli/src/connect/flow.js +++ b/packages/cli/src/connect/flow.js @@ -1,19 +1,19 @@ // Copyright The PDP-Connect Contributors // SPDX-License-Identifier: Apache-2.0 -import { mkdir, readFile, writeFile } from 'node:fs/promises'; -import { dirname, join } from 'node:path'; +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; -const PROTECTED_RESOURCE_METADATA_PATH = '/.well-known/oauth-protected-resource'; -const AUTHORIZATION_SERVER_METADATA_PATH = '/.well-known/oauth-authorization-server'; -const DEFAULT_SCOPE = 'pdpp:read'; +const PROTECTED_RESOURCE_METADATA_PATH = "/.well-known/oauth-protected-resource"; +const AUTHORIZATION_SERVER_METADATA_PATH = "/.well-known/oauth-authorization-server"; +const DEFAULT_SCOPE = "pdpp:read"; const DEFAULT_POLL_INTERVAL_MS = 2000; const DEFAULT_POLL_TIMEOUT_MS = 5 * 60 * 1000; export class ConnectError extends Error { constructor(code, message, exitCode = 69) { super(message); - this.name = 'ConnectError'; + this.name = "ConnectError"; this.code = code; this.exitCode = exitCode; } @@ -22,37 +22,40 @@ export class ConnectError extends Error { export async function connectProvider(providerUrl, options = {}) { const normalizedProviderUrl = normalizeProviderUrl(providerUrl); if (!normalizedProviderUrl) { - throw new ConnectError('invalid_provider_url', `Invalid provider URL: ${providerUrl}`, 64); + throw new ConnectError("invalid_provider_url", `Invalid provider URL: ${providerUrl}`, 64); } const fetchFn = options.fetch ?? globalThis.fetch; - if (typeof fetchFn !== 'function') { - throw new ConnectError('fetch_unavailable', 'This Node runtime does not provide fetch().'); + if (typeof fetchFn !== "function") { + throw new ConnectError("fetch_unavailable", "This Node runtime does not provide fetch()."); } const io = options.io ?? { stdout: process.stdout, stderr: process.stderr }; - const cacheRoot = options.cacheRoot ?? '.pdpp'; + const cacheRoot = options.cacheRoot ?? ".pdpp"; const scope = options.scope ?? DEFAULT_SCOPE; const resourceMetadata = await discoverProtectedResourceMetadata(normalizedProviderUrl, fetchFn); const cliDiscovery = resourceMetadata.pdpp_agent_discovery?.cli; if (cliDiscovery?.no_owner_token === false) { - const policy = cliDiscovery.no_owner_token_policy ? ` Policy: ${cliDiscovery.no_owner_token_policy}.` : ''; + const policy = cliDiscovery.no_owner_token_policy ? ` Policy: ${cliDiscovery.no_owner_token_policy}.` : ""; throw new ConnectError( - 'connect_not_supported', + "connect_not_supported", `Provider metadata does not advertise a complete no-owner-token connect flow.${policy}` ); } const authorizationServerUrl = selectAuthorizationServer(resourceMetadata, normalizedProviderUrl); if (!authorizationServerUrl) { - throw new ConnectError('metadata_failure', 'Protected-resource metadata did not include a valid authorization server.'); + throw new ConnectError( + "metadata_failure", + "Protected-resource metadata did not include a valid authorization server." + ); } const authorizationMetadata = await discoverAuthorizationServerMetadata(authorizationServerUrl, fetchFn); const connectEndpoint = findAgentConnectEndpoint(resourceMetadata, authorizationMetadata); if (!connectEndpoint) { throw new ConnectError( - 'connect_not_supported', - 'Provider metadata does not advertise a no-owner-token agent connect endpoint yet. Backend must expose pdpp_agent_discovery.agent_connect_endpoint or agent_connect_endpoint before this command can complete.' + "connect_not_supported", + "Provider metadata does not advertise a no-owner-token agent connect endpoint yet. Backend must expose pdpp_agent_discovery.agent_connect_endpoint or agent_connect_endpoint before this command can complete." ); } @@ -61,13 +64,13 @@ export async function connectProvider(providerUrl, options = {}) { authorizationMetadata, cacheRoot, providerUrl: normalizedProviderUrl, - clientName: 'PDPP CLI', + clientName: "PDPP CLI", }); const startRequest = { resource: normalizedProviderUrl, scope, - client_name: 'PDPP CLI', + client_name: "PDPP CLI", }; if (publicClient?.client_id) { startRequest.client_id = publicClient.client_id; @@ -79,8 +82,8 @@ export async function connectProvider(providerUrl, options = {}) { const pollUrl = start.poll_url ?? start.token_url ?? start.device_poll_endpoint ?? start.completion_endpoint; if (!approvalUrl || !pollUrl) { throw new ConnectError( - 'connect_contract_invalid', - 'Agent connect start response must include approval_url and a polling token URL.' + "connect_contract_invalid", + "Agent connect start response must include approval_url and a polling token URL." ); } @@ -88,7 +91,7 @@ export async function connectProvider(providerUrl, options = {}) { if (start.user_code) { io.stdout.write(`Enter code: ${start.user_code}\n`); } - io.stdout.write('Waiting for approval...\n'); + io.stdout.write("Waiting for approval...\n"); const credential = await pollForCredential(fetchFn, pollUrl, { intervalMs: Number(start.interval_ms ?? start.interval ?? options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS), @@ -105,7 +108,7 @@ export async function connectProvider(providerUrl, options = {}) { scope, client: publicClient, credential, - created_at: new Date((options.now?.() ?? Date.now())).toISOString(), + created_at: new Date(options.now?.() ?? Date.now()).toISOString(), }); io.stdout.write( @@ -124,18 +127,18 @@ export async function connectProvider(providerUrl, options = {}) { export async function readStoredCredential(providerUrl, options = {}) { const normalizedProviderUrl = normalizeProviderUrl(providerUrl); if (!normalizedProviderUrl) { - throw new ConnectError('invalid_provider_url', `Invalid provider URL: ${providerUrl}`, 64); + throw new ConnectError("invalid_provider_url", `Invalid provider URL: ${providerUrl}`, 64); } - const cacheRoot = options.cacheRoot ?? '.pdpp'; + const cacheRoot = options.cacheRoot ?? ".pdpp"; const cacheFile = getCredentialCacheFile(cacheRoot, normalizedProviderUrl); let payload; try { - payload = JSON.parse(await readFile(cacheFile, 'utf8')); + payload = JSON.parse(await readFile(cacheFile, "utf8")); } catch (error) { - if (error?.code === 'ENOENT') { + if (error?.code === "ENOENT") { throw new ConnectError( - 'not_connected', + "not_connected", `No PDPP credential found for ${normalizedProviderUrl}. Run pdpp connect ${normalizedProviderUrl} first.` ); } @@ -144,7 +147,7 @@ export async function readStoredCredential(providerUrl, options = {}) { const credential = payload?.credential; if (!credential?.access_token) { - throw new ConnectError('credential_invalid', `Credential cache entry is missing an access token: ${cacheFile}`); + throw new ConnectError("credential_invalid", `Credential cache entry is missing an access token: ${cacheFile}`); } if (credential.expires_at) { @@ -152,7 +155,7 @@ export async function readStoredCredential(providerUrl, options = {}) { const now = options.now?.() ?? Date.now(); if (Number.isFinite(expiresAtMs) && expiresAtMs <= now) { throw new ConnectError( - 'credential_expired', + "credential_expired", `Credential for ${normalizedProviderUrl} expired. Run pdpp connect ${normalizedProviderUrl} again.` ); } @@ -163,19 +166,19 @@ export async function readStoredCredential(providerUrl, options = {}) { export function normalizeProviderUrl(value) { try { - const parsed = new URL(value.includes('://') ? value : `https://${value}`); - if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') { + const parsed = new URL(value.includes("://") ? value : `https://${value}`); + if (parsed.protocol !== "https:" && parsed.protocol !== "http:") { return null; } - parsed.username = ''; - parsed.password = ''; - parsed.hash = ''; - parsed.search = ''; + parsed.username = ""; + parsed.password = ""; + parsed.hash = ""; + parsed.search = ""; parsed.pathname = trimTrailingSlash(parsed.pathname); - if (parsed.pathname === '/') { - parsed.pathname = ''; + if (parsed.pathname === "/") { + parsed.pathname = ""; } - return parsed.toString().replace(/\/$/, ''); + return parsed.toString().replace(/\/$/, ""); } catch { return null; } @@ -183,10 +186,10 @@ export function normalizeProviderUrl(value) { export async function discoverProtectedResourceMetadata(providerUrl, fetchFn = globalThis.fetch) { const metadataUrl = new URL(PROTECTED_RESOURCE_METADATA_PATH, providerUrl).toString(); - const metadata = await getJson(fetchFn, metadataUrl, 'metadata_failure'); + const metadata = await getJson(fetchFn, metadataUrl, "metadata_failure"); if (metadata.resource && normalizeProviderUrl(metadata.resource) !== providerUrl) { throw new ConnectError( - 'metadata_failure', + "metadata_failure", `Protected-resource metadata resource mismatch: expected ${providerUrl}.` ); } @@ -195,9 +198,9 @@ export async function discoverProtectedResourceMetadata(providerUrl, fetchFn = g export async function discoverAuthorizationServerMetadata(issuerUrl, fetchFn = globalThis.fetch) { const metadataUrl = new URL(AUTHORIZATION_SERVER_METADATA_PATH, issuerUrl).toString(); - const metadata = await getJson(fetchFn, metadataUrl, 'metadata_failure'); + const metadata = await getJson(fetchFn, metadataUrl, "metadata_failure"); if (metadata.issuer && normalizeProviderUrl(metadata.issuer) !== normalizeProviderUrl(issuerUrl)) { - throw new ConnectError('metadata_failure', 'Authorization-server metadata issuer mismatch.'); + throw new ConnectError("metadata_failure", "Authorization-server metadata issuer mismatch."); } return metadata; } @@ -221,7 +224,7 @@ function findAgentConnectEndpoint(resourceMetadata, authorizationMetadata) { try { return new URL(endpoint, authorizationMetadata.issuer ?? resourceMetadata.resource).toString(); } catch { - throw new ConnectError('metadata_failure', 'Agent connect endpoint in provider metadata is not a valid URL.'); + throw new ConnectError("metadata_failure", "Agent connect endpoint in provider metadata is not a valid URL."); } } @@ -231,13 +234,13 @@ function findRegistrationEndpoint(authorizationMetadata) { return null; } const modes = authorizationMetadata.pdpp_registration_modes_supported; - if (Array.isArray(modes) && !modes.includes('dynamic')) { + if (Array.isArray(modes) && !modes.includes("dynamic")) { return null; } try { return new URL(endpoint, authorizationMetadata.issuer).toString(); } catch { - throw new ConnectError('metadata_failure', 'Registration endpoint in provider metadata is not a valid URL.'); + throw new ConnectError("metadata_failure", "Registration endpoint in provider metadata is not a valid URL."); } } @@ -252,25 +255,28 @@ async function getOrRegisterPublicClient({ fetchFn, authorizationMetadata, cache } const registered = await postJson(fetchFn, registrationEndpoint, { client_name: clientName, - token_endpoint_auth_method: 'none', + token_endpoint_auth_method: "none", }); if (!registered?.client_id) { - throw new ConnectError('connect_contract_invalid', 'Dynamic client registration response did not include client_id.'); + throw new ConnectError( + "connect_contract_invalid", + "Dynamic client registration response did not include client_id." + ); } return { client_id: registered.client_id, client_name: registered.client_name ?? clientName, - token_endpoint_auth_method: registered.token_endpoint_auth_method ?? 'none', + token_endpoint_auth_method: registered.token_endpoint_auth_method ?? "none", }; } async function readCachedClientRegistration(cacheRoot, providerUrl) { try { - const payload = JSON.parse(await readFile(getCredentialCacheFile(cacheRoot, providerUrl), 'utf8')); + const payload = JSON.parse(await readFile(getCredentialCacheFile(cacheRoot, providerUrl), "utf8")); const client = payload?.client; return client?.client_id ? client : null; } catch (error) { - if (error?.code === 'ENOENT') return null; + if (error?.code === "ENOENT") return null; throw error; } } @@ -282,66 +288,69 @@ async function pollForCredential(fetchFn, pollUrl, options) { while ((options.now?.() ?? Date.now()) - startedAt <= options.timeoutMs) { const result = await postJson(fetchFn, pollUrl, options.pollingCode ? { polling_code: options.pollingCode } : {}); const errorStatus = result.error?.code ?? result.error ?? result.code; - if (errorStatus === 'access_denied') { - throw new ConnectError('approval_denied', 'Owner denied the delegated access request.'); + if (errorStatus === "access_denied") { + throw new ConnectError("approval_denied", "Owner denied the delegated access request."); } - if (errorStatus === 'expired_token') { - throw new ConnectError('approval_expired', 'Delegated access approval expired. Run connect again.'); + if (errorStatus === "expired_token") { + throw new ConnectError("approval_expired", "Delegated access approval expired. Run connect again."); } - if (errorStatus === 'insufficient_scope') { - throw new ConnectError('insufficient_scope', 'Approved grant did not include the required PDPP scope.'); + if (errorStatus === "insufficient_scope") { + throw new ConnectError("insufficient_scope", "Approved grant did not include the required PDPP scope."); } - const status = result.status ?? (result.access_token ? 'approved' : 'pending'); + const status = result.status ?? (result.access_token ? "approved" : "pending"); - if (status === 'approved') { + if (status === "approved") { const credential = result.credential ?? result; if (!credential.access_token) { - throw new ConnectError('token_verification_failed', 'Approved connect response did not include an access token.'); + throw new ConnectError( + "token_verification_failed", + "Approved connect response did not include an access token." + ); } return { access_token: credential.access_token, - token_type: credential.token_type ?? 'Bearer', + token_type: credential.token_type ?? "Bearer", expires_at: credential.expires_at, grant_id: credential.grant_id ?? result.grant_id, scope: credential.scope, }; } - if (status === 'pending' || status === 'authorization_pending') { + if (status === "pending" || status === "authorization_pending") { await sleep(options.intervalMs); continue; } - if (status === 'denied' || status === 'access_denied') { - throw new ConnectError('approval_denied', 'Owner denied the delegated access request.'); + if (status === "denied" || status === "access_denied") { + throw new ConnectError("approval_denied", "Owner denied the delegated access request."); } - if (status === 'expired' || status === 'expired_token') { - throw new ConnectError('approval_expired', 'Delegated access approval expired. Run connect again.'); + if (status === "expired" || status === "expired_token") { + throw new ConnectError("approval_expired", "Delegated access approval expired. Run connect again."); } - if (status === 'insufficient_scope') { - throw new ConnectError('insufficient_scope', 'Approved grant did not include the required PDPP scope.'); + if (status === "insufficient_scope") { + throw new ConnectError("insufficient_scope", "Approved grant did not include the required PDPP scope."); } - throw new ConnectError('connect_contract_invalid', `Unexpected connect polling status: ${status}`); + throw new ConnectError("connect_contract_invalid", `Unexpected connect polling status: ${status}`); } - throw new ConnectError('approval_expired', 'Timed out waiting for delegated access approval.'); + throw new ConnectError("approval_expired", "Timed out waiting for delegated access approval."); } async function verifySchema(fetchFn, providerUrl, accessToken) { - const response = await fetchFn(new URL('/v1/schema', providerUrl), { + const response = await fetchFn(new URL("/v1/schema", providerUrl), { headers: { Authorization: `Bearer ${accessToken}` }, }); if (response.status === 403) { - throw new ConnectError('insufficient_scope', 'Grant cannot read /v1/schema; required scope is missing.'); + throw new ConnectError("insufficient_scope", "Grant cannot read /v1/schema; required scope is missing."); } if (response.status === 401) { - throw new ConnectError('token_verification_failed', 'Grant token was rejected by /v1/schema.'); + throw new ConnectError("token_verification_failed", "Grant token was rejected by /v1/schema."); } if (!response.ok) { - throw new ConnectError('token_verification_failed', `/v1/schema verification failed with HTTP ${response.status}.`); + throw new ConnectError("token_verification_failed", `/v1/schema verification failed with HTTP ${response.status}.`); } return response; } @@ -355,28 +364,28 @@ async function storeCredential(cacheRoot, providerUrl, payload) { } function getCredentialCacheFile(cacheRoot, providerUrl) { - const host = new URL(providerUrl).host.replace(/[^a-zA-Z0-9.-]/g, '_'); - return join(cacheRoot, 'clients', `${host}.json`); + const host = new URL(providerUrl).host.replace(/[^a-zA-Z0-9.-]/g, "_"); + return join(cacheRoot, "clients", `${host}.json`); } async function ensurePdppGitignore(cacheRoot) { - const gitignorePath = join(cacheRoot, '.gitignore'); - let current = ''; + const gitignorePath = join(cacheRoot, ".gitignore"); + let current = ""; try { - current = await readFile(gitignorePath, 'utf8'); + current = await readFile(gitignorePath, "utf8"); } catch {} - if (current.includes('*')) { + if (current.includes("*")) { return; } await mkdir(cacheRoot, { recursive: true, mode: 0o700 }); - const prefix = current && !current.endsWith('\n') ? `${current}\n` : current; + const prefix = current && !current.endsWith("\n") ? `${current}\n` : current; await writeFile(gitignorePath, `${prefix}*\n!.gitignore\n`, { mode: 0o600 }); } async function getJson(fetchFn, url, errorCode) { let response; try { - response = await fetchFn(url, { headers: { Accept: 'application/json' } }); + response = await fetchFn(url, { headers: { Accept: "application/json" } }); } catch (error) { throw new ConnectError(errorCode, `Failed to fetch ${url}: ${error.message}.`); } @@ -390,12 +399,12 @@ async function postJson(fetchFn, url, body) { let response; try { response = await fetchFn(url, { - method: 'POST', - headers: { Accept: 'application/json', 'Content-Type': 'application/json' }, + method: "POST", + headers: { Accept: "application/json", "Content-Type": "application/json" }, body: JSON.stringify(body), }); } catch (error) { - throw new ConnectError('connect_request_failed', `Connect request failed at ${url}: ${error.message}.`); + throw new ConnectError("connect_request_failed", `Connect request failed at ${url}: ${error.message}.`); } if (!response.ok) { let body = null; @@ -403,25 +412,28 @@ async function postJson(fetchFn, url, body) { body = await response.json(); } catch {} const errorCode = body?.error?.code ?? body?.error ?? body?.code; - if (errorCode === 'access_denied') { - throw new ConnectError('approval_denied', 'Owner denied the delegated access request.'); + if (errorCode === "access_denied") { + throw new ConnectError("approval_denied", "Owner denied the delegated access request."); } - if (errorCode === 'expired_token') { - throw new ConnectError('approval_expired', 'Delegated access approval expired. Run connect again.'); + if (errorCode === "expired_token") { + throw new ConnectError("approval_expired", "Delegated access approval expired. Run connect again."); } - if (errorCode === 'insufficient_scope') { - throw new ConnectError('insufficient_scope', 'Approved grant did not include the required PDPP scope.'); + if (errorCode === "insufficient_scope") { + throw new ConnectError("insufficient_scope", "Approved grant did not include the required PDPP scope."); } - if (errorCode === 'invalid_grant') { - throw new ConnectError('token_verification_failed', 'Provider rejected the agent-connect polling handle.'); + if (errorCode === "invalid_grant") { + throw new ConnectError("token_verification_failed", "Provider rejected the agent-connect polling handle."); } const detail = body?.error?.message ?? body?.error_description ?? body?.message; - const suffix = detail ? ` ${detail}` : ''; - throw new ConnectError('connect_request_failed', `Connect request failed at ${url}: HTTP ${response.status}.${suffix}`); + const suffix = detail ? ` ${detail}` : ""; + throw new ConnectError( + "connect_request_failed", + `Connect request failed at ${url}: HTTP ${response.status}.${suffix}` + ); } return response.json(); } function trimTrailingSlash(value) { - return value.length > 1 ? value.replace(/\/+$/, '') : value; + return value.length > 1 ? value.replace(/\/+$/, "") : value; } diff --git a/packages/cli/src/index.js b/packages/cli/src/index.js index 785ecbb44..7b577476f 100644 --- a/packages/cli/src/index.js +++ b/packages/cli/src/index.js @@ -1,23 +1,19 @@ // Copyright The PDP-Connect Contributors // SPDX-License-Identifier: Apache-2.0 -import { - createPdppCliCommand, - getPdppCliPackageInfo, - PDPP_CLI_BIN_NAME, -} from './package-info.js'; -import { ConnectError, connectProvider, normalizeProviderUrl, readStoredCredential } from './connect/flow.js'; -import { runCollector } from './collector/commands.js'; -import { runRefRun } from './ref/commands/run.js'; -import { runRefGrant } from './ref/commands/grant.js'; -import { runRefTrace } from './ref/commands/trace.js'; -import { runRefLogin } from './ref/commands/login.js'; -import { runRefConnectors } from './ref/commands/connectors.js'; -import { runRefEventSubscriptions } from './ref/commands/event-subscriptions.js'; -import { runRefCall } from './ref/commands/call.js'; -import { readHelp, runRead } from './read/commands.js'; -import { runOwnerAgent } from './owner-agent/command.js'; -import { PdppCliError, PdppUsageError } from './ref/errors.js'; +import { createPdppCliCommand, getPdppCliPackageInfo, PDPP_CLI_BIN_NAME } from "./package-info.js"; +import { ConnectError, connectProvider, normalizeProviderUrl, readStoredCredential } from "./connect/flow.js"; +import { runCollector } from "./collector/commands.js"; +import { runRefRun } from "./ref/commands/run.js"; +import { runRefGrant } from "./ref/commands/grant.js"; +import { runRefTrace } from "./ref/commands/trace.js"; +import { runRefLogin } from "./ref/commands/login.js"; +import { runRefConnectors } from "./ref/commands/connectors.js"; +import { runRefEventSubscriptions } from "./ref/commands/event-subscriptions.js"; +import { runRefCall } from "./ref/commands/call.js"; +import { readHelp, runRead } from "./read/commands.js"; +import { runOwnerAgent } from "./owner-agent/command.js"; +import { PdppCliError, PdppUsageError } from "./ref/errors.js"; const HELP = `PDPP CLI @@ -74,21 +70,21 @@ Notes: export async function runCli(argv, io = { stdout: process.stdout, stderr: process.stderr }) { const [command, ...rest] = argv; - if (!command || command === '--help' || command === '-h' || command === 'help') { + if (!command || command === "--help" || command === "-h" || command === "help") { io.stdout.write(`${HELP}\n`); return 0; } - if (command === 'package-info') { - const providerUrl = readOption(rest, '--provider-url'); + if (command === "package-info") { + const providerUrl = readOption(rest, "--provider-url"); io.stdout.write(`${JSON.stringify(getPdppCliPackageInfo(providerUrl), null, 2)}\n`); return 0; } - if (command === 'connect') { + if (command === "connect") { const providerUrl = rest[0]; if (!providerUrl) { - io.stderr.write('Usage: pdpp connect \n'); + io.stderr.write("Usage: pdpp connect \n"); return 64; } @@ -104,16 +100,16 @@ export async function runCli(argv, io = { stdout: process.stdout, stderr: proces } } - if (command === 'token') { + if (command === "token") { const providerUrl = readFirstPositional(rest); if (!providerUrl) { - io.stderr.write('Usage: pdpp token \n'); + io.stderr.write("Usage: pdpp token \n"); return 64; } try { const { credential } = await readStoredCredential(providerUrl, { - cacheRoot: readOption(rest, '--cache-root'), + cacheRoot: readOption(rest, "--cache-root"), }); io.stdout.write(`${credential.access_token}\n`); return 0; @@ -126,11 +122,11 @@ export async function runCli(argv, io = { stdout: process.stdout, stderr: proces } } - if (command === 'collector') { + if (command === "collector") { return await runCollector(rest, io); } - if (command === 'read') { + if (command === "read") { try { return await runRead(rest, io); } catch (error) { @@ -146,25 +142,43 @@ export async function runCli(argv, io = { stdout: process.stdout, stderr: proces } } - if (command === 'owner-agent') { + if (command === "owner-agent") { return await runOwnerAgent(rest, io); } - if (command === 'ref') { + if (command === "ref") { const [refCommand, ...refRest] = rest; - if (!refCommand || refCommand === '--help' || refCommand === '-h') { + if (!refCommand || refCommand === "--help" || refCommand === "-h") { io.stdout.write(`Reference diagnostics (reference server only):\n`); io.stdout.write(` ${PDPP_CLI_BIN_NAME} ref login [--password-stdin] [--cache-root ]\n`); - io.stdout.write(` ${PDPP_CLI_BIN_NAME} ref call --as-url [--data | --data-stdin] [--auth cookie|bearer] [--owner-session ] [--owner-token-stdin] [--status-only] [--format json|table]\n`); - io.stdout.write(` ${PDPP_CLI_BIN_NAME} ref run timeline --as-url [--owner-session ] [--format json|table]\n`); - io.stdout.write(` ${PDPP_CLI_BIN_NAME} ref grant timeline --as-url [--owner-session ] [--format json|table]\n`); - io.stdout.write(` ${PDPP_CLI_BIN_NAME} ref trace show --as-url [--owner-session ] [--format json|table]\n`); - io.stdout.write(` ${PDPP_CLI_BIN_NAME} ref connectors list --as-url [--owner-session ] [--format json|table] [--verbose]\n`); - io.stdout.write(` ${PDPP_CLI_BIN_NAME} ref connectors show --as-url [--owner-session ] [--format json|table] [--verbose]\n`); - io.stdout.write(` ${PDPP_CLI_BIN_NAME} ref event-subscriptions list --as-url [--client-id ] [--grant-id ] [--status ] [--owner-session ] [--format json|table]\n`); - io.stdout.write(` ${PDPP_CLI_BIN_NAME} ref event-subscriptions show --as-url [--owner-session ] [--format json|table]\n`); - io.stdout.write(` ${PDPP_CLI_BIN_NAME} ref event-subscriptions disable --as-url [--reason ] [--yes] [--owner-session ]\n`); + io.stdout.write( + ` ${PDPP_CLI_BIN_NAME} ref call --as-url [--data | --data-stdin] [--auth cookie|bearer] [--owner-session ] [--owner-token-stdin] [--status-only] [--format json|table]\n` + ); + io.stdout.write( + ` ${PDPP_CLI_BIN_NAME} ref run timeline --as-url [--owner-session ] [--format json|table]\n` + ); + io.stdout.write( + ` ${PDPP_CLI_BIN_NAME} ref grant timeline --as-url [--owner-session ] [--format json|table]\n` + ); + io.stdout.write( + ` ${PDPP_CLI_BIN_NAME} ref trace show --as-url [--owner-session ] [--format json|table]\n` + ); + io.stdout.write( + ` ${PDPP_CLI_BIN_NAME} ref connectors list --as-url [--owner-session ] [--format json|table] [--verbose]\n` + ); + io.stdout.write( + ` ${PDPP_CLI_BIN_NAME} ref connectors show --as-url [--owner-session ] [--format json|table] [--verbose]\n` + ); + io.stdout.write( + ` ${PDPP_CLI_BIN_NAME} ref event-subscriptions list --as-url [--client-id ] [--grant-id ] [--status ] [--owner-session ] [--format json|table]\n` + ); + io.stdout.write( + ` ${PDPP_CLI_BIN_NAME} ref event-subscriptions show --as-url [--owner-session ] [--format json|table]\n` + ); + io.stdout.write( + ` ${PDPP_CLI_BIN_NAME} ref event-subscriptions disable --as-url [--reason ] [--yes] [--owner-session ]\n` + ); io.stdout.write(`\nNotes:\n`); io.stdout.write(` "ref login" prompts the reference server's owner-login route and caches the\n`); io.stdout.write(` resulting session in .pdpp/owner-sessions/ (mode 0600). The cookie value is\n`); @@ -184,7 +198,7 @@ export async function runCli(argv, io = { stdout: process.stdout, stderr: proces grant: runRefGrant, trace: runRefTrace, connectors: runRefConnectors, - 'event-subscriptions': runRefEventSubscriptions, + "event-subscriptions": runRefEventSubscriptions, }; const handler = refDispatch[refCommand]; if (!handler) { @@ -223,7 +237,7 @@ function readOption(argv, name) { function readFirstPositional(argv) { for (let index = 0; index < argv.length; index += 1) { const value = argv[index]; - if (value.startsWith('--')) { + if (value.startsWith("--")) { index += 1; continue; } diff --git a/packages/cli/src/owner-agent/command.js b/packages/cli/src/owner-agent/command.js index 8bdd36a75..418e56e23 100644 --- a/packages/cli/src/owner-agent/command.js +++ b/packages/cli/src/owner-agent/command.js @@ -24,11 +24,11 @@ // `pdpp connect` path. It must never present owner bearers as the default path // for ordinary agents or external MCP clients. -import { parseArgs } from '../ref/args.js'; -import { ownerSessionHeaders } from '../ref/fetch.js'; +import { parseArgs } from "../ref/args.js"; +import { ownerSessionHeaders } from "../ref/fetch.js"; -import { resolveCredentialFile, writeOwnerAgentCredential, buildCredentialRecord } from './credential-store.js'; -import { discoverOwnerAgentControl, formatOwnerAgentControl } from './control.js'; +import { resolveCredentialFile, writeOwnerAgentCredential, buildCredentialRecord } from "./credential-store.js"; +import { discoverOwnerAgentControl, formatOwnerAgentControl } from "./control.js"; import { findConnectorTemplates, formatConnectionSetupPlan, @@ -36,11 +36,11 @@ import { formatConnectorTemplates, requestConnectionSetupPlan, requestConnectorTemplates, -} from './setup.js'; -import { discoverOwnerAgentProfile, normalizeEntrypointUrl } from './discovery.js'; -import { initiateDeviceAuthorization, pollForOwnerAgentToken } from './device-flow.js'; -import { OwnerAgentError } from './errors.js'; -import { introspectOwnerAgentCredential, readCredentialRecord, revokeOwnerAgentCredential } from './lifecycle.js'; +} from "./setup.js"; +import { discoverOwnerAgentProfile, normalizeEntrypointUrl } from "./discovery.js"; +import { initiateDeviceAuthorization, pollForOwnerAgentToken } from "./device-flow.js"; +import { OwnerAgentError } from "./errors.js"; +import { introspectOwnerAgentCredential, readCredentialRecord, revokeOwnerAgentCredential } from "./lifecycle.js"; const USAGE = `Trusted owner-agent onboarding (owner-level local automation): pdpp owner-agent onboard [--credential-file ] [--client-id ] [--client-name ] @@ -72,28 +72,28 @@ export async function runOwnerAgent(argv, io = {}, deps = {}) { const err = io.stderr ?? process.stderr; const [subcommand, ...rest] = argv; - if (!subcommand || subcommand === '--help' || subcommand === '-h' || subcommand === 'help') { + if (!subcommand || subcommand === "--help" || subcommand === "-h" || subcommand === "help") { out.write(`${USAGE}\n`); return 0; } try { - if (subcommand === 'onboard') { + if (subcommand === "onboard") { return await runOnboard(rest, { out }, deps); } - if (subcommand === 'status') { + if (subcommand === "status") { return await runStatus(rest, { out }, deps); } - if (subcommand === 'control') { + if (subcommand === "control") { return await runControl(rest, { out }, deps); } - if (subcommand === 'connectors') { + if (subcommand === "connectors") { return await runConnectors(rest, { out }, deps); } - if (subcommand === 'setup') { + if (subcommand === "setup") { return await runSetup(rest, { out }, deps); } - if (subcommand === 'revoke') { + if (subcommand === "revoke") { return await runRevoke(rest, { out }, deps); } err.write(`Unknown owner-agent command: ${subcommand}\n\n${USAGE}\n`); @@ -110,48 +110,44 @@ export async function runOwnerAgent(argv, io = {}, deps = {}) { async function runConnectors(argv, { out }, deps) { const { record, positionals } = await loadRecord(argv, deps); const fetchFn = deps.fetch ?? globalThis.fetch; - const action = positionals[0] ?? 'list'; + const action = positionals[0] ?? "list"; const templates = await requestConnectorTemplates({ fetchFn, record }); - if (action === 'list') { + if (action === "list") { out.write(formatConnectorTemplates(templates)); return 0; } - if (action === 'search') { - const query = positionals.slice(1).join(' ').trim(); + if (action === "search") { + const query = positionals.slice(1).join(" ").trim(); if (!query) { throw new OwnerAgentError( - 'invalid_request', - 'Usage: pdpp owner-agent connectors search [--credential-file ] [--entrypoint ]', + "invalid_request", + "Usage: pdpp owner-agent connectors search [--credential-file ] [--entrypoint ]", 64 ); } out.write(formatConnectorTemplates(templates, { query })); return 0; } - if (action === 'explain') { + if (action === "explain") { const connectorId = positionals[1]; - if (typeof connectorId !== 'string' || !connectorId.trim()) { + if (typeof connectorId !== "string" || !connectorId.trim()) { throw new OwnerAgentError( - 'invalid_request', - 'Usage: pdpp owner-agent connectors explain [--credential-file ] [--entrypoint ]', + "invalid_request", + "Usage: pdpp owner-agent connectors explain [--credential-file ] [--entrypoint ]", 64 ); } const matches = findConnectorTemplates(templates, connectorId); const exact = matches.find((template) => { const key = template?.connector_key ?? template?.connector_id; - return typeof key === 'string' && key.toLowerCase() === connectorId.trim().toLowerCase(); + return typeof key === "string" && key.toLowerCase() === connectorId.trim().toLowerCase(); }); out.write(formatConnectorTemplateExplain(exact ?? matches[0] ?? null)); return 0; } - throw new OwnerAgentError( - 'invalid_request', - `Unknown owner-agent connectors command: ${action}\n\n${USAGE}`, - 64 - ); + throw new OwnerAgentError("invalid_request", `Unknown owner-agent connectors command: ${action}\n\n${USAGE}`, 64); } async function runOnboard(argv, { out }, deps) { @@ -159,8 +155,8 @@ async function runOnboard(argv, { out }, deps) { const entrypoint = normalizeEntrypointUrl(positionals[0]); if (!entrypoint) { throw new OwnerAgentError( - 'invalid_entrypoint', - 'Usage: pdpp owner-agent onboard [--credential-file ]', + "invalid_entrypoint", + "Usage: pdpp owner-agent onboard [--credential-file ]", 64 ); } @@ -169,16 +165,16 @@ async function runOnboard(argv, { out }, deps) { const profile = await discoverOwnerAgentProfile(entrypoint, { fetch: fetchFn }); - const explicitClientId = typeof flags['client-id'] === 'string' ? flags['client-id'] : undefined; + const explicitClientId = typeof flags["client-id"] === "string" ? flags["client-id"] : undefined; const registeredClient = explicitClientId ? { client_id: explicitClientId, client_name: null } : await registerOwnerAgentClient({ fetchFn, endpoint: profile.registrationEndpoint, clientName: - typeof flags['client-name'] === 'string' && flags['client-name'].trim() - ? flags['client-name'].trim() - : 'PDPP trusted owner agent', + typeof flags["client-name"] === "string" && flags["client-name"].trim() + ? flags["client-name"].trim() + : "PDPP trusted owner agent", }); const clientId = registeredClient.client_id; const registrationClientUri = buildRegistrationClientUri(profile, clientId); @@ -189,12 +185,12 @@ async function runOnboard(argv, { out }, deps) { }); // Print only non-secret approval instructions. - out.write('Trusted owner-agent onboarding (owner-level local automation).\n'); + out.write("Trusted owner-agent onboarding (owner-level local automation).\n"); out.write(`Open this URL in a browser to approve owner-agent access:\n${device.verificationUri}\n`); if (device.userCode) { out.write(`Verification code: ${device.userCode}\n`); } - out.write('Waiting for owner approval...\n'); + out.write("Waiting for owner approval...\n"); const credential = await pollForOwnerAgentToken({ fetchFn, @@ -223,7 +219,7 @@ async function runOnboard(argv, { out }, deps) { }); const targetPath = resolveCredentialFile({ - credentialFile: typeof flags['credential-file'] === 'string' ? flags['credential-file'] : undefined, + credentialFile: typeof flags["credential-file"] === "string" ? flags["credential-file"] : undefined, resource: profile.resource, home: deps.home, }); @@ -240,9 +236,9 @@ async function runOnboard(argv, { out }, deps) { out.write(` expires: ${record.expires_at}\n`); } if (record.registration_client_uri) { - out.write(' revocation: owner-session-gated RFC 7592 client delete handle stored\n'); + out.write(" revocation: owner-session-gated RFC 7592 client delete handle stored\n"); } - out.write('Note: /mcp rejects owner bearers; this credential is for owner-level REST/control-plane use.\n'); + out.write("Note: /mcp rejects owner bearers; this credential is for owner-level REST/control-plane use.\n"); return 0; } @@ -269,14 +265,14 @@ async function runControl(argv, { out }, deps) { async function runSetup(argv, { out }, deps) { const { record, flags, positionals } = await loadRecord(argv, deps); const connectorId = positionals[0]; - if (typeof connectorId !== 'string' || !connectorId.trim()) { + if (typeof connectorId !== "string" || !connectorId.trim()) { throw new OwnerAgentError( - 'invalid_request', - 'Usage: pdpp owner-agent setup [--display-name ] [--credential-file ] [--entrypoint ]', + "invalid_request", + "Usage: pdpp owner-agent setup [--display-name ] [--credential-file ] [--entrypoint ]", 64 ); } - const displayName = typeof flags['display-name'] === 'string' ? flags['display-name'] : null; + const displayName = typeof flags["display-name"] === "string" ? flags["display-name"] : null; const fetchFn = deps.fetch ?? globalThis.fetch; const plan = await requestConnectionSetupPlan({ fetchFn, record, connectorId, displayName }); out.write(formatConnectionSetupPlan(plan)); @@ -287,9 +283,9 @@ async function runRevoke(argv, { out }, deps) { const { record, targetPath, flags } = await loadRecord(argv, deps); const fetchFn = deps.fetch ?? globalThis.fetch; const ownerSession = ownerSessionHeaders({ - ownerSession: flags['owner-session'] || '', + ownerSession: flags["owner-session"] || "", referenceUrl: record.authorization_server, - cacheRoot: flags['cache-root'], + cacheRoot: flags["cache-root"], }).Cookie; const result = await revokeOwnerAgentCredential({ fetchFn, record, ownerSessionCookie: ownerSession }); out.write( @@ -302,11 +298,11 @@ async function runRevoke(argv, { out }, deps) { async function loadRecord(argv, deps) { const { flags, positionals } = parseArgs(argv); - const credentialFile = typeof flags['credential-file'] === 'string' ? flags['credential-file'] : undefined; - const entrypoint = typeof flags.entrypoint === 'string' ? normalizeEntrypointUrl(flags.entrypoint) : null; + const credentialFile = typeof flags["credential-file"] === "string" ? flags["credential-file"] : undefined; + const entrypoint = typeof flags.entrypoint === "string" ? normalizeEntrypointUrl(flags.entrypoint) : null; const targetPath = resolveCredentialFile({ credentialFile, - resource: entrypoint ?? 'https://owner-agent.invalid', + resource: entrypoint ?? "https://owner-agent.invalid", home: deps.home, }); const record = await readCredentialRecord(targetPath); @@ -318,25 +314,25 @@ export { USAGE as OWNER_AGENT_USAGE }; async function registerOwnerAgentClient({ fetchFn, endpoint, clientName }) { if (!endpoint) { throw new OwnerAgentError( - 'registration_unavailable', - 'Owner-agent onboarding requires a registration_endpoint, or pass --client-id for an existing public client.' + "registration_unavailable", + "Owner-agent onboarding requires a registration_endpoint, or pass --client-id for an existing public client." ); } let response; try { response = await fetchFn(endpoint, { - method: 'POST', + method: "POST", headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', + Accept: "application/json", + "Content-Type": "application/json", }, body: JSON.stringify({ client_name: clientName, - token_endpoint_auth_method: 'none', + token_endpoint_auth_method: "none", }), }); } catch (error) { - throw new OwnerAgentError('registration_failed', `Dynamic client registration failed: ${error.message}.`); + throw new OwnerAgentError("registration_failed", `Dynamic client registration failed: ${error.message}.`); } let json = null; try { @@ -346,10 +342,13 @@ async function registerOwnerAgentClient({ fetchFn, endpoint, clientName }) { } if (!response.ok) { const code = json?.error?.code ?? json?.error ?? `http_${response.status}`; - throw new OwnerAgentError('registration_failed', `Dynamic client registration failed (${code}).`); + throw new OwnerAgentError("registration_failed", `Dynamic client registration failed (${code}).`); } if (!json?.client_id) { - throw new OwnerAgentError('registration_invalid', 'Dynamic client registration response did not include client_id.'); + throw new OwnerAgentError( + "registration_invalid", + "Dynamic client registration response did not include client_id." + ); } return json; } @@ -359,10 +358,10 @@ function buildRegistrationClientUri(profile, clientId) { return null; } if (profile.revocationPathTemplate) { - return profile.revocationPathTemplate.replace('{client_id}', encodeURIComponent(clientId)); + return profile.revocationPathTemplate.replace("{client_id}", encodeURIComponent(clientId)); } if (profile.registrationEndpoint) { - const base = profile.registrationEndpoint.endsWith('/') + const base = profile.registrationEndpoint.endsWith("/") ? profile.registrationEndpoint : `${profile.registrationEndpoint}/`; return `${base}${encodeURIComponent(clientId)}`; diff --git a/packages/cli/src/owner-agent/control.js b/packages/cli/src/owner-agent/control.js index 5570af667..ea16fd1dc 100644 --- a/packages/cli/src/owner-agent/control.js +++ b/packages/cli/src/owner-agent/control.js @@ -18,8 +18,8 @@ // header. It is NEVER printed: this command emits only non-secret capability // and connection metadata. -import { OwnerAgentError } from './errors.js'; -import { getOwnerAgentAccessToken } from './lifecycle.js'; +import { OwnerAgentError } from "./errors.js"; +import { getOwnerAgentAccessToken } from "./lifecycle.js"; /** * Fetch the owner-agent control capability document and the configured @@ -34,24 +34,19 @@ import { getOwnerAgentAccessToken } from './lifecycle.js'; export async function discoverOwnerAgentControl({ fetchFn, record }) { const token = getOwnerAgentAccessToken(record); if (!token) { - throw new OwnerAgentError('credential_invalid', 'Stored credential is missing an access token.'); + throw new OwnerAgentError("credential_invalid", "Stored credential is missing an access token."); } - const resource = typeof record?.resource === 'string' ? record.resource.replace(/\/$/, '') : null; + const resource = typeof record?.resource === "string" ? record.resource.replace(/\/$/, "") : null; if (!resource) { throw new OwnerAgentError( - 'credential_invalid', - 'Stored credential has no resource origin; re-run `pdpp owner-agent onboard`.' + "credential_invalid", + "Stored credential has no resource origin; re-run `pdpp owner-agent onboard`." ); } - const headers = { Accept: 'application/json', Authorization: `Bearer ${token}` }; + const headers = { Accept: "application/json", Authorization: `Bearer ${token}` }; - const control = await getJson(fetchFn, `${resource}/v1/owner/control`, headers, 'control_failed'); - const connectionsBody = await getJson( - fetchFn, - `${resource}/v1/owner/connections`, - headers, - 'connections_failed' - ); + const control = await getJson(fetchFn, `${resource}/v1/owner/control`, headers, "control_failed"); + const connectionsBody = await getJson(fetchFn, `${resource}/v1/owner/connections`, headers, "connections_failed"); const connections = Array.isArray(connectionsBody?.data) ? connectionsBody.data : []; return { control, connections }; } @@ -64,32 +59,32 @@ export async function discoverOwnerAgentControl({ fetchFn, record }) { */ export function formatOwnerAgentControl({ control, connections }) { const lines = []; - lines.push('Owner-agent control capabilities (non-secret):'); + lines.push("Owner-agent control capabilities (non-secret):"); if (control?.entrypoint) { lines.push(` entrypoint: ${control.entrypoint}`); } lines.push(` /mcp owner bearer: rejected (use owner-bearer /v1/* REST, not /mcp)`); - lines.push(''); - lines.push(' Action families:'); + lines.push(""); + lines.push(" Action families:"); const actions = Array.isArray(control?.actions) ? control.actions : []; for (const action of actions) { - const family = action?.family ?? 'unknown'; - const status = action?.status ?? 'unknown'; - const route = action?.method && action?.url ? `${action.method} ${action.url}` : '(owner-mediated / not a route)'; + const family = action?.family ?? "unknown"; + const status = action?.status ?? "unknown"; + const route = action?.method && action?.url ? `${action.method} ${action.url}` : "(owner-mediated / not a route)"; lines.push(` - ${family} [${status}] ${route}`); if (action?.reason) { lines.push(` ${action.reason}`); } } - lines.push(''); + lines.push(""); lines.push(`Configured connections (${connections.length}):`); if (connections.length === 0) { - lines.push(' (none yet — initiate one with the initiate_connection action above)'); + lines.push(" (none yet — initiate one with the initiate_connection action above)"); } for (const connection of connections) { - const connectionId = connection?.connection_id ?? '(no connection_id)'; - const connectorId = connection?.connector_id ?? connection?.connector_key ?? '(unknown connector)'; + const connectionId = connection?.connection_id ?? "(no connection_id)"; + const connectorId = connection?.connector_id ?? connection?.connector_key ?? "(unknown connector)"; lines.push(` - ${connectionId} connector=${connectorId}`); const label = formatLabel(connection); lines.push(` label: ${label}`); @@ -97,7 +92,7 @@ export function formatOwnerAgentControl({ control, connections }) { lines.push(` status: ${connection.status}`); } } - return `${lines.join('\n')}\n`; + return `${lines.join("\n")}\n`; } // An owner-meaningful label (`owner_set`) is printed as-is. A `fallback` label @@ -106,14 +101,14 @@ export function formatOwnerAgentControl({ control, connections }) { // relying on it. Never invent a label here. function formatLabel(connection) { const labelStatus = connection?.label_status; - const displayName = typeof connection?.display_name === 'string' ? connection.display_name : null; - if (labelStatus === 'owner_set' && displayName) { + const displayName = typeof connection?.display_name === "string" ? connection.display_name : null; + if (labelStatus === "owner_set" && displayName) { return `"${displayName}" (owner_set)`; } if (displayName) { return `label-needed (fallback: "${displayName}" — rename with rename_connection)`; } - return 'label-needed (no display_name — rename with rename_connection)'; + return "label-needed (no display_name — rename with rename_connection)"; } async function getJson(fetchFn, url, headers, errorCode) { @@ -125,7 +120,7 @@ async function getJson(fetchFn, url, headers, errorCode) { } if (response.status === 401 || response.status === 403) { throw new OwnerAgentError( - 'control_unauthorized', + "control_unauthorized", `Owner-agent control is not authorized (HTTP ${response.status}). The credential may be revoked or inactive; run \`pdpp owner-agent status\`.`, 4 ); diff --git a/packages/cli/src/owner-agent/credential-store.js b/packages/cli/src/owner-agent/credential-store.js index 8e12e6641..05dc73a15 100644 --- a/packages/cli/src/owner-agent/credential-store.js +++ b/packages/cli/src/owner-agent/credential-store.js @@ -16,11 +16,11 @@ // home directory, not a project-local `.pdpp/`, so an owner-level bearer is // never accidentally committed alongside project files. -import { chmod, mkdir, writeFile } from 'node:fs/promises'; -import { homedir } from 'node:os'; -import { dirname, isAbsolute, join, resolve } from 'node:path'; +import { chmod, mkdir, writeFile } from "node:fs/promises"; +import { homedir } from "node:os"; +import { dirname, isAbsolute, join, resolve } from "node:path"; -export const DEFAULT_OWNER_AGENT_DIR = join('.pdpp', 'owner-agents'); +export const DEFAULT_OWNER_AGENT_DIR = join(".pdpp", "owner-agents"); /** * Resolve the absolute credential-file path for an owner-agent credential. @@ -79,12 +79,12 @@ export function buildCredentialRecord({ streamsEndpoint, createdAt, }) { - const tokenType = credential.token_type ?? 'Bearer'; + const tokenType = credential.token_type ?? "Bearer"; const expiresAt = credential.expires_at ?? null; const scope = credential.scope ?? null; return { - profile: 'trusted_owner_agent', - pdpp_token_kind: 'owner', + profile: "trusted_owner_agent", + pdpp_token_kind: "owner", resource, authorization_server: authorizationServer ?? null, client_id: clientId ?? null, @@ -115,15 +115,15 @@ export function buildCredentialRecord({ } function expandHome(p, base) { - if (p === '~') return base; - if (p.startsWith('~/')) return join(base, p.slice(2)); + if (p === "~") return base; + if (p.startsWith("~/")) return join(base, p.slice(2)); return p; } function hostSlug(resource) { try { - return new URL(resource).host.replace(/[^a-zA-Z0-9.-]/g, '_'); + return new URL(resource).host.replace(/[^a-zA-Z0-9.-]/g, "_"); } catch { - return 'owner-agent'; + return "owner-agent"; } } diff --git a/packages/cli/src/owner-agent/device-flow.js b/packages/cli/src/owner-agent/device-flow.js index 21b180990..6e8d0ea1c 100644 --- a/packages/cli/src/owner-agent/device-flow.js +++ b/packages/cli/src/owner-agent/device-flow.js @@ -8,9 +8,9 @@ // token endpoint is NEVER printed here — it is returned to the caller for // non-printing storage. -import { OwnerAgentError } from './errors.js'; +import { OwnerAgentError } from "./errors.js"; -const DEVICE_CODE_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:device_code'; +const DEVICE_CODE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code"; const DEFAULT_POLL_INTERVAL_MS = 5000; const DEFAULT_POLL_TIMEOUT_MS = 5 * 60 * 1000; @@ -20,14 +20,14 @@ const DEFAULT_POLL_TIMEOUT_MS = 5 * 60 * 1000; export async function initiateDeviceAuthorization({ fetchFn, endpoint, clientId }) { const body = new URLSearchParams(); if (clientId) { - body.set('client_id', clientId); + body.set("client_id", clientId); } const result = await postForm(fetchFn, endpoint, body); const verificationUri = result.verification_uri_complete ?? result.verification_uri; if (!result.device_code || !verificationUri) { throw new OwnerAgentError( - 'device_authorization_invalid', - 'Device authorization response did not include a device_code and verification URI.' + "device_authorization_invalid", + "Device authorization response did not include a device_code and verification URI." ); } return { @@ -36,7 +36,9 @@ export async function initiateDeviceAuthorization({ fetchFn, endpoint, clientId verificationUri, verificationUriComplete: result.verification_uri_complete ?? null, intervalMs: Number.isFinite(Number(result.interval)) ? Number(result.interval) * 1000 : DEFAULT_POLL_INTERVAL_MS, - expiresInMs: Number.isFinite(Number(result.expires_in)) ? Number(result.expires_in) * 1000 : DEFAULT_POLL_TIMEOUT_MS, + expiresInMs: Number.isFinite(Number(result.expires_in)) + ? Number(result.expires_in) * 1000 + : DEFAULT_POLL_TIMEOUT_MS, }; } @@ -61,10 +63,10 @@ export async function pollForOwnerAgentToken({ while (now() - startedAt <= timeoutMs) { const body = new URLSearchParams(); - body.set('grant_type', DEVICE_CODE_GRANT_TYPE); - body.set('device_code', deviceCode); + body.set("grant_type", DEVICE_CODE_GRANT_TYPE); + body.set("device_code", deviceCode); if (clientId) { - body.set('client_id', clientId); + body.set("client_id", clientId); } const { status, json } = await postFormRaw(fetchFn, endpoint, body); @@ -73,38 +75,44 @@ export async function pollForOwnerAgentToken({ if (status >= 200 && status < 300 && json?.access_token) { return { access_token: json.access_token, - token_type: json.token_type ?? 'Bearer', + token_type: json.token_type ?? "Bearer", expires_at: expiresAt(json.expires_in, now), scope: json.scope ?? null, registration_client_uri: json.registration_client_uri ?? null, }; } - if (errorCode === 'authorization_pending') { - onPending?.('pending'); + if (errorCode === "authorization_pending") { + onPending?.("pending"); await sleep(currentInterval); continue; } - if (errorCode === 'slow_down') { + if (errorCode === "slow_down") { currentInterval += 5000; - onPending?.('slow_down'); + onPending?.("slow_down"); await sleep(currentInterval); continue; } - if (errorCode === 'access_denied') { - throw new OwnerAgentError('approval_denied', 'Owner denied the trusted owner-agent onboarding request.'); + if (errorCode === "access_denied") { + throw new OwnerAgentError("approval_denied", "Owner denied the trusted owner-agent onboarding request."); } - if (errorCode === 'expired_token') { - throw new OwnerAgentError('approval_expired', 'Owner-agent approval expired before it was granted. Run onboarding again.'); + if (errorCode === "expired_token") { + throw new OwnerAgentError( + "approval_expired", + "Owner-agent approval expired before it was granted. Run onboarding again." + ); } - if (errorCode === 'invalid_client' || errorCode === 'invalid_grant') { - throw new OwnerAgentError('token_exchange_failed', `Token endpoint rejected the device-code exchange (${errorCode}).`); + if (errorCode === "invalid_client" || errorCode === "invalid_grant") { + throw new OwnerAgentError( + "token_exchange_failed", + `Token endpoint rejected the device-code exchange (${errorCode}).` + ); } - throw new OwnerAgentError('token_exchange_failed', `Unexpected token endpoint response (HTTP ${status}).`); + throw new OwnerAgentError("token_exchange_failed", `Unexpected token endpoint response (HTTP ${status}).`); } - throw new OwnerAgentError('approval_expired', 'Timed out waiting for owner approval of the owner-agent credential.'); + throw new OwnerAgentError("approval_expired", "Timed out waiting for owner approval of the owner-agent credential."); } function expiresAt(expiresIn, now) { @@ -119,7 +127,7 @@ async function postForm(fetchFn, url, body) { const { status, json } = await postFormRaw(fetchFn, url, body); if (status < 200 || status >= 300) { const errorCode = json?.error?.code ?? json?.error ?? json?.code ?? `http_${status}`; - throw new OwnerAgentError('device_authorization_failed', `Device authorization failed (${errorCode}).`); + throw new OwnerAgentError("device_authorization_failed", `Device authorization failed (${errorCode}).`); } return json ?? {}; } @@ -128,15 +136,15 @@ async function postFormRaw(fetchFn, url, body) { let response; try { response = await fetchFn(url, { - method: 'POST', + method: "POST", headers: { - Accept: 'application/json', - 'Content-Type': 'application/x-www-form-urlencoded', + Accept: "application/json", + "Content-Type": "application/x-www-form-urlencoded", }, body: body.toString(), }); } catch (error) { - throw new OwnerAgentError('request_failed', `Request to ${url} failed: ${error.message}.`); + throw new OwnerAgentError("request_failed", `Request to ${url} failed: ${error.message}.`); } let json = null; try { diff --git a/packages/cli/src/owner-agent/discovery.js b/packages/cli/src/owner-agent/discovery.js index cb1af3754..e7fc45933 100644 --- a/packages/cli/src/owner-agent/discovery.js +++ b/packages/cli/src/owner-agent/discovery.js @@ -23,29 +23,29 @@ // // This module does NOT emit server metadata. It only consumes it. -import { OwnerAgentError } from './errors.js'; +import { OwnerAgentError } from "./errors.js"; -const PROTECTED_RESOURCE_METADATA_PATH = '/.well-known/oauth-protected-resource'; -const AUTHORIZATION_SERVER_METADATA_PATH = '/.well-known/oauth-authorization-server'; +const PROTECTED_RESOURCE_METADATA_PATH = "/.well-known/oauth-protected-resource"; +const AUTHORIZATION_SERVER_METADATA_PATH = "/.well-known/oauth-authorization-server"; export function normalizeEntrypointUrl(value) { - if (typeof value !== 'string' || value.length === 0) { + if (typeof value !== "string" || value.length === 0) { return null; } try { - const parsed = new URL(value.includes('://') ? value : `https://${value}`); - if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') { + const parsed = new URL(value.includes("://") ? value : `https://${value}`); + if (parsed.protocol !== "https:" && parsed.protocol !== "http:") { return null; } - parsed.username = ''; - parsed.password = ''; - parsed.hash = ''; - parsed.search = ''; - parsed.pathname = parsed.pathname.length > 1 ? parsed.pathname.replace(/\/+$/, '') : parsed.pathname; - if (parsed.pathname === '/') { - parsed.pathname = ''; + parsed.username = ""; + parsed.password = ""; + parsed.hash = ""; + parsed.search = ""; + parsed.pathname = parsed.pathname.length > 1 ? parsed.pathname.replace(/\/+$/, "") : parsed.pathname; + if (parsed.pathname === "/") { + parsed.pathname = ""; } - return parsed.toString().replace(/\/$/, ''); + return parsed.toString().replace(/\/$/, ""); } catch { return null; } @@ -62,17 +62,17 @@ export function normalizeEntrypointUrl(value) { export async function discoverOwnerAgentProfile(entrypointUrl, options = {}) { const resource = normalizeEntrypointUrl(entrypointUrl); if (!resource) { - throw new OwnerAgentError('invalid_entrypoint', `Invalid entrypoint URL: ${entrypointUrl}`, 64); + throw new OwnerAgentError("invalid_entrypoint", `Invalid entrypoint URL: ${entrypointUrl}`, 64); } const fetchFn = options.fetch ?? globalThis.fetch; - if (typeof fetchFn !== 'function') { - throw new OwnerAgentError('fetch_unavailable', 'This Node runtime does not provide fetch().'); + if (typeof fetchFn !== "function") { + throw new OwnerAgentError("fetch_unavailable", "This Node runtime does not provide fetch()."); } const resourceMetadata = await getJson( fetchFn, new URL(PROTECTED_RESOURCE_METADATA_PATH, resource).toString(), - 'metadata_failure' + "metadata_failure" ); // The root pointer (GET /) may also carry the advisory block. We only fetch @@ -85,10 +85,7 @@ export async function discoverOwnerAgentProfile(entrypointUrl, options = {}) { const authorizationServerUrl = selectAuthorizationServer(resourceMetadata, resource); const authorizationMetadata = authorizationServerUrl - ? await getJsonOptional( - fetchFn, - new URL(AUTHORIZATION_SERVER_METADATA_PATH, authorizationServerUrl).toString() - ) + ? await getJsonOptional(fetchFn, new URL(AUTHORIZATION_SERVER_METADATA_PATH, authorizationServerUrl).toString()) : null; const profile = buildProfile({ @@ -100,9 +97,9 @@ export async function discoverOwnerAgentProfile(entrypointUrl, options = {}) { if (!profile.deviceAuthorizationEndpoint || !profile.tokenEndpoint) { throw new OwnerAgentError( - 'onboarding_unavailable', - 'This deployment does not advertise a trusted owner-agent onboarding flow. ' + - 'Expected a pdpp_owner_agent_onboarding block or an RFC 8628 device_authorization_endpoint + token_endpoint.' + "onboarding_unavailable", + "This deployment does not advertise a trusted owner-agent onboarding flow. " + + "Expected a pdpp_owner_agent_onboarding block or an RFC 8628 device_authorization_endpoint + token_endpoint." ); } @@ -110,14 +107,11 @@ export async function discoverOwnerAgentProfile(entrypointUrl, options = {}) { } function readOnboardingBlock(metadata) { - if (!metadata || typeof metadata !== 'object') { + if (!metadata || typeof metadata !== "object") { return null; } - const block = - metadata.pdpp_owner_agent_onboarding ?? - metadata.pdpp_agent_discovery?.owner_agent_onboarding ?? - null; - return block && typeof block === 'object' ? block : null; + const block = metadata.pdpp_owner_agent_onboarding ?? metadata.pdpp_agent_discovery?.owner_agent_onboarding ?? null; + return block && typeof block === "object" ? block : null; } function buildProfile({ resource, authorizationServerUrl, onboarding, authorizationMetadata }) { @@ -130,10 +124,7 @@ function buildProfile({ resource, authorizationServerUrl, onboarding, authorizat onboarding?.device_authorization_endpoint ?? authorizationMetadata?.device_authorization_endpoint, base ); - const tokenEndpoint = resolveEndpoint( - onboarding?.token_endpoint ?? authorizationMetadata?.token_endpoint, - base - ); + const tokenEndpoint = resolveEndpoint(onboarding?.token_endpoint ?? authorizationMetadata?.token_endpoint, base); const introspectionEndpoint = resolveEndpoint( onboarding?.introspection_endpoint ?? authorizationMetadata?.introspection_endpoint, base @@ -150,10 +141,10 @@ function buildProfile({ resource, authorizationServerUrl, onboarding, authorizat ); const streamsEndpoint = resolveEndpoint(onboarding?.streams_endpoint, resource); const revocationPathTemplate = - typeof onboarding?.revocation_path_template === 'string' ? onboarding.revocation_path_template : null; + typeof onboarding?.revocation_path_template === "string" ? onboarding.revocation_path_template : null; return { - profile: onboarding?.profile ?? 'trusted_owner_agent', + profile: onboarding?.profile ?? "trusted_owner_agent", advisory: Boolean(onboarding), resource, authorizationServer: issuer, @@ -177,7 +168,7 @@ function selectAuthorizationServer(resourceMetadata, resource) { } function resolveEndpoint(value, base) { - if (!value || typeof value !== 'string') { + if (!value || typeof value !== "string") { return null; } try { @@ -190,7 +181,7 @@ function resolveEndpoint(value, base) { async function getJson(fetchFn, url, errorCode) { let response; try { - response = await fetchFn(url, { headers: { Accept: 'application/json' } }); + response = await fetchFn(url, { headers: { Accept: "application/json" } }); } catch (error) { throw new OwnerAgentError(errorCode, `Failed to fetch ${url}: ${error.message}.`); } @@ -203,7 +194,7 @@ async function getJson(fetchFn, url, errorCode) { async function getJsonOptional(fetchFn, url) { let response; try { - response = await fetchFn(url, { headers: { Accept: 'application/json' } }); + response = await fetchFn(url, { headers: { Accept: "application/json" } }); } catch { return null; } diff --git a/packages/cli/src/owner-agent/errors.js b/packages/cli/src/owner-agent/errors.js index 1fc181cee..f6480bb5d 100644 --- a/packages/cli/src/owner-agent/errors.js +++ b/packages/cli/src/owner-agent/errors.js @@ -9,7 +9,7 @@ export class OwnerAgentError extends Error { constructor(code, message, exitCode = 69) { super(message); - this.name = 'OwnerAgentError'; + this.name = "OwnerAgentError"; this.code = code; this.exitCode = exitCode; } diff --git a/packages/cli/src/owner-agent/lifecycle.js b/packages/cli/src/owner-agent/lifecycle.js index 84b4f0d59..ec2b69dcf 100644 --- a/packages/cli/src/owner-agent/lifecycle.js +++ b/packages/cli/src/owner-agent/lifecycle.js @@ -6,9 +6,9 @@ // owner-agent credential stops working and an active one can be confirmed // without printing the bearer. -import { readFile } from 'node:fs/promises'; +import { readFile } from "node:fs/promises"; -import { OwnerAgentError } from './errors.js'; +import { OwnerAgentError } from "./errors.js"; /** * Introspect a stored owner-agent credential. Returns the non-secret subset of @@ -17,36 +17,36 @@ import { OwnerAgentError } from './errors.js'; */ export async function introspectOwnerAgentCredential({ fetchFn, record }) { if (!record?.introspection_endpoint) { - throw new OwnerAgentError('introspection_unavailable', 'Stored credential has no introspection endpoint.'); + throw new OwnerAgentError("introspection_unavailable", "Stored credential has no introspection endpoint."); } const token = getOwnerAgentAccessToken(record); if (!token) { - throw new OwnerAgentError('credential_invalid', 'Stored credential is missing an access token.'); + throw new OwnerAgentError("credential_invalid", "Stored credential is missing an access token."); } const body = new URLSearchParams(); - body.set('token', token); + body.set("token", token); let response; try { response = await fetchFn(record.introspection_endpoint, { - method: 'POST', + method: "POST", headers: { - Accept: 'application/json', - 'Content-Type': 'application/x-www-form-urlencoded', + Accept: "application/json", + "Content-Type": "application/x-www-form-urlencoded", Authorization: `Bearer ${token}`, }, body: body.toString(), }); } catch (error) { - throw new OwnerAgentError('request_failed', `Introspection request failed: ${error.message}.`); + throw new OwnerAgentError("request_failed", `Introspection request failed: ${error.message}.`); } if (!response.ok) { - throw new OwnerAgentError('introspection_failed', `Introspection failed with HTTP ${response.status}.`); + throw new OwnerAgentError("introspection_failed", `Introspection failed with HTTP ${response.status}.`); } let json; try { json = await response.json(); } catch { - throw new OwnerAgentError('introspection_failed', 'Introspection response was not valid JSON.'); + throw new OwnerAgentError("introspection_failed", "Introspection response was not valid JSON."); } return { active: Boolean(json.active), @@ -67,39 +67,39 @@ export async function revokeOwnerAgentCredential({ fetchFn, record, ownerSession const uri = record?.registration_client_uri; if (!uri) { throw new OwnerAgentError( - 'revocation_unavailable', - 'Stored credential has no RFC 7592 registration handle (registration_client_uri). ' + - 'Revoke it from the owner dashboard instead.' + "revocation_unavailable", + "Stored credential has no RFC 7592 registration handle (registration_client_uri). " + + "Revoke it from the owner dashboard instead." ); } if (!ownerSessionCookie) { throw new OwnerAgentError( - 'owner_session_required', - 'Revocation requires an owner session. Run `pdpp ref login ` first, or set PDPP_OWNER_SESSION_COOKIE.', + "owner_session_required", + "Revocation requires an owner session. Run `pdpp ref login ` first, or set PDPP_OWNER_SESSION_COOKIE.", 5 ); } let response; try { response = await fetchFn(uri, { - method: 'DELETE', - headers: { Cookie: normalizeOwnerSessionCookie(ownerSessionCookie), Accept: 'application/json' }, + method: "DELETE", + headers: { Cookie: normalizeOwnerSessionCookie(ownerSessionCookie), Accept: "application/json" }, }); } catch (error) { - throw new OwnerAgentError('request_failed', `Revocation request failed: ${error.message}.`); + throw new OwnerAgentError("request_failed", `Revocation request failed: ${error.message}.`); } // RFC 7592 specifies 204 No Content on successful delete. if (response.status === 204 || response.status === 200) { return { revoked: true }; } if (response.status === 401 || response.status === 403) { - throw new OwnerAgentError('revocation_unauthorized', `Revocation rejected (HTTP ${response.status}).`, 4); + throw new OwnerAgentError("revocation_unauthorized", `Revocation rejected (HTTP ${response.status}).`, 4); } if (response.status === 404) { // Already gone is an acceptable terminal state for revocation. return { revoked: true, already_absent: true }; } - throw new OwnerAgentError('revocation_failed', `Revocation failed with HTTP ${response.status}.`); + throw new OwnerAgentError("revocation_failed", `Revocation failed with HTTP ${response.status}.`); } export function getOwnerAgentAccessToken(record) { @@ -109,21 +109,21 @@ export function getOwnerAgentAccessToken(record) { export async function readCredentialRecord(targetPath) { let raw; try { - raw = await readFile(targetPath, 'utf8'); + raw = await readFile(targetPath, "utf8"); } catch (error) { - if (error?.code === 'ENOENT') { - throw new OwnerAgentError('not_onboarded', `No owner-agent credential found at ${targetPath}.`, 5); + if (error?.code === "ENOENT") { + throw new OwnerAgentError("not_onboarded", `No owner-agent credential found at ${targetPath}.`, 5); } throw error; } try { return JSON.parse(raw); } catch { - throw new OwnerAgentError('credential_invalid', `Owner-agent credential at ${targetPath} is not valid JSON.`); + throw new OwnerAgentError("credential_invalid", `Owner-agent credential at ${targetPath} is not valid JSON.`); } } function normalizeOwnerSessionCookie(value) { - const raw = String(value || '').trim(); - return raw.includes('=') ? raw : `pdpp_owner_session=${raw}`; + const raw = String(value || "").trim(); + return raw.includes("=") ? raw : `pdpp_owner_session=${raw}`; } diff --git a/packages/cli/src/owner-agent/setup.js b/packages/cli/src/owner-agent/setup.js index 6caba32c0..04b6c6490 100644 --- a/packages/cli/src/owner-agent/setup.js +++ b/packages/cli/src/owner-agent/setup.js @@ -20,8 +20,8 @@ // cookies, or grant-scoped MCP bearers; it may carry an owner-openable // enrollment code and route names, which are setup material, not secrets. -import { OwnerAgentError } from './errors.js'; -import { getOwnerAgentAccessToken } from './lifecycle.js'; +import { OwnerAgentError } from "./errors.js"; +import { getOwnerAgentAccessToken } from "./lifecycle.js"; /** * Request an owner-mediated connection setup plan from the reference's @@ -38,26 +38,26 @@ import { getOwnerAgentAccessToken } from './lifecycle.js'; export async function requestConnectionSetupPlan({ fetchFn, record, connectorId, displayName }) { const token = getOwnerAgentAccessToken(record); if (!token) { - throw new OwnerAgentError('credential_invalid', 'Stored credential is missing an access token.'); + throw new OwnerAgentError("credential_invalid", "Stored credential is missing an access token."); } - const resource = typeof record?.resource === 'string' ? record.resource.replace(/\/$/, '') : null; + const resource = typeof record?.resource === "string" ? record.resource.replace(/\/$/, "") : null; if (!resource) { throw new OwnerAgentError( - 'credential_invalid', - 'Stored credential has no resource origin; re-run `pdpp owner-agent onboard`.' + "credential_invalid", + "Stored credential has no resource origin; re-run `pdpp owner-agent onboard`." ); } - const trimmedConnector = typeof connectorId === 'string' ? connectorId.trim() : ''; + const trimmedConnector = typeof connectorId === "string" ? connectorId.trim() : ""; if (!trimmedConnector) { throw new OwnerAgentError( - 'invalid_request', - 'Usage: pdpp owner-agent setup [--display-name ]', + "invalid_request", + "Usage: pdpp owner-agent setup [--display-name ]", 64 ); } const body = { connector_id: trimmedConnector }; - const trimmedDisplayName = typeof displayName === 'string' ? displayName.trim() : ''; + const trimmedDisplayName = typeof displayName === "string" ? displayName.trim() : ""; if (trimmedDisplayName) { body.display_name = trimmedDisplayName; } @@ -66,21 +66,21 @@ export async function requestConnectionSetupPlan({ fetchFn, record, connectorId, let response; try { response = await fetchFn(url, { - method: 'POST', + method: "POST", headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', + Accept: "application/json", + "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify(body), }); } catch (error) { - throw new OwnerAgentError('setup_failed', `Failed to request setup plan from ${url}: ${error.message}.`); + throw new OwnerAgentError("setup_failed", `Failed to request setup plan from ${url}: ${error.message}.`); } if (response.status === 401 || response.status === 403) { throw new OwnerAgentError( - 'setup_unauthorized', + "setup_unauthorized", `Owner-agent setup is not authorized (HTTP ${response.status}). The credential may be revoked or inactive; run \`pdpp owner-agent status\`.`, 4 ); @@ -96,12 +96,12 @@ export async function requestConnectionSetupPlan({ fetchFn, record, connectorId, if (!response.ok) { const code = json?.error?.code ?? json?.error ?? `http_${response.status}`; const message = json?.error?.message ?? json?.message ?? null; - const detail = typeof message === 'string' && message.trim() ? `: ${message.trim()}` : ''; - throw new OwnerAgentError('setup_failed', `Setup plan request failed (${code})${detail}.`); + const detail = typeof message === "string" && message.trim() ? `: ${message.trim()}` : ""; + throw new OwnerAgentError("setup_failed", `Setup plan request failed (${code})${detail}.`); } - if (!json || typeof json !== 'object') { - throw new OwnerAgentError('setup_failed', `Response from ${url} was not a valid setup plan.`); + if (!json || typeof json !== "object") { + throw new OwnerAgentError("setup_failed", `Response from ${url} was not a valid setup plan.`); } return json; } @@ -109,13 +109,13 @@ export async function requestConnectionSetupPlan({ fetchFn, record, connectorId, export async function requestConnectorTemplates({ fetchFn, record }) { const token = getOwnerAgentAccessToken(record); if (!token) { - throw new OwnerAgentError('credential_invalid', 'Stored credential is missing an access token.'); + throw new OwnerAgentError("credential_invalid", "Stored credential is missing an access token."); } - const resource = typeof record?.resource === 'string' ? record.resource.replace(/\/$/, '') : null; + const resource = typeof record?.resource === "string" ? record.resource.replace(/\/$/, "") : null; if (!resource) { throw new OwnerAgentError( - 'credential_invalid', - 'Stored credential has no resource origin; re-run `pdpp owner-agent onboard`.' + "credential_invalid", + "Stored credential has no resource origin; re-run `pdpp owner-agent onboard`." ); } const url = `${resource}/v1/owner/connector-templates`; @@ -123,16 +123,19 @@ export async function requestConnectorTemplates({ fetchFn, record }) { try { response = await fetchFn(url, { headers: { - Accept: 'application/json', + Accept: "application/json", Authorization: `Bearer ${token}`, }, }); } catch (error) { - throw new OwnerAgentError('templates_failed', `Failed to request connector templates from ${url}: ${error.message}.`); + throw new OwnerAgentError( + "templates_failed", + `Failed to request connector templates from ${url}: ${error.message}.` + ); } if (response.status === 401 || response.status === 403) { throw new OwnerAgentError( - 'setup_unauthorized', + "setup_unauthorized", `Owner-agent connector discovery is not authorized (HTTP ${response.status}). The credential may be revoked or inactive; run \`pdpp owner-agent status\`.`, 4 ); @@ -145,7 +148,7 @@ export async function requestConnectorTemplates({ fetchFn, record }) { } if (!response.ok) { const code = json?.error?.code ?? json?.error ?? `http_${response.status}`; - throw new OwnerAgentError('templates_failed', `Connector template request failed (${code}).`); + throw new OwnerAgentError("templates_failed", `Connector template request failed (${code}).`); } return Array.isArray(json?.data) ? json.data : []; } @@ -155,16 +158,16 @@ export async function requestConnectorTemplates({ fetchFn, record }) { // next action. function describeSetupStatus(supportState, kind) { switch (supportState) { - case 'supported': - return { label: 'supported', summary: 'This setup path can start now.' }; - case 'proof_gated': - return { label: 'proof-gated', summary: 'A setup path exists, but support is not flipped without live proof.' }; - case 'needs_deployment_config': - return { label: 'deployment-blocked', summary: 'An instance-level prerequisite is missing.' }; - case 'unsupported': - return { label: 'unsupported', summary: 'No reference setup path for this connector yet.' }; + case "supported": + return { label: "supported", summary: "This setup path can start now." }; + case "proof_gated": + return { label: "proof-gated", summary: "A setup path exists, but support is not flipped without live proof." }; + case "needs_deployment_config": + return { label: "deployment-blocked", summary: "An instance-level prerequisite is missing." }; + case "unsupported": + return { label: "unsupported", summary: "No reference setup path for this connector yet." }; default: - return { label: kind ? `next-step:${kind}` : 'unknown', summary: 'See the next-step details below.' }; + return { label: kind ? `next-step:${kind}` : "unknown", summary: "See the next-step details below." }; } } @@ -173,14 +176,14 @@ function describeSetupStatus(supportState, kind) { // rendered generically. No field here is a provider/credential secret: the // route never returns those. const NEXT_STEP_DETAIL_KEYS = [ - ['enroll_endpoint', 'enroll endpoint'], - ['enrollment_code', 'enrollment code'], - ['local_binding_name', 'local binding name'], - ['capture_endpoint', 'capture endpoint'], - ['upload_endpoint', 'upload endpoint'], - ['authorization_url', 'authorization url'], - ['runbook_path', 'runbook'], - ['expires_at', 'expires'], + ["enroll_endpoint", "enroll endpoint"], + ["enrollment_code", "enrollment code"], + ["local_binding_name", "local binding name"], + ["capture_endpoint", "capture endpoint"], + ["upload_endpoint", "upload endpoint"], + ["authorization_url", "authorization url"], + ["runbook_path", "runbook"], + ["expires_at", "expires"], ]; /** @@ -190,90 +193,91 @@ const NEXT_STEP_DETAIL_KEYS = [ * next step with its reason, and any owner-openable next-step details. */ export function formatConnectionSetupPlan(plan) { - const connectorKey = plan?.connector_key ?? plan?.connector_id ?? '(unknown connector)'; - const connectorModality = plan?.connector_modality ?? 'unknown'; - const setupModality = plan?.setup_modality ?? 'unknown'; + const connectorKey = plan?.connector_key ?? plan?.connector_id ?? "(unknown connector)"; + const connectorModality = plan?.connector_modality ?? "unknown"; + const setupModality = plan?.setup_modality ?? "unknown"; const supportState = plan?.support_state ?? null; - const nextStep = plan?.next_step && typeof plan.next_step === 'object' ? plan.next_step : {}; - const kind = typeof nextStep.kind === 'string' ? nextStep.kind : null; + const nextStep = plan?.next_step && typeof plan.next_step === "object" ? plan.next_step : {}; + const kind = typeof nextStep.kind === "string" ? nextStep.kind : null; const status = describeSetupStatus(supportState, kind); - const deployment = plan?.deployment_readiness && typeof plan.deployment_readiness === 'object' - ? plan.deployment_readiness - : null; + const deployment = + plan?.deployment_readiness && typeof plan.deployment_readiness === "object" ? plan.deployment_readiness : null; const lines = []; lines.push(`Connection setup plan for ${connectorKey} (non-secret):`); lines.push(` status: ${status.label} — ${status.summary}`); lines.push(` setup modality: ${setupModality}`); lines.push(` connector modality: ${connectorModality}`); - if (typeof plan?.validation === 'string') { + if (typeof plan?.validation === "string") { lines.push( ` credential validation: ${plan.validation}${ - plan.validation === 'synchronous' - ? ' (the credential is checked and the account identity echoed before storing)' - : ' (the connection activates when the first sync accepts records)' + plan.validation === "synchronous" + ? " (the credential is checked and the account identity echoed before storing)" + : " (the connection activates when the first sync accepts records)" }` ); } - lines.push(` connection active: ${plan?.connection_active === true ? 'yes' : 'no (materializes when the owner step completes)'}`); - if (deployment?.state && deployment.state !== 'not_applicable') { + lines.push( + ` connection active: ${plan?.connection_active === true ? "yes" : "no (materializes when the owner step completes)"}` + ); + if (deployment?.state && deployment.state !== "not_applicable") { lines.push(` deployment readiness: ${deployment.state}`); } - lines.push(''); - lines.push(` Next step: ${kind ?? '(none)'}`); - if (typeof nextStep.reason === 'string' && nextStep.reason.trim()) { + lines.push(""); + lines.push(` Next step: ${kind ?? "(none)"}`); + if (typeof nextStep.reason === "string" && nextStep.reason.trim()) { lines.push(` ${nextStep.reason.trim()}`); } const detailLines = []; for (const [key, label] of NEXT_STEP_DETAIL_KEYS) { const value = nextStep[key]; - if (typeof value === 'string' && value.trim()) { + if (typeof value === "string" && value.trim()) { detailLines.push(` ${label}: ${value.trim()}`); } } if (detailLines.length > 0) { - lines.push(''); - lines.push(' Details:'); + lines.push(""); + lines.push(" Details:"); lines.push(...detailLines); } const blockers = Array.isArray(deployment?.blockers) ? deployment.blockers : []; if (blockers.length > 0) { - lines.push(''); - lines.push(' Deployment blockers:'); + lines.push(""); + lines.push(" Deployment blockers:"); for (const blocker of blockers) { - const label = typeof blocker?.label === 'string' && blocker.label.trim() ? blocker.label.trim() : blocker?.key; - if (typeof label === 'string' && label.trim()) { - lines.push(` ${label.trim()}${blocker?.secret === true ? ' (secret)' : ''}`); + const label = typeof blocker?.label === "string" && blocker.label.trim() ? blocker.label.trim() : blocker?.key; + if (typeof label === "string" && label.trim()) { + lines.push(` ${label.trim()}${blocker?.secret === true ? " (secret)" : ""}`); } } } - lines.push(''); - lines.push(' Note: provider secrets are captured only through owner-mediated flows;'); - lines.push(' this plan and the owner bearer are never exposed to /mcp or grant-scoped reads.'); - return `${lines.join('\n')}\n`; + lines.push(""); + lines.push(" Note: provider secrets are captured only through owner-mediated flows;"); + lines.push(" this plan and the owner bearer are never exposed to /mcp or grant-scoped reads."); + return `${lines.join("\n")}\n`; } function templateSupportStatus(template) { - const setupPlan = template?.setup_plan && typeof template.setup_plan === 'object' ? template.setup_plan : null; - if (typeof setupPlan?.support_state === 'string') { + const setupPlan = template?.setup_plan && typeof template.setup_plan === "object" ? template.setup_plan : null; + if (typeof setupPlan?.support_state === "string") { return setupPlan.support_state; } const actions = Array.isArray(template?.supported_actions) ? template.supported_actions : []; - const initiate = actions.find((action) => action?.family === 'initiate_connection'); - return typeof initiate?.status === 'string' ? initiate.status : 'unknown'; + const initiate = actions.find((action) => action?.family === "initiate_connection"); + return typeof initiate?.status === "string" ? initiate.status : "unknown"; } function templateNextStep(template) { - const setupPlan = template?.setup_plan && typeof template.setup_plan === 'object' ? template.setup_plan : null; - if (typeof setupPlan?.next_step_kind === 'string') { + const setupPlan = template?.setup_plan && typeof template.setup_plan === "object" ? template.setup_plan : null; + if (typeof setupPlan?.next_step_kind === "string") { return setupPlan.next_step_kind; } const actions = Array.isArray(template?.supported_actions) ? template.supported_actions : []; - const initiate = actions.find((action) => action?.family === 'initiate_connection'); - return typeof initiate?.reason === 'string' ? initiate.reason : 'unknown'; + const initiate = actions.find((action) => action?.family === "initiate_connection"); + return typeof initiate?.reason === "string" ? initiate.reason : "unknown"; } function templateMatches(template, query) { @@ -289,16 +293,16 @@ function templateMatches(template, query) { templateSupportStatus(template), templateNextStep(template), ] - .filter((value) => typeof value === 'string') - .join(' ') + .filter((value) => typeof value === "string") + .join(" ") .toLowerCase() .includes(needle); } function sortedTemplates(templates) { return [...templates].sort((left, right) => { - const leftName = left?.display_name ?? left?.connector_key ?? ''; - const rightName = right?.display_name ?? right?.connector_key ?? ''; + const leftName = left?.display_name ?? left?.connector_key ?? ""; + const rightName = right?.display_name ?? right?.connector_key ?? ""; return String(leftName).localeCompare(String(rightName)); }); } @@ -307,16 +311,18 @@ export function findConnectorTemplates(templates, query) { return sortedTemplates(templates).filter((template) => templateMatches(template, query)); } -export function formatConnectorTemplates(templates, { query = '' } = {}) { +export function formatConnectorTemplates(templates, { query = "" } = {}) { const rows = findConnectorTemplates(templates, query); const lines = []; - lines.push(query ? `Connector setup catalog matching "${query}" (non-secret):` : 'Connector setup catalog (non-secret):'); + lines.push( + query ? `Connector setup catalog matching "${query}" (non-secret):` : "Connector setup catalog (non-secret):" + ); if (rows.length === 0) { - lines.push(' (no matching connector templates)'); - return `${lines.join('\n')}\n`; + lines.push(" (no matching connector templates)"); + return `${lines.join("\n")}\n`; } for (const template of rows) { - const connectorKey = template?.connector_key ?? template?.connector_id ?? '(unknown)'; + const connectorKey = template?.connector_key ?? template?.connector_id ?? "(unknown)"; const displayName = template?.display_name ?? connectorKey; const status = templateSupportStatus(template); const next = templateNextStep(template); @@ -325,25 +331,25 @@ export function formatConnectorTemplates(templates, { query = '' } = {}) { lines.push(` next: ${next}`); lines.push(` explain: pdpp owner-agent connectors explain ${connectorKey}`); } - lines.push(''); - lines.push(' Start setup with: pdpp owner-agent setup [--display-name ]'); - lines.push(' Note: setup may mint short-lived enrollment material; explain/list/search are read-only.'); - return `${lines.join('\n')}\n`; + lines.push(""); + lines.push(" Start setup with: pdpp owner-agent setup [--display-name ]"); + lines.push(" Note: setup may mint short-lived enrollment material; explain/list/search are read-only."); + return `${lines.join("\n")}\n`; } export function formatConnectorTemplateExplain(template) { if (!template) { - throw new OwnerAgentError('not_found', 'No matching connector template found.', 1); + throw new OwnerAgentError("not_found", "No matching connector template found.", 1); } - const connectorKey = template?.connector_key ?? template?.connector_id ?? '(unknown)'; + const connectorKey = template?.connector_key ?? template?.connector_id ?? "(unknown)"; const displayName = template?.display_name ?? connectorKey; - const setupPlan = template?.setup_plan && typeof template.setup_plan === 'object' ? template.setup_plan : {}; + const setupPlan = template?.setup_plan && typeof template.setup_plan === "object" ? template.setup_plan : {}; const lines = []; lines.push(`Connector setup preview for ${displayName} (${connectorKey}) — non-secret, read-only:`); lines.push(` status: ${templateSupportStatus(template)}`); - lines.push(` connector modality: ${template?.connector_modality ?? 'unknown'}`); - lines.push(` setup modality: ${setupPlan.setup_modality ?? 'unknown'}`); - if (typeof setupPlan.validation === 'string') { + lines.push(` connector modality: ${template?.connector_modality ?? "unknown"}`); + lines.push(` setup modality: ${setupPlan.setup_modality ?? "unknown"}`); + if (typeof setupPlan.validation === "string") { lines.push(` credential validation: ${setupPlan.validation}`); } lines.push(` next step: ${templateNextStep(template)}`); @@ -357,26 +363,29 @@ export function formatConnectorTemplateExplain(template) { ? setupPlan.deployment_readiness.blockers : []; if (blockers.length > 0) { - lines.push(' deployment blockers:'); + lines.push(" deployment blockers:"); for (const blocker of blockers) { - const label = typeof blocker?.label === 'string' ? blocker.label : blocker?.key; + const label = typeof blocker?.label === "string" ? blocker.label : blocker?.key; if (label) { - lines.push(` ${label}${blocker?.secret === true ? ' (secret)' : ''}`); + lines.push(` ${label}${blocker?.secret === true ? " (secret)" : ""}`); } } } const connections = Array.isArray(template?.connections) ? template.connections : []; lines.push(` existing connections: ${connections.length}`); for (const connection of connections) { - const connectionId = connection?.connection_id ?? connection?.connector_instance_id ?? '(no connection_id)'; - const display = typeof connection?.display_name === 'string' && connection.display_name.trim() - ? ` "${connection.display_name.trim()}"` - : ''; + const connectionId = connection?.connection_id ?? connection?.connector_instance_id ?? "(no connection_id)"; + const display = + typeof connection?.display_name === "string" && connection.display_name.trim() + ? ` "${connection.display_name.trim()}"` + : ""; lines.push(` - ${connectionId}${display}`); } - lines.push(''); + lines.push(""); lines.push(` Start setup: pdpp owner-agent setup ${connectorKey} --display-name ""`); - lines.push(' Repeat setup with another display name to add another account when this connector supports it.'); - lines.push(' This preview did not mint enrollment codes, provider secrets, owner cookies, or grant-scoped MCP bearers.'); - return `${lines.join('\n')}\n`; + lines.push(" Repeat setup with another display name to add another account when this connector supports it."); + lines.push( + " This preview did not mint enrollment codes, provider secrets, owner cookies, or grant-scoped MCP bearers." + ); + return `${lines.join("\n")}\n`; } diff --git a/packages/cli/src/package-info.js b/packages/cli/src/package-info.js index 75264925a..9127cfbe2 100644 --- a/packages/cli/src/package-info.js +++ b/packages/cli/src/package-info.js @@ -1,16 +1,16 @@ // Copyright The PDP-Connect Contributors // SPDX-License-Identifier: Apache-2.0 -export const PDPP_CLI_PACKAGE_NAME = '@pdpp/cli'; -export const PDPP_CLI_BIN_NAME = 'pdpp'; +export const PDPP_CLI_PACKAGE_NAME = "@pdpp/cli"; +export const PDPP_CLI_BIN_NAME = "pdpp"; // Single release channel: the published package rides npm's default `latest` // dist-tag, so the advertised specifier is the plain package name. -export const PDPP_CLI_VERSION_POLICY = 'latest'; +export const PDPP_CLI_VERSION_POLICY = "latest"; export const PDPP_CLI_PACKAGE_SPECIFIER = PDPP_CLI_PACKAGE_NAME; -export const PDPP_CLI_DEFAULT_CLIENT_ID = 'pdpp_cli'; -export const PDPP_CLI_NO_OWNER_TOKEN_POLICY = 'owner_browser_approval_required'; +export const PDPP_CLI_DEFAULT_CLIENT_ID = "pdpp_cli"; +export const PDPP_CLI_NO_OWNER_TOKEN_POLICY = "owner_browser_approval_required"; -export function createPdppCliCommand(providerUrl = '') { +export function createPdppCliCommand(providerUrl = "") { return `npx -y ${PDPP_CLI_PACKAGE_SPECIFIER} connect ${providerUrl}`; } diff --git a/packages/cli/src/read/commands.js b/packages/cli/src/read/commands.js index 8e6453dec..23d213cfd 100644 --- a/packages/cli/src/read/commands.js +++ b/packages/cli/src/read/commands.js @@ -1,14 +1,14 @@ // Copyright The PDP-Connect Contributors // SPDX-License-Identifier: Apache-2.0 -import { ConnectError, normalizeProviderUrl, readStoredCredential } from '../connect/flow.js'; -import { parseArgs, requirePositional } from '../ref/args.js'; -import { PdppHttpError, PdppUsageError } from '../ref/errors.js'; -import { resolveFormat, writeData, writeEnvelopeWarnings } from '../ref/output.js'; +import { ConnectError, normalizeProviderUrl, readStoredCredential } from "../connect/flow.js"; +import { parseArgs, requirePositional } from "../ref/args.js"; +import { PdppHttpError, PdppUsageError } from "../ref/errors.js"; +import { resolveFormat, writeData, writeEnvelopeWarnings } from "../ref/output.js"; -const COMMANDS = new Set(['schema', 'streams', 'query-records', 'fetch', 'field-window', 'search', 'aggregate']); +const COMMANDS = new Set(["schema", "streams", "query-records", "fetch", "field-window", "search", "aggregate"]); -export function readHelp(binName = 'pdpp') { +export function readHelp(binName = "pdpp") { return `Grant-scoped reads (uses pdpp connect/token cache, never owner credentials): ${binName} read schema [--view compact] [--stream ] [--connection-id ] [--cache-root ] [--format json|table] ${binName} read streams [--connection-id ] [--cache-root ] [--format json|table] @@ -24,7 +24,7 @@ export async function runRead(argv, io = {}, fetchImpl = globalThis.fetch) { const err = io.stderr || process.stderr; const [command, ...rest] = argv; - if (!command || command === '--help' || command === '-h' || command === 'help') { + if (!command || command === "--help" || command === "-h" || command === "help") { out.write(`${readHelp()}\n`); return 0; } @@ -34,11 +34,11 @@ export async function runRead(argv, io = {}, fetchImpl = globalThis.fetch) { } const { flags, positionals } = parseArgs(rest); - const providerUrl = requirePositional(positionals, 0, 'provider-url'); + const providerUrl = requirePositional(positionals, 0, "provider-url"); let credential; let normalizedProviderUrl; try { - const stored = await readStoredCredential(providerUrl, { cacheRoot: flags['cache-root'] }); + const stored = await readStoredCredential(providerUrl, { cacheRoot: flags["cache-root"] }); credential = stored.credential; normalizedProviderUrl = stored.providerUrl; } catch (error) { @@ -50,7 +50,7 @@ export async function runRead(argv, io = {}, fetchImpl = globalThis.fetch) { const request = buildReadRequest(command, positionals.slice(1), flags, normalizedProviderUrl); const body = await fetchReadJson(request, credential.access_token, fetchImpl); - writeData(projectOutput(body, flags), resolveFormat(flags, 'json', 'json'), out); + writeData(projectOutput(body, flags), resolveFormat(flags, "json", "json"), out); writeEnvelopeWarnings(body, err); return 0; } @@ -59,72 +59,68 @@ export function buildReadRequest(command, positionals, flags, providerUrl) { const origin = normalizeProviderUrl(providerUrl); if (!origin) throw new PdppUsageError(`Invalid provider URL: ${providerUrl}`); - if (command === 'schema') { + if (command === "schema") { return { - method: 'GET', - url: buildUrl(origin, '/v1/schema', pickQuery(flags, ['connector-id', 'connection-id', 'stream', 'view'])), + method: "GET", + url: buildUrl(origin, "/v1/schema", pickQuery(flags, ["connector-id", "connection-id", "stream", "view"])), }; } - if (command === 'streams') { + if (command === "streams") { return { - method: 'GET', - url: buildUrl(origin, '/v1/streams', pickQuery(flags, ['connection-id', 'connector-instance-id'])), + method: "GET", + url: buildUrl(origin, "/v1/streams", pickQuery(flags, ["connection-id", "connector-instance-id"])), }; } - if (command === 'query-records') { - const stream = requirePositional(positionals, 0, 'stream'); + if (command === "query-records") { + const stream = requirePositional(positionals, 0, "stream"); const query = { ...pickQuery(flags, [ - 'connection-id', - 'connector-instance-id', - 'cursor', - 'limit', - 'order', - 'sort', - 'count', - 'changes-since', + "connection-id", + "connector-instance-id", + "cursor", + "limit", + "order", + "sort", + "count", + "changes-since", ]), - ...csvQuery(flags, 'fields'), + ...csvQuery(flags, "fields"), ...jsonFilterQuery(flags), }; - return { method: 'GET', url: buildUrl(origin, `/v1/streams/${encodeURIComponent(stream)}/records`, query) }; + return { method: "GET", url: buildUrl(origin, `/v1/streams/${encodeURIComponent(stream)}/records`, query) }; } - if (command === 'fetch') { - const stream = requirePositional(positionals, 0, 'stream'); - const recordId = requirePositional(positionals, 1, 'record-id'); + if (command === "fetch") { + const stream = requirePositional(positionals, 0, "stream"); + const recordId = requirePositional(positionals, 1, "record-id"); const query = { - ...pickQuery(flags, ['connection-id', 'connector-instance-id']), - ...csvQuery(flags, 'fields'), + ...pickQuery(flags, ["connection-id", "connector-instance-id"]), + ...csvQuery(flags, "fields"), }; return { - method: 'GET', - url: buildUrl( - origin, - `/v1/streams/${encodeURIComponent(stream)}/records/${encodeURIComponent(recordId)}`, - query, - ), + method: "GET", + url: buildUrl(origin, `/v1/streams/${encodeURIComponent(stream)}/records/${encodeURIComponent(recordId)}`, query), }; } - if (command === 'field-window') { - const stream = requirePositional(positionals, 0, 'stream'); - const recordId = requirePositional(positionals, 1, 'record-id'); - if (!flags.field) throw new PdppUsageError('Missing required flag: --field'); + if (command === "field-window") { + const stream = requirePositional(positionals, 0, "stream"); + const recordId = requirePositional(positionals, 1, "record-id"); + if (!flags.field) throw new PdppUsageError("Missing required flag: --field"); const query = pickQuery(flags, [ - 'connection-id', - 'field', - 'cursor', - 'offset-chars', - 'limit-chars', - 'q', - 'before-chars', - 'after-chars', + "connection-id", + "field", + "cursor", + "offset-chars", + "limit-chars", + "q", + "before-chars", + "after-chars", ]); return { - method: 'GET', + method: "GET", url: buildUrl( origin, `/v1/streams/${encodeURIComponent(stream)}/records/${encodeURIComponent(recordId)}/field-window`, @@ -133,36 +129,36 @@ export function buildReadRequest(command, positionals, flags, providerUrl) { }; } - if (command === 'search') { - const queryText = requirePositional(positionals, 0, 'query'); + if (command === "search") { + const queryText = requirePositional(positionals, 0, "query"); const mode = flags.mode ? String(flags.mode) : undefined; - const path = mode === 'semantic' ? '/v1/search/semantic' : mode === 'hybrid' ? '/v1/search/hybrid' : '/v1/search'; + const path = mode === "semantic" ? "/v1/search/semantic" : mode === "hybrid" ? "/v1/search/hybrid" : "/v1/search"; const query = { q: queryText, - ...pickQuery(flags, ['connection-id', 'connector-instance-id', 'cursor', 'limit']), - ...csvQuery(flags, 'streams'), + ...pickQuery(flags, ["connection-id", "connector-instance-id", "cursor", "limit"]), + ...csvQuery(flags, "streams"), }; - return { method: 'GET', url: buildUrl(origin, path, query) }; + return { method: "GET", url: buildUrl(origin, path, query) }; } - if (command === 'aggregate') { - const stream = requirePositional(positionals, 0, 'stream'); - if (!flags.metric) throw new PdppUsageError('Missing required flag: --metric'); - if (flags['group-by'] && flags['group-by-time']) { - throw new PdppUsageError('Use only one of --group-by or --group-by-time.'); + if (command === "aggregate") { + const stream = requirePositional(positionals, 0, "stream"); + if (!flags.metric) throw new PdppUsageError("Missing required flag: --metric"); + if (flags["group-by"] && flags["group-by-time"]) { + throw new PdppUsageError("Use only one of --group-by or --group-by-time."); } const query = pickQuery(flags, [ - 'connection-id', - 'connector-instance-id', - 'field', - 'granularity', - 'limit', - 'metric', - 'time-zone', + "connection-id", + "connector-instance-id", + "field", + "granularity", + "limit", + "metric", + "time-zone", ]); - if (flags['group-by']) query.group_by = flags['group-by']; - if (flags['group-by-time']) query.group_by_time = flags['group-by-time']; - return { method: 'GET', url: buildUrl(origin, `/v1/streams/${encodeURIComponent(stream)}/aggregate`, query) }; + if (flags["group-by"]) query.group_by = flags["group-by"]; + if (flags["group-by-time"]) query.group_by_time = flags["group-by-time"]; + return { method: "GET", url: buildUrl(origin, `/v1/streams/${encodeURIComponent(stream)}/aggregate`, query) }; } throw new PdppUsageError(`Unsupported read command: ${command}`); @@ -174,7 +170,7 @@ async function fetchReadJson(request, token, fetchImpl) { resp = await fetchImpl(request.url, { method: request.method, headers: { - Accept: 'application/json', + Accept: "application/json", Authorization: `Bearer ${token}`, }, }); @@ -182,7 +178,7 @@ async function fetchReadJson(request, token, fetchImpl) { throw new PdppUsageError(`Network request failed: ${error.message}`); } - const text = typeof resp.text === 'function' ? await resp.text() : ''; + const text = typeof resp.text === "function" ? await resp.text() : ""; let parsed = null; if (text) { try { @@ -197,9 +193,9 @@ async function fetchReadJson(request, token, fetchImpl) { parsed?.error_description || parsed?.error?.message || parsed?.message || - `HTTP ${resp.status} ${resp.statusText || ''}`.trim(); + `HTTP ${resp.status} ${resp.statusText || ""}`.trim(); throw new PdppHttpError(String(message), resp.status, parsed, { - request_id: resp.headers?.get?.('x-request-id') ?? null, + request_id: resp.headers?.get?.("x-request-id") ?? null, }); } @@ -215,7 +211,7 @@ function buildUrl(origin, path, query = {}) { } function appendQuery(url, key, value) { - if (value === undefined || value === null || value === '') return; + if (value === undefined || value === null || value === "") return; if (Array.isArray(value)) { for (const entry of value) appendQuery(url, key, entry); return; @@ -228,38 +224,43 @@ function pickQuery(flags, names) { for (const name of names) { const value = flags[name]; if (value === undefined || value === true) continue; - query[name.replaceAll('-', '_')] = value; + query[name.replaceAll("-", "_")] = value; } return query; } function csvQuery(flags, name) { const raw = flags[name]; - if (typeof raw !== 'string' || raw.trim() === '') return {}; - return { [name]: raw.split(',').map((entry) => entry.trim()).filter(Boolean) }; + if (typeof raw !== "string" || raw.trim() === "") return {}; + return { + [name]: raw + .split(",") + .map((entry) => entry.trim()) + .filter(Boolean), + }; } function jsonFilterQuery(flags) { - if (flags.filter !== undefined && flags['filter-json'] !== undefined) { - throw new PdppUsageError('Use only one of --filter or --filter-json.'); + if (flags.filter !== undefined && flags["filter-json"] !== undefined) { + throw new PdppUsageError("Use only one of --filter or --filter-json."); } - if (typeof flags.filter === 'string') { + if (typeof flags.filter === "string") { return { filter: flags.filter }; } - if (flags['filter-json'] === undefined) return {}; + if (flags["filter-json"] === undefined) return {}; let parsed; try { - parsed = JSON.parse(flags['filter-json']); + parsed = JSON.parse(flags["filter-json"]); } catch (error) { throw new PdppUsageError(`--filter-json must be valid JSON: ${error.message}`); } - if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { - throw new PdppUsageError('--filter-json must be a JSON object.'); + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { + throw new PdppUsageError("--filter-json must be a JSON object."); } const query = {}; for (const [field, value] of Object.entries(parsed)) { - if (value && typeof value === 'object' && !Array.isArray(value)) { + if (value && typeof value === "object" && !Array.isArray(value)) { for (const [op, opValue] of Object.entries(value)) { query[`filter[${field}][${op}]`] = opValue; } @@ -272,7 +273,7 @@ function jsonFilterQuery(flags) { function projectOutput(body, flags) { if (!flags.data) return body; - if (body && typeof body === 'object' && Array.isArray(body.data)) return body.data; - if (body && typeof body === 'object' && Array.isArray(body.records)) return body.records; + if (body && typeof body === "object" && Array.isArray(body.data)) return body.data; + if (body && typeof body === "object" && Array.isArray(body.records)) return body.records; return body; } diff --git a/packages/cli/src/ref/args.js b/packages/cli/src/ref/args.js index 25b8403bc..4983b5641 100644 --- a/packages/cli/src/ref/args.js +++ b/packages/cli/src/ref/args.js @@ -1,7 +1,7 @@ // Copyright The PDP-Connect Contributors // SPDX-License-Identifier: Apache-2.0 -import { PdppUsageError } from './errors.js'; +import { PdppUsageError } from "./errors.js"; export function parseArgs(argv) { const flags = {}; @@ -9,18 +9,18 @@ export function parseArgs(argv) { for (let i = 0; i < argv.length; i += 1) { const arg = argv[i]; - if (arg === '--') { + if (arg === "--") { positionals.push(...argv.slice(i + 1)); break; } - if (!arg.startsWith('--')) { + if (!arg.startsWith("--")) { positionals.push(arg); continue; } - const [rawKey, inlineValue] = arg.slice(2).split('=', 2); + const [rawKey, inlineValue] = arg.slice(2).split("=", 2); if (!rawKey) { - throw new PdppUsageError('Invalid empty flag'); + throw new PdppUsageError("Invalid empty flag"); } if (inlineValue !== undefined) { @@ -29,7 +29,7 @@ export function parseArgs(argv) { } const next = argv[i + 1]; - if (next && !next.startsWith('--')) { + if (next && !next.startsWith("--")) { flags[rawKey] = next; i += 1; continue; diff --git a/packages/cli/src/ref/auth.js b/packages/cli/src/ref/auth.js index 21fb90f6a..16e9a6119 100644 --- a/packages/cli/src/ref/auth.js +++ b/packages/cli/src/ref/auth.js @@ -18,28 +18,28 @@ // otherwise surface as a confusing 401/404 — the "401 = wrong auth, 404 = wrong // path" trap from the live route map. -import { PdppUsageError } from './errors.js'; -import { ownerSessionHeaders } from './fetch.js'; +import { PdppUsageError } from "./errors.js"; +import { ownerSessionHeaders } from "./fetch.js"; -export const AUTH_COOKIE = 'cookie'; -export const AUTH_BEARER = 'bearer'; +export const AUTH_COOKIE = "cookie"; +export const AUTH_BEARER = "bearer"; // Infer the auth mode a path expects from its prefix. Returns 'cookie', // 'bearer', or null when the prefix is not an owner surface we recognize. export function inferAuthMode(path) { const p = normalizePath(path); - if (p.startsWith('/v1/owner/') || p === '/v1/owner') { + if (p.startsWith("/v1/owner/") || p === "/v1/owner") { return AUTH_BEARER; } - if (p.startsWith('/_ref/') || p === '/_ref') { + if (p.startsWith("/_ref/") || p === "/_ref") { return AUTH_COOKIE; } return null; } function normalizePath(path) { - if (typeof path !== 'string' || !path) { - return ''; + if (typeof path !== "string" || !path) { + return ""; } // Strip a query/hash and a leading origin if the caller passed a full URL. let p = path; @@ -50,11 +50,11 @@ function normalizePath(path) { } catch { // fall through; treat as a path } - const q = p.indexOf('?'); + const q = p.indexOf("?"); if (q !== -1) p = p.slice(0, q); - const h = p.indexOf('#'); + const h = p.indexOf("#"); if (h !== -1) p = p.slice(0, h); - if (!p.startsWith('/')) p = `/${p}`; + if (!p.startsWith("/")) p = `/${p}`; return p; } @@ -65,7 +65,7 @@ function normalizePath(path) { export function resolveAuthMode(path, override) { const inferred = inferAuthMode(path); - if (override !== undefined && override !== null && override !== '') { + if (override !== undefined && override !== null && override !== "") { const chosen = String(override).toLowerCase(); if (chosen !== AUTH_COOKIE && chosen !== AUTH_BEARER) { throw new PdppUsageError(`Invalid --auth value: ${override}. Use "cookie" or "bearer".`); @@ -79,21 +79,21 @@ export function resolveAuthMode(path, override) { if (!inferred) { throw new PdppUsageError( `Cannot infer owner auth mode for path "${path}". ` + - 'Owner routes are /_ref/* (cookie) or /v1/owner/* (bearer). ' + - 'Pass --auth cookie|bearer to call a non-standard path explicitly.' + "Owner routes are /_ref/* (cookie) or /v1/owner/* (bearer). " + + "Pass --auth cookie|bearer to call a non-standard path explicitly." ); } return inferred; } function mismatchMessage(path, inferred, chosen) { - const surface = inferred === AUTH_COOKIE ? '/_ref/*' : '/v1/owner/*'; - const correct = inferred === AUTH_COOKIE ? 'cookie' : 'bearer'; + const surface = inferred === AUTH_COOKIE ? "/_ref/*" : "/v1/owner/*"; + const correct = inferred === AUTH_COOKIE ? "cookie" : "bearer"; return ( `Auth mismatch: ${path} is a ${surface} route and uses ${correct} auth, ` + `but --auth ${chosen} was given. ` + - '/_ref/* uses the owner session cookie; /v1/owner/* uses the owner bearer. ' + - 'Pointing the wrong auth at a route returns a confusing 401/404. ' + + "/_ref/* uses the owner session cookie; /v1/owner/* uses the owner bearer. " + + "Pointing the wrong auth at a route returns a confusing 401/404. " + `Drop --auth (it is inferred) or use --auth ${correct}.` ); } @@ -107,14 +107,14 @@ function mismatchMessage(path, inferred, chosen) { export async function buildAuthHeaders({ mode, referenceUrl, flags, io, env = process.env }) { if (mode === AUTH_COOKIE) { const headers = ownerSessionHeaders({ - ownerSession: flags['owner-session'] || '', + ownerSession: flags["owner-session"] || "", referenceUrl, - cacheRoot: flags['cache-root'], + cacheRoot: flags["cache-root"], }); if (!headers.Cookie) { throw new PdppUsageError( - 'No owner session available. Run `pdpp ref login ` first, ' + - 'pass --owner-session , or set PDPP_OWNER_SESSION_COOKIE.' + "No owner session available. Run `pdpp ref login ` first, " + + "pass --owner-session , or set PDPP_OWNER_SESSION_COOKIE." ); } return headers; @@ -124,9 +124,9 @@ export async function buildAuthHeaders({ mode, referenceUrl, flags, io, env = pr const token = await resolveOwnerToken(flags, io, env); if (!token) { throw new PdppUsageError( - 'No owner bearer available for a /v1/owner/* call. ' + - 'Pipe it via `--owner-token-stdin` or set PDPP_OWNER_TOKEN. ' + - 'The token is never accepted on the command line.' + "No owner bearer available for a /v1/owner/* call. " + + "Pipe it via `--owner-token-stdin` or set PDPP_OWNER_TOKEN. " + + "The token is never accepted on the command line." ); } return { Authorization: `Bearer ${token}` }; @@ -136,11 +136,11 @@ export async function buildAuthHeaders({ mode, referenceUrl, flags, io, env = pr } async function resolveOwnerToken(flags, io, env) { - if (flags['owner-token-stdin']) { + if (flags["owner-token-stdin"]) { return readFirstLine((io && io.stdin) || process.stdin); } const fromEnv = env.PDPP_OWNER_TOKEN; - if (typeof fromEnv === 'string' && fromEnv.length > 0) { + if (typeof fromEnv === "string" && fromEnv.length > 0) { return fromEnv.trim(); } return null; @@ -148,35 +148,35 @@ async function resolveOwnerToken(flags, io, env) { function readFirstLine(stream) { return new Promise((resolve, reject) => { - if (!stream || typeof stream.on !== 'function') { - resolve(''); + if (!stream || typeof stream.on !== "function") { + resolve(""); return; } - let buf = ''; - stream.setEncoding?.('utf8'); + let buf = ""; + stream.setEncoding?.("utf8"); const onData = (chunk) => { buf += chunk; - const nl = buf.indexOf('\n'); + const nl = buf.indexOf("\n"); if (nl !== -1) { cleanup(); - resolve(buf.slice(0, nl).replace(/\r$/, '')); + resolve(buf.slice(0, nl).replace(/\r$/, "")); } }; const onEnd = () => { cleanup(); - resolve(buf.replace(/\r?\n$/, '')); + resolve(buf.replace(/\r?\n$/, "")); }; const onError = (e) => { cleanup(); reject(e); }; function cleanup() { - stream.off?.('data', onData); - stream.off?.('end', onEnd); - stream.off?.('error', onError); + stream.off?.("data", onData); + stream.off?.("end", onEnd); + stream.off?.("error", onError); } - stream.on('data', onData); - stream.on('end', onEnd); - stream.on('error', onError); + stream.on("data", onData); + stream.on("end", onEnd); + stream.on("error", onError); }); } diff --git a/packages/cli/src/ref/commands/call.js b/packages/cli/src/ref/commands/call.js index 32f0ca60e..eb6c241f8 100644 --- a/packages/cli/src/ref/commands/call.js +++ b/packages/cli/src/ref/commands/call.js @@ -24,27 +24,25 @@ // [--cache-root ] [--format json|table] // [--status-only] -import { parseArgs, requirePositional } from '../args.js'; -import { resolveAuthMode, buildAuthHeaders } from '../auth.js'; -import { PdppUsageError, PdppHttpError } from '../errors.js'; -import { resolveReferenceUrl } from '../fetch.js'; -import { resolveFormat, writeData, writeEnvelopeWarnings } from '../output.js'; +import { parseArgs, requirePositional } from "../args.js"; +import { resolveAuthMode, buildAuthHeaders } from "../auth.js"; +import { PdppUsageError, PdppHttpError } from "../errors.js"; +import { resolveReferenceUrl } from "../fetch.js"; +import { resolveFormat, writeData, writeEnvelopeWarnings } from "../output.js"; -const METHODS_WITH_BODY = new Set(['POST', 'PUT', 'PATCH', 'DELETE']); -const KNOWN_METHODS = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD']); +const METHODS_WITH_BODY = new Set(["POST", "PUT", "PATCH", "DELETE"]); +const KNOWN_METHODS = new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"]); export async function runRefCall(argv, io = {}, fetchImpl = globalThis.fetch) { const out = io.stdout || process.stdout; const err = io.stderr || process.stderr; const { flags, positionals } = parseArgs(argv); - const method = requirePositional(positionals, 0, 'method').toUpperCase(); + const method = requirePositional(positionals, 0, "method").toUpperCase(); if (!KNOWN_METHODS.has(method)) { - throw new PdppUsageError( - `Unsupported method: ${method}. Use one of ${[...KNOWN_METHODS].join(', ')}.` - ); + throw new PdppUsageError(`Unsupported method: ${method}. Use one of ${[...KNOWN_METHODS].join(", ")}.`); } - const path = requirePositional(positionals, 1, 'path'); + const path = requirePositional(positionals, 1, "path"); const referenceUrl = resolveReferenceUrl(flags); const mode = resolveAuthMode(path, flags.auth); @@ -52,14 +50,14 @@ export async function runRefCall(argv, io = {}, fetchImpl = globalThis.fetch) { const body = await resolveBody(flags, io, method); - const headers = { Accept: 'application/json', ...authHeaders }; - const init = { method, headers, redirect: 'manual' }; + const headers = { Accept: "application/json", ...authHeaders }; + const init = { method, headers, redirect: "manual" }; if (body !== undefined) { - headers['Content-Type'] = 'application/json'; + headers["Content-Type"] = "application/json"; init.body = body; } - const url = `${referenceUrl}${path.startsWith('/') ? path : `/${path}`}`; + const url = `${referenceUrl}${path.startsWith("/") ? path : `/${path}`}`; let resp; try { @@ -70,9 +68,9 @@ export async function runRefCall(argv, io = {}, fetchImpl = globalThis.fetch) { // Always surface the status line on stderr so machine-readable stdout stays // clean. The URL is intentionally the path only — never the cookie/bearer. - err.write(`${method} ${path} → ${resp.status} ${resp.statusText || ''}`.trimEnd() + '\n'); + err.write(`${method} ${path} → ${resp.status} ${resp.statusText || ""}`.trimEnd() + "\n"); - if (flags['status-only']) { + if (flags["status-only"]) { return statusExitCode(resp.status); } @@ -88,15 +86,15 @@ export async function runRefCall(argv, io = {}, fetchImpl = globalThis.fetch) { if (resp.status >= 400) { const message = - (parsed && typeof parsed === 'object' && (parsed.error_description || parsed.error?.message || parsed.message)) || - `HTTP ${resp.status} ${resp.statusText || ''}`.trim(); + (parsed && typeof parsed === "object" && (parsed.error_description || parsed.error?.message || parsed.message)) || + `HTTP ${resp.status} ${resp.statusText || ""}`.trim(); throw new PdppHttpError(String(message), resp.status, parsed); } - if (parsed !== null && parsed !== '') { - const format = resolveFormat(flags, 'json', 'json'); + if (parsed !== null && parsed !== "") { + const format = resolveFormat(flags, "json", "json"); writeData(parsed, format, out); - if (parsed && typeof parsed === 'object') { + if (parsed && typeof parsed === "object") { writeEnvelopeWarnings(parsed, err); } } @@ -104,10 +102,10 @@ export async function runRefCall(argv, io = {}, fetchImpl = globalThis.fetch) { } async function resolveBody(flags, io, method) { - const hasInline = typeof flags.data === 'string'; - const hasStdin = Boolean(flags['data-stdin']); + const hasInline = typeof flags.data === "string"; + const hasStdin = Boolean(flags["data-stdin"]); if (hasInline && hasStdin) { - throw new PdppUsageError('Use only one of --data or --data-stdin, not both.'); + throw new PdppUsageError("Use only one of --data or --data-stdin, not both."); } let raw; @@ -148,24 +146,24 @@ function statusExitCode(status) { } async function readBody(resp) { - if (typeof resp.text === 'function') { + if (typeof resp.text === "function") { return await resp.text(); } - return ''; + return ""; } function readAll(stream) { return new Promise((resolve, reject) => { - if (!stream || typeof stream.on !== 'function') { - resolve(''); + if (!stream || typeof stream.on !== "function") { + resolve(""); return; } - let buf = ''; - stream.setEncoding?.('utf8'); - stream.on('data', (chunk) => { + let buf = ""; + stream.setEncoding?.("utf8"); + stream.on("data", (chunk) => { buf += chunk; }); - stream.on('end', () => resolve(buf)); - stream.on('error', reject); + stream.on("end", () => resolve(buf)); + stream.on("error", reject); }); } diff --git a/packages/cli/src/ref/commands/connectors.js b/packages/cli/src/ref/commands/connectors.js index a1d842bb6..f63ab31ee 100644 --- a/packages/cli/src/ref/commands/connectors.js +++ b/packages/cli/src/ref/commands/connectors.js @@ -1,10 +1,10 @@ // Copyright The PDP-Connect Contributors // SPDX-License-Identifier: Apache-2.0 -import { parseArgs, requirePositional } from '../args.js'; -import { PdppUsageError } from '../errors.js'; -import { fetchJson, ownerSessionHeaders, resolveReferenceUrl } from '../fetch.js'; -import { resolveFormat, writeData, writeEnvelopeWarnings } from '../output.js'; +import { parseArgs, requirePositional } from "../args.js"; +import { PdppUsageError } from "../errors.js"; +import { fetchJson, ownerSessionHeaders, resolveReferenceUrl } from "../fetch.js"; +import { resolveFormat, writeData, writeEnvelopeWarnings } from "../output.js"; // Operator-facing summary projection. Mirrors the evidence the dashboard renders // in `apps/console/src/app/(console)/lib/ref-client.ts` (RefConnectorSummary + @@ -27,11 +27,11 @@ function projectSummaryRow(summary) { connection_id: summary?.connection_id ?? null, connector_id: summary?.connector_id ?? null, display_name: summary?.display_name ?? null, - state: health.state ?? 'unknown', - coverage: axes.coverage ?? 'unknown', - freshness: axes.freshness ?? 'unknown', - attention: axes.attention ?? 'none', - outbox: axes.outbox ?? 'unknown', + state: health.state ?? "unknown", + coverage: axes.coverage ?? "unknown", + freshness: axes.freshness ?? "unknown", + attention: axes.attention ?? "none", + outbox: axes.outbox ?? "unknown", syncing: badges.syncing === true, stale: badges.stale === true, reason_code: health.reason_code ?? null, @@ -52,7 +52,7 @@ function projectSummaryRow(summary) { primary_action_cta: primaryAction?.cta ?? null, primary_action_satisfied_when: primaryAction?.satisfied_when?.kind ?? null, primary_action_terminal: primaryAction?.terminal ?? null, - next_action_source: nextAction?.source ?? 'none', + next_action_source: nextAction?.source ?? "none", next_action_reason: nextAction?.reason_code ?? null, next_action_owner_action: nextAction?.owner_action ?? null, next_action_target: nextAction?.action_target ?? null, @@ -90,53 +90,53 @@ export async function runRefConnectors(argv, io = {}, fetchImpl = globalThis.fet const out = io.stdout || process.stdout; const err = io.stderr || process.stderr; - if (subcommand === 'list') { + if (subcommand === "list") { const asUrl = resolveReferenceUrl(flags); - const ownerSession = flags['owner-session'] || ''; - const cacheRoot = flags['cache-root']; + const ownerSession = flags["owner-session"] || ""; + const cacheRoot = flags["cache-root"]; const { body } = await fetchJson( `${asUrl}/_ref/connectors`, { headers: { ...ownerSessionHeaders({ ownerSession, referenceUrl: asUrl, cacheRoot }) } }, fetchImpl ); - const format = resolveFormat(flags, 'table', 'json'); - const verbose = flags.verbose === true || flags.verbose === 'true'; + const format = resolveFormat(flags, "table", "json"); + const verbose = flags.verbose === true || flags.verbose === "true"; if (verbose) { - writeData(format === 'table' ? (body.data || []) : body, format, out); + writeData(format === "table" ? body.data || [] : body, format, out); writeEnvelopeWarnings(body, err); return 0; } const rows = Array.isArray(body?.data) ? body.data.map(projectSummaryRow) : []; - writeData(format === 'table' ? rows.map(projectSummaryTableRow) : { object: 'list', data: rows }, format, out); + writeData(format === "table" ? rows.map(projectSummaryTableRow) : { object: "list", data: rows }, format, out); writeEnvelopeWarnings(body, err); return 0; } - if (subcommand === 'show') { - const connectorId = requirePositional(positionals, 0, 'connector-id'); + if (subcommand === "show") { + const connectorId = requirePositional(positionals, 0, "connector-id"); const asUrl = resolveReferenceUrl(flags); - const ownerSession = flags['owner-session'] || ''; - const cacheRoot = flags['cache-root']; + const ownerSession = flags["owner-session"] || ""; + const cacheRoot = flags["cache-root"]; const { body } = await fetchJson( `${asUrl}/_ref/connectors/${encodeURIComponent(connectorId)}`, { headers: { ...ownerSessionHeaders({ ownerSession, referenceUrl: asUrl, cacheRoot }) } }, fetchImpl ); - const format = resolveFormat(flags, 'table', 'json'); - const verbose = flags.verbose === true || flags.verbose === 'true'; + const format = resolveFormat(flags, "table", "json"); + const verbose = flags.verbose === true || flags.verbose === "true"; if (verbose) { writeData(body, format, out); writeEnvelopeWarnings(body, err); return 0; } const row = projectSummaryRow(body); - writeData(format === 'table' ? [projectSummaryTableRow(row)] : row, format, out); + writeData(format === "table" ? [projectSummaryTableRow(row)] : row, format, out); writeEnvelopeWarnings(body, err); return 0; } throw new PdppUsageError( - 'Usage: pdpp ref connectors > [--as-url ] [--owner-session ] [--format json|table] [--verbose]' + "Usage: pdpp ref connectors > [--as-url ] [--owner-session ] [--format json|table] [--verbose]" ); } diff --git a/packages/cli/src/ref/commands/event-subscriptions.js b/packages/cli/src/ref/commands/event-subscriptions.js index 22b825d7b..c857e8ef1 100644 --- a/packages/cli/src/ref/commands/event-subscriptions.js +++ b/packages/cli/src/ref/commands/event-subscriptions.js @@ -11,25 +11,26 @@ * reference-implementation-architecture/spec.md */ -import { parseArgs, requirePositional } from '../args.js'; -import { PdppCliError, PdppUsageError } from '../errors.js'; -import { fetchJson, ownerSessionHeaders, resolveReferenceUrl } from '../fetch.js'; -import { resolveFormat, writeData, writeEnvelopeWarnings } from '../output.js'; +import { parseArgs, requirePositional } from "../args.js"; +import { PdppCliError, PdppUsageError } from "../errors.js"; +import { fetchJson, ownerSessionHeaders, resolveReferenceUrl } from "../fetch.js"; +import { resolveFormat, writeData, writeEnvelopeWarnings } from "../output.js"; function projectListRow(row) { return { subscription_id: row.subscription_id, - authority: row.authority_kind || 'client_grant', + authority: row.authority_kind || "client_grant", client_id: row.client_id, - grant_id: row.grant_id || '', + grant_id: row.grant_id || "", status: row.status, callback_host: row.callback_host, - disabled_reason: row.disabled_reason ?? '', + disabled_reason: row.disabled_reason ?? "", pending: row.pending_queue_count ?? 0, final_failures: row.final_failure_count ?? 0, - last_attempt_at: row.last_attempted_at ?? '', - last_attempt_ok: row.last_attempt_ok === null || row.last_attempt_ok === undefined ? '' : row.last_attempt_ok ? 'ok' : 'fail', - last_attempt_code: row.last_attempt_status_code ?? '', + last_attempt_at: row.last_attempted_at ?? "", + last_attempt_ok: + row.last_attempt_ok === null || row.last_attempt_ok === undefined ? "" : row.last_attempt_ok ? "ok" : "fail", + last_attempt_code: row.last_attempt_status_code ?? "", updated_at: row.updated_at, }; } @@ -37,20 +38,20 @@ function projectListRow(row) { function projectDetail(detail) { return { subscription_id: detail.subscription_id, - authority: detail.authority_kind || 'client_grant', + authority: detail.authority_kind || "client_grant", client_id: detail.client_id, - grant_id: detail.grant_id || '', + grant_id: detail.grant_id || "", status: detail.status, - disabled_reason: detail.disabled_reason ?? '', + disabled_reason: detail.disabled_reason ?? "", callback_url: detail.callback_url, created_at: detail.created_at, updated_at: detail.updated_at, - disabled_at: detail.disabled_at ?? '', + disabled_at: detail.disabled_at ?? "", pending_queue_count: detail.pending_queue_count, final_failure_count: detail.final_failure_count, - last_attempt_at: detail.last_attempted_at ?? '', - last_attempt_ok: detail.last_attempt_ok ?? '', - last_attempt_code: detail.last_attempt_status_code ?? '', + last_attempt_at: detail.last_attempted_at ?? "", + last_attempt_ok: detail.last_attempt_ok ?? "", + last_attempt_code: detail.last_attempt_status_code ?? "", recent_attempts: (detail.recent_attempts || []).length, }; } @@ -61,22 +62,38 @@ async function readConfirmation(io) { return null; } return new Promise((resolve) => { - let buf = ''; + let buf = ""; const onData = (chunk) => { buf += chunk; - const newlineIdx = buf.indexOf('\n'); + const newlineIdx = buf.indexOf("\n"); if (newlineIdx !== -1) { - stdin.removeListener('data', onData); - if (typeof stdin.setRawMode === 'function') { - try { stdin.setRawMode(false); } catch { /* ignore */ } + stdin.removeListener("data", onData); + if (typeof stdin.setRawMode === "function") { + try { + stdin.setRawMode(false); + } catch { + /* ignore */ + } + } + try { + stdin.pause(); + } catch { + /* ignore */ } - try { stdin.pause(); } catch { /* ignore */ } resolve(buf.slice(0, newlineIdx).trim()); } }; - try { stdin.setEncoding('utf8'); } catch { /* ignore */ } - stdin.on('data', onData); - try { stdin.resume(); } catch { /* ignore */ } + try { + stdin.setEncoding("utf8"); + } catch { + /* ignore */ + } + stdin.on("data", onData); + try { + stdin.resume(); + } catch { + /* ignore */ + } }); } @@ -86,25 +103,25 @@ export async function runRefEventSubscriptions(argv, io = {}, fetchImpl = global const out = io.stdout || process.stdout; const err = io.stderr || process.stderr; - if (subcommand === 'list') { + if (subcommand === "list") { const asUrl = resolveReferenceUrl(flags); - const ownerSession = flags['owner-session'] || ''; - const cacheRoot = flags['cache-root']; + const ownerSession = flags["owner-session"] || ""; + const cacheRoot = flags["cache-root"]; const query = new URLSearchParams(); - if (flags['client-id']) query.set('client_id', String(flags['client-id'])); - if (flags['grant-id']) query.set('grant_id', String(flags['grant-id'])); - if (flags.status) query.set('status', String(flags.status)); + if (flags["client-id"]) query.set("client_id", String(flags["client-id"])); + if (flags["grant-id"]) query.set("grant_id", String(flags["grant-id"])); + if (flags.status) query.set("status", String(flags.status)); const queryString = query.toString(); - const url = `${asUrl}/_ref/event-subscriptions${queryString ? `?${queryString}` : ''}`; + const url = `${asUrl}/_ref/event-subscriptions${queryString ? `?${queryString}` : ""}`; const { body } = await fetchJson( url, { headers: { ...ownerSessionHeaders({ ownerSession, referenceUrl: asUrl, cacheRoot }) } }, - fetchImpl, + fetchImpl ); - const format = resolveFormat(flags, 'table', 'json'); + const format = resolveFormat(flags, "table", "json"); const rows = body?.data || []; - if (format === 'table') { - writeData(rows.map(projectListRow), 'table', out); + if (format === "table") { + writeData(rows.map(projectListRow), "table", out); } else { writeData(body, format, out); } @@ -112,19 +129,19 @@ export async function runRefEventSubscriptions(argv, io = {}, fetchImpl = global return 0; } - if (subcommand === 'show') { - const subscriptionId = requirePositional(positionals, 0, 'subscription-id'); + if (subcommand === "show") { + const subscriptionId = requirePositional(positionals, 0, "subscription-id"); const asUrl = resolveReferenceUrl(flags); - const ownerSession = flags['owner-session'] || ''; - const cacheRoot = flags['cache-root']; + const ownerSession = flags["owner-session"] || ""; + const cacheRoot = flags["cache-root"]; const { body } = await fetchJson( `${asUrl}/_ref/event-subscriptions/${encodeURIComponent(subscriptionId)}`, { headers: { ...ownerSessionHeaders({ ownerSession, referenceUrl: asUrl, cacheRoot }) } }, - fetchImpl, + fetchImpl ); - const format = resolveFormat(flags, 'table', 'json'); - if (format === 'table') { - writeData(projectDetail(body), 'table', out); + const format = resolveFormat(flags, "table", "json"); + if (format === "table") { + writeData(projectDetail(body), "table", out); } else { writeData(body, format, out); } @@ -132,29 +149,31 @@ export async function runRefEventSubscriptions(argv, io = {}, fetchImpl = global return 0; } - if (subcommand === 'disable') { - const subscriptionId = requirePositional(positionals, 0, 'subscription-id'); + if (subcommand === "disable") { + const subscriptionId = requirePositional(positionals, 0, "subscription-id"); const asUrl = resolveReferenceUrl(flags); - const ownerSession = flags['owner-session'] || ''; - const cacheRoot = flags['cache-root']; - const reason = typeof flags.reason === 'string' ? flags.reason : null; - const explicitYes = flags.yes === true || flags.yes === 'true'; + const ownerSession = flags["owner-session"] || ""; + const cacheRoot = flags["cache-root"]; + const reason = typeof flags.reason === "string" ? flags.reason : null; + const explicitYes = flags.yes === true || flags.yes === "true"; if (!explicitYes) { const headers = { ...ownerSessionHeaders({ ownerSession, referenceUrl: asUrl, cacheRoot }) }; const { body } = await fetchJson( `${asUrl}/_ref/event-subscriptions/${encodeURIComponent(subscriptionId)}`, { headers }, - fetchImpl, + fetchImpl + ); + const authority = body.authority_kind || "client_grant"; + const grant = body.grant_id || "none"; + err.write( + `Subscription ${body.subscription_id} (authority=${authority}, client=${body.client_id}, grant=${grant}, status=${body.status})\n` ); - const authority = body.authority_kind || 'client_grant'; - const grant = body.grant_id || 'none'; - err.write(`Subscription ${body.subscription_id} (authority=${authority}, client=${body.client_id}, grant=${grant}, status=${body.status})\n`); err.write(`Callback: ${body.callback_url}\n`); err.write(`Disable subscription? Type 'yes' to confirm: `); const answer = await readConfirmation(io); - if (!answer || answer.toLowerCase() !== 'yes') { - err.write('Aborted.\n'); + if (!answer || answer.toLowerCase() !== "yes") { + err.write("Aborted.\n"); return 1; } } @@ -163,18 +182,18 @@ export async function runRefEventSubscriptions(argv, io = {}, fetchImpl = global const { body: detail } = await fetchJson( `${asUrl}/_ref/event-subscriptions/${encodeURIComponent(subscriptionId)}/disable`, { - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", ...ownerSessionHeaders({ ownerSession, referenceUrl: asUrl, cacheRoot }), }, body: JSON.stringify(body), }, - fetchImpl, + fetchImpl ); - const format = resolveFormat(flags, 'table', 'json'); - if (format === 'table') { - writeData(projectDetail(detail), 'table', out); + const format = resolveFormat(flags, "table", "json"); + if (format === "table") { + writeData(projectDetail(detail), "table", out); } else { writeData(detail, format, out); } @@ -182,10 +201,10 @@ export async function runRefEventSubscriptions(argv, io = {}, fetchImpl = global } throw new PdppUsageError( - 'Usage:\n' + - ' pdpp ref event-subscriptions list [--client-id ] [--grant-id ] [--status ] [--as-url ] [--owner-session ] [--format json|table]\n' + - ' pdpp ref event-subscriptions show [--as-url ] [--owner-session ] [--format json|table]\n' + - ' pdpp ref event-subscriptions disable [--reason ] [--yes] [--as-url ] [--owner-session ]' + "Usage:\n" + + " pdpp ref event-subscriptions list [--client-id ] [--grant-id ] [--status ] [--as-url ] [--owner-session ] [--format json|table]\n" + + " pdpp ref event-subscriptions show [--as-url ] [--owner-session ] [--format json|table]\n" + + " pdpp ref event-subscriptions disable [--reason ] [--yes] [--as-url ] [--owner-session ]" ); } diff --git a/packages/cli/src/ref/commands/grant.js b/packages/cli/src/ref/commands/grant.js index e50d867d2..8cb063f11 100644 --- a/packages/cli/src/ref/commands/grant.js +++ b/packages/cli/src/ref/commands/grant.js @@ -1,10 +1,10 @@ // Copyright The PDP-Connect Contributors // SPDX-License-Identifier: Apache-2.0 -import { parseArgs, requirePositional } from '../args.js'; -import { PdppUsageError } from '../errors.js'; -import { fetchJson, ownerSessionHeaders, resolveReferenceUrl } from '../fetch.js'; -import { resolveFormat, writeData, writeEnvelopeWarnings } from '../output.js'; +import { parseArgs, requirePositional } from "../args.js"; +import { PdppUsageError } from "../errors.js"; +import { fetchJson, ownerSessionHeaders, resolveReferenceUrl } from "../fetch.js"; +import { resolveFormat, writeData, writeEnvelopeWarnings } from "../output.js"; export async function runRefGrant(argv, io = {}, fetchImpl = globalThis.fetch) { const [subcommand, ...rest] = argv; @@ -12,23 +12,23 @@ export async function runRefGrant(argv, io = {}, fetchImpl = globalThis.fetch) { const out = io.stdout || process.stdout; const err = io.stderr || process.stderr; - if (subcommand === 'timeline') { - const grantId = requirePositional(positionals, 0, 'grant-id'); + if (subcommand === "timeline") { + const grantId = requirePositional(positionals, 0, "grant-id"); const asUrl = resolveReferenceUrl(flags); - const ownerSession = flags['owner-session'] || ''; - const cacheRoot = flags['cache-root']; + const ownerSession = flags["owner-session"] || ""; + const cacheRoot = flags["cache-root"]; const { body } = await fetchJson( `${asUrl}/_ref/grants/${encodeURIComponent(grantId)}/timeline`, { headers: { ...ownerSessionHeaders({ ownerSession, referenceUrl: asUrl, cacheRoot }) } }, fetchImpl ); - const format = resolveFormat(flags, 'table', 'json'); - writeData(format === 'table' ? (body.data || []) : body, format, out); + const format = resolveFormat(flags, "table", "json"); + writeData(format === "table" ? body.data || [] : body, format, out); writeEnvelopeWarnings(body, err); return 0; } throw new PdppUsageError( - 'Usage: pdpp ref grant timeline [--as-url ] [--owner-session ] [--format json|table]' + "Usage: pdpp ref grant timeline [--as-url ] [--owner-session ] [--format json|table]" ); } diff --git a/packages/cli/src/ref/commands/login.js b/packages/cli/src/ref/commands/login.js index 5b90c64f3..fb4173ddd 100644 --- a/packages/cli/src/ref/commands/login.js +++ b/packages/cli/src/ref/commands/login.js @@ -1,14 +1,10 @@ // Copyright The PDP-Connect Contributors // SPDX-License-Identifier: Apache-2.0 -import { parseArgs } from '../args.js'; -import { PdppCliError, PdppUsageError } from '../errors.js'; -import { OWNER_SESSION_COOKIE_NAME } from '../fetch.js'; -import { - extractCookieFromSetCookie, - getOwnerSessionPaths, - writeOwnerSession, -} from '../session.js'; +import { parseArgs } from "../args.js"; +import { PdppCliError, PdppUsageError } from "../errors.js"; +import { OWNER_SESSION_COOKIE_NAME } from "../fetch.js"; +import { extractCookieFromSetCookie, getOwnerSessionPaths, writeOwnerSession } from "../session.js"; // Owner-session login UX for `pdpp ref ...`. // @@ -30,17 +26,15 @@ export async function runRefLogin(argv, io = {}, fetchImpl = globalThis.fetch) { const { flags, positionals } = parseArgs(argv); const referenceUrlRaw = positionals[0]; if (!referenceUrlRaw) { - throw new PdppUsageError( - 'Usage: pdpp ref login [--password-stdin] [--cache-root ]' - ); + throw new PdppUsageError("Usage: pdpp ref login [--password-stdin] [--cache-root ]"); } - const referenceUrl = referenceUrlRaw.replace(/\/$/, ''); + const referenceUrl = referenceUrlRaw.replace(/\/$/, ""); const password = await resolvePassword(flags, io); if (!password) { throw new PdppUsageError( - 'Owner password required. Pipe it via `--password-stdin` or set PDPP_OWNER_PASSWORD. ' + - 'The password is never accepted on the command line.' + "Owner password required. Pipe it via `--password-stdin` or set PDPP_OWNER_PASSWORD. " + + "The password is never accepted on the command line." ); } @@ -48,13 +42,13 @@ export async function runRefLogin(argv, io = {}, fetchImpl = globalThis.fetch) { let resp; try { resp = await fetchImpl(loginUrl, { - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', + "Content-Type": "application/json", + Accept: "application/json", }, body: JSON.stringify({ password }), - redirect: 'manual', + redirect: "manual", }); } catch (e) { throw new PdppCliError(`Network request to ${loginUrl} failed: ${e.message}`); @@ -66,16 +60,10 @@ export async function runRefLogin(argv, io = {}, fetchImpl = globalThis.fetch) { // 200-399 means the login was rejected. if (status >= 400) { if (status === 401) { - throw new PdppCliError( - 'Owner login rejected: incorrect password (HTTP 401).', - 3 - ); + throw new PdppCliError("Owner login rejected: incorrect password (HTTP 401).", 3); } if (status === 403) { - throw new PdppCliError( - 'Owner login rejected: CSRF/policy failure (HTTP 403).', - 4 - ); + throw new PdppCliError("Owner login rejected: CSRF/policy failure (HTTP 403).", 4); } if (status === 404) { throw new PdppCliError( @@ -90,12 +78,12 @@ export async function runRefLogin(argv, io = {}, fetchImpl = globalThis.fetch) { const cookieValue = extractCookieFromSetCookie(setCookie, OWNER_SESSION_COOKIE_NAME); if (!cookieValue) { throw new PdppCliError( - 'Owner login succeeded but no owner-session cookie was returned. ' + - 'Confirm that placeholder owner auth is enabled on the reference server.' + "Owner login succeeded but no owner-session cookie was returned. " + + "Confirm that placeholder owner auth is enabled on the reference server." ); } - const cacheRoot = flags['cache-root'] || '.pdpp'; + const cacheRoot = flags["cache-root"] || ".pdpp"; const file = writeOwnerSession({ referenceUrl, cookie: `${OWNER_SESSION_COOKIE_NAME}=${cookieValue}`, @@ -109,11 +97,11 @@ export async function runRefLogin(argv, io = {}, fetchImpl = globalThis.fetch) { } async function resolvePassword(flags, io) { - if (flags['password-stdin']) { + if (flags["password-stdin"]) { return readFirstLine(io.stdin || process.stdin); } const fromEnv = process.env.PDPP_OWNER_PASSWORD; - if (typeof fromEnv === 'string' && fromEnv.length > 0) { + if (typeof fromEnv === "string" && fromEnv.length > 0) { return fromEnv; } return null; @@ -121,50 +109,50 @@ async function resolvePassword(flags, io) { function readFirstLine(stream) { return new Promise((resolve, reject) => { - let buf = ''; - if (!stream || typeof stream.on !== 'function') { - resolve(''); + let buf = ""; + if (!stream || typeof stream.on !== "function") { + resolve(""); return; } - stream.setEncoding?.('utf8'); + stream.setEncoding?.("utf8"); const onData = (chunk) => { buf += chunk; - const nl = buf.indexOf('\n'); + const nl = buf.indexOf("\n"); if (nl !== -1) { cleanup(); - resolve(buf.slice(0, nl).replace(/\r$/, '')); + resolve(buf.slice(0, nl).replace(/\r$/, "")); } }; const onEnd = () => { cleanup(); - resolve(buf.replace(/\r?\n$/, '')); + resolve(buf.replace(/\r?\n$/, "")); }; const onError = (e) => { cleanup(); reject(e); }; function cleanup() { - stream.off?.('data', onData); - stream.off?.('end', onEnd); - stream.off?.('error', onError); + stream.off?.("data", onData); + stream.off?.("end", onEnd); + stream.off?.("error", onError); } - stream.on('data', onData); - stream.on('end', onEnd); - stream.on('error', onError); + stream.on("data", onData); + stream.on("end", onEnd); + stream.on("error", onError); }); } function readSetCookie(resp) { const headers = resp.headers; if (!headers) return null; - if (typeof headers.getSetCookie === 'function') { + if (typeof headers.getSetCookie === "function") { const arr = headers.getSetCookie(); if (arr && arr.length) return arr; } - if (typeof headers.get === 'function') { - return headers.get('set-cookie') || headers.get('Set-Cookie'); + if (typeof headers.get === "function") { + return headers.get("set-cookie") || headers.get("Set-Cookie"); } - return headers['set-cookie'] || headers['Set-Cookie'] || null; + return headers["set-cookie"] || headers["Set-Cookie"] || null; } export { getOwnerSessionPaths }; diff --git a/packages/cli/src/ref/commands/run.js b/packages/cli/src/ref/commands/run.js index e8da7f0e2..336b09aaa 100644 --- a/packages/cli/src/ref/commands/run.js +++ b/packages/cli/src/ref/commands/run.js @@ -1,10 +1,10 @@ // Copyright The PDP-Connect Contributors // SPDX-License-Identifier: Apache-2.0 -import { parseArgs, requirePositional } from '../args.js'; -import { PdppUsageError } from '../errors.js'; -import { fetchJson, ownerSessionHeaders, resolveReferenceUrl } from '../fetch.js'; -import { resolveFormat, writeData, writeEnvelopeWarnings } from '../output.js'; +import { parseArgs, requirePositional } from "../args.js"; +import { PdppUsageError } from "../errors.js"; +import { fetchJson, ownerSessionHeaders, resolveReferenceUrl } from "../fetch.js"; +import { resolveFormat, writeData, writeEnvelopeWarnings } from "../output.js"; export async function runRefRun(argv, io = {}, fetchImpl = globalThis.fetch) { const [subcommand, ...rest] = argv; @@ -12,23 +12,23 @@ export async function runRefRun(argv, io = {}, fetchImpl = globalThis.fetch) { const out = io.stdout || process.stdout; const err = io.stderr || process.stderr; - if (subcommand === 'timeline') { - const runId = requirePositional(positionals, 0, 'run-id'); + if (subcommand === "timeline") { + const runId = requirePositional(positionals, 0, "run-id"); const asUrl = resolveReferenceUrl(flags); - const ownerSession = flags['owner-session'] || ''; - const cacheRoot = flags['cache-root']; + const ownerSession = flags["owner-session"] || ""; + const cacheRoot = flags["cache-root"]; const { body } = await fetchJson( `${asUrl}/_ref/runs/${encodeURIComponent(runId)}/timeline`, { headers: { ...ownerSessionHeaders({ ownerSession, referenceUrl: asUrl, cacheRoot }) } }, fetchImpl ); - const format = resolveFormat(flags, 'table', 'json'); - writeData(format === 'table' ? (body.data || []) : body, format, out); + const format = resolveFormat(flags, "table", "json"); + writeData(format === "table" ? body.data || [] : body, format, out); writeEnvelopeWarnings(body, err); return 0; } throw new PdppUsageError( - 'Usage: pdpp ref run timeline [--as-url ] [--owner-session ] [--format json|table]' + "Usage: pdpp ref run timeline [--as-url ] [--owner-session ] [--format json|table]" ); } diff --git a/packages/cli/src/ref/commands/trace.js b/packages/cli/src/ref/commands/trace.js index e032e34a5..70476e88f 100644 --- a/packages/cli/src/ref/commands/trace.js +++ b/packages/cli/src/ref/commands/trace.js @@ -1,10 +1,10 @@ // Copyright The PDP-Connect Contributors // SPDX-License-Identifier: Apache-2.0 -import { parseArgs, requirePositional } from '../args.js'; -import { PdppUsageError } from '../errors.js'; -import { fetchJson, ownerSessionHeaders, resolveReferenceUrl } from '../fetch.js'; -import { resolveFormat, writeData, writeEnvelopeWarnings } from '../output.js'; +import { parseArgs, requirePositional } from "../args.js"; +import { PdppUsageError } from "../errors.js"; +import { fetchJson, ownerSessionHeaders, resolveReferenceUrl } from "../fetch.js"; +import { resolveFormat, writeData, writeEnvelopeWarnings } from "../output.js"; export async function runRefTrace(argv, io = {}, fetchImpl = globalThis.fetch) { const [subcommand, ...rest] = argv; @@ -12,23 +12,23 @@ export async function runRefTrace(argv, io = {}, fetchImpl = globalThis.fetch) { const out = io.stdout || process.stdout; const err = io.stderr || process.stderr; - if (subcommand === 'show') { - const traceId = requirePositional(positionals, 0, 'trace-id'); + if (subcommand === "show") { + const traceId = requirePositional(positionals, 0, "trace-id"); const asUrl = resolveReferenceUrl(flags); - const ownerSession = flags['owner-session'] || ''; - const cacheRoot = flags['cache-root']; + const ownerSession = flags["owner-session"] || ""; + const cacheRoot = flags["cache-root"]; const { body } = await fetchJson( `${asUrl}/_ref/traces/${encodeURIComponent(traceId)}`, { headers: { ...ownerSessionHeaders({ ownerSession, referenceUrl: asUrl, cacheRoot }) } }, fetchImpl ); - const format = resolveFormat(flags, 'table', 'json'); - writeData(format === 'table' ? (body.data || []) : body, format, out); + const format = resolveFormat(flags, "table", "json"); + writeData(format === "table" ? body.data || [] : body, format, out); writeEnvelopeWarnings(body, err); return 0; } throw new PdppUsageError( - 'Usage: pdpp ref trace show [--as-url ] [--owner-session ] [--format json|table]' + "Usage: pdpp ref trace show [--as-url ] [--owner-session ] [--format json|table]" ); } diff --git a/packages/cli/src/ref/errors.js b/packages/cli/src/ref/errors.js index f1540935a..57c66a747 100644 --- a/packages/cli/src/ref/errors.js +++ b/packages/cli/src/ref/errors.js @@ -4,7 +4,7 @@ export class PdppCliError extends Error { constructor(message, exitCode = 1, details = null) { super(message); - this.name = 'PdppCliError'; + this.name = "PdppCliError"; this.exitCode = exitCode; this.details = details; } @@ -13,7 +13,7 @@ export class PdppCliError extends Error { export class PdppUsageError extends PdppCliError { constructor(message, details = null) { super(message, 2, details); - this.name = 'PdppUsageError'; + this.name = "PdppUsageError"; } } @@ -24,7 +24,7 @@ export class PdppHttpError extends PdppCliError { body, ...(responseMetadata || {}), }); - this.name = 'PdppHttpError'; + this.name = "PdppHttpError"; this.status = status; this.body = body; this.requestId = responseMetadata?.request_id || null; diff --git a/packages/cli/src/ref/fetch.js b/packages/cli/src/ref/fetch.js index 5c0570776..d6bb1c67a 100644 --- a/packages/cli/src/ref/fetch.js +++ b/packages/cli/src/ref/fetch.js @@ -1,10 +1,10 @@ // Copyright The PDP-Connect Contributors // SPDX-License-Identifier: Apache-2.0 -import { PdppCliError, PdppHttpError } from './errors.js'; -import { readOwnerSession } from './session.js'; +import { PdppCliError, PdppHttpError } from "./errors.js"; +import { readOwnerSession } from "./session.js"; -export const OWNER_SESSION_COOKIE_NAME = 'pdpp_owner_session'; +export const OWNER_SESSION_COOKIE_NAME = "pdpp_owner_session"; export async function fetchJson(url, opts = {}, fetchImpl = globalThis.fetch) { let resp; @@ -26,10 +26,7 @@ export async function fetchJson(url, opts = {}, fetchImpl = globalThis.fetch) { if (!resp.ok) { const message = - body?.error_description || - body?.error?.message || - body?.message || - `HTTP ${resp.status} ${resp.statusText}`; + body?.error_description || body?.error?.message || body?.message || `HTTP ${resp.status} ${resp.statusText}`; throw new PdppHttpError(message, resp.status, body); } @@ -42,11 +39,9 @@ export async function fetchJson(url, opts = {}, fetchImpl = globalThis.fetch) { // 3. project-local cached session (when opts.referenceUrl is provided) // Returns headers object with Cookie set, or empty object if no session found. export function ownerSessionHeaders(opts = {}) { - const fromOpts = typeof opts.ownerSession === 'string' ? opts.ownerSession : ''; + const fromOpts = typeof opts.ownerSession === "string" ? opts.ownerSession : ""; const fromEnv = - typeof process.env.PDPP_OWNER_SESSION_COOKIE === 'string' - ? process.env.PDPP_OWNER_SESSION_COOKIE - : ''; + typeof process.env.PDPP_OWNER_SESSION_COOKIE === "string" ? process.env.PDPP_OWNER_SESSION_COOKIE : ""; let value = (fromOpts || fromEnv).trim(); @@ -59,20 +54,15 @@ export function ownerSessionHeaders(opts = {}) { } if (!value) return {}; - const cookie = value.includes('=') ? value : `${OWNER_SESSION_COOKIE_NAME}=${value}`; + const cookie = value.includes("=") ? value : `${OWNER_SESSION_COOKIE_NAME}=${value}`; return { Cookie: cookie }; } // Resolves the reference base URL from --as-url flag or PDPP_AS_URL / AS_URL env vars. export function resolveReferenceUrl(flags) { - const url = - flags['as-url'] || - process.env.PDPP_AS_URL || - process.env.AS_URL; + const url = flags["as-url"] || process.env.PDPP_AS_URL || process.env.AS_URL; if (!url) { - throw new PdppCliError( - 'Missing reference server URL. Provide --as-url or set PDPP_AS_URL.' - ); + throw new PdppCliError("Missing reference server URL. Provide --as-url or set PDPP_AS_URL."); } - return url.replace(/\/$/, ''); + return url.replace(/\/$/, ""); } diff --git a/packages/cli/src/ref/output.js b/packages/cli/src/ref/output.js index 9344f7b9e..554c9b48c 100644 --- a/packages/cli/src/ref/output.js +++ b/packages/cli/src/ref/output.js @@ -1,7 +1,7 @@ // Copyright The PDP-Connect Contributors // SPDX-License-Identifier: Apache-2.0 -export function resolveFormat(flags, defaultWhenTty = 'json', defaultWhenPipe = 'json') { +export function resolveFormat(flags, defaultWhenTty = "json", defaultWhenPipe = "json") { return flags.format || (process.stdout.isTTY ? defaultWhenTty : defaultWhenPipe); } @@ -15,15 +15,15 @@ export function resolveFormat(flags, defaultWhenTty = 'json', defaultWhenPipe = * the renderer prints nothing. */ export function extractEnvelopeWarnings(body) { - if (!body || typeof body !== 'object') { + if (!body || typeof body !== "object") { return []; } const meta = body.meta; - if (!meta || typeof meta !== 'object') { + if (!meta || typeof meta !== "object") { return []; } const warnings = Array.isArray(meta.warnings) ? meta.warnings : []; - return warnings.filter((w) => w && typeof w === 'object' && typeof w.code === 'string'); + return warnings.filter((w) => w && typeof w === "object" && typeof w.code === "string"); } /** @@ -45,17 +45,17 @@ export function writeEnvelopeWarnings(body, err = process.stderr) { if (warning.dropped_parameter) { parts.push(`(dropped: ${warning.dropped_parameter})`); } - err.write(`${parts.join(' — ')}\n`); + err.write(`${parts.join(" — ")}\n`); } } -export function writeData(data, format = 'json', out = process.stdout) { - if (format === 'json') { +export function writeData(data, format = "json", out = process.stdout) { + if (format === "json") { out.write(`${JSON.stringify(data, null, 2)}\n`); return; } - if (format === 'jsonl') { + if (format === "jsonl") { if (!Array.isArray(data)) { out.write(`${JSON.stringify(data)}\n`); return; @@ -66,7 +66,7 @@ export function writeData(data, format = 'json', out = process.stdout) { return; } - if (format === 'table') { + if (format === "table") { writeTable(data, out); return; } @@ -77,7 +77,7 @@ export function writeData(data, format = 'json', out = process.stdout) { function writeTable(data, out = process.stdout) { const rows = Array.isArray(data) ? data : [data]; if (!rows.length) { - out.write('(empty)\n'); + out.write("(empty)\n"); return; } @@ -92,17 +92,14 @@ function writeTable(data, out = process.stdout) { const widths = Object.fromEntries( columns.map((column) => [ column, - Math.max(column.length, ...normalized.map((row) => String(row[column] ?? '').length)), + Math.max(column.length, ...normalized.map((row) => String(row[column] ?? "").length)), ]) ); - const renderRow = (row) => - columns - .map((column) => String(row[column] ?? '').padEnd(widths[column])) - .join(' '); + const renderRow = (row) => columns.map((column) => String(row[column] ?? "").padEnd(widths[column])).join(" "); out.write(`${renderRow(Object.fromEntries(columns.map((column) => [column, column])))}\n`); - out.write(`${columns.map((column) => '-'.repeat(widths[column])).join(' ')}\n`); + out.write(`${columns.map((column) => "-".repeat(widths[column])).join(" ")}\n`); for (const row of normalized) { out.write(`${renderRow(row)}\n`); } @@ -112,11 +109,11 @@ function flattenRow(row) { const out = {}; for (const [key, value] of Object.entries(row)) { if (Array.isArray(value)) { - out[key] = value.join(', '); - } else if (value && typeof value === 'object') { + out[key] = value.join(", "); + } else if (value && typeof value === "object") { out[key] = JSON.stringify(value); } else { - out[key] = value ?? ''; + out[key] = value ?? ""; } } return out; diff --git a/packages/cli/src/ref/session.js b/packages/cli/src/ref/session.js index 9034ea39f..182a51472 100644 --- a/packages/cli/src/ref/session.js +++ b/packages/cli/src/ref/session.js @@ -1,16 +1,16 @@ // Copyright The PDP-Connect Contributors // SPDX-License-Identifier: Apache-2.0 -import { existsSync, mkdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from 'node:fs'; -import { dirname, join } from 'node:path'; +import { existsSync, mkdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; -import { writePdppSecretFile } from '../cache-layout.js'; -import { PdppCliError } from './errors.js'; +import { writePdppSecretFile } from "../cache-layout.js"; +import { PdppCliError } from "./errors.js"; -const SESSION_DIR_NAME = 'owner-sessions'; +const SESSION_DIR_NAME = "owner-sessions"; export function getOwnerSessionPaths(referenceUrl, opts = {}) { - const cacheRoot = opts.cacheRoot || '.pdpp'; + const cacheRoot = opts.cacheRoot || ".pdpp"; const dir = join(cacheRoot, SESSION_DIR_NAME); const file = join(dir, `${sessionCacheKey(referenceUrl)}.json`); return { cacheRoot, dir, file }; @@ -21,15 +21,15 @@ function sessionCacheKey(referenceUrl) { const u = new URL(referenceUrl); // Owner sessions are origin-scoped. Include protocol so an HTTPS login // cannot be silently reused for an HTTP reference URL on the same host. - return `${u.protocol}//${u.host}`.replace(/[^a-zA-Z0-9.-]/g, '_'); + return `${u.protocol}//${u.host}`.replace(/[^a-zA-Z0-9.-]/g, "_"); } catch { - return referenceUrl.replace(/[^a-zA-Z0-9.-]/g, '_'); + return referenceUrl.replace(/[^a-zA-Z0-9.-]/g, "_"); } } export function writeOwnerSession({ referenceUrl, cookie, cacheRoot } = {}) { - if (!referenceUrl) throw new PdppCliError('writeOwnerSession requires referenceUrl'); - if (!cookie) throw new PdppCliError('writeOwnerSession requires cookie'); + if (!referenceUrl) throw new PdppCliError("writeOwnerSession requires referenceUrl"); + if (!cookie) throw new PdppCliError("writeOwnerSession requires cookie"); const { file } = getOwnerSessionPaths(referenceUrl, { cacheRoot }); const payload = { @@ -38,7 +38,7 @@ export function writeOwnerSession({ referenceUrl, cookie, cacheRoot } = {}) { saved_at: new Date().toISOString(), }; writePdppSecretFile(file, JSON.stringify(payload, null, 2)); - ensureGitignore(cacheRoot || '.pdpp'); + ensureGitignore(cacheRoot || ".pdpp"); return file; } @@ -47,8 +47,8 @@ export function readOwnerSession({ referenceUrl, cacheRoot } = {}) { const { file } = getOwnerSessionPaths(referenceUrl, { cacheRoot }); if (!existsSync(file)) return null; try { - const data = JSON.parse(readFileSync(file, 'utf8')); - if (data && typeof data.cookie === 'string' && data.cookie.length > 0) { + const data = JSON.parse(readFileSync(file, "utf8")); + if (data && typeof data.cookie === "string" && data.cookie.length > 0) { return { cookie: data.cookie, savedAt: data.saved_at || null, file }; } } catch { @@ -76,11 +76,11 @@ export function getOwnerSessionFileMode(referenceUrl, opts = {}) { } function ensureGitignore(cacheRoot) { - const gi = join(cacheRoot, '.gitignore'); + const gi = join(cacheRoot, ".gitignore"); try { mkdirSync(dirname(gi), { recursive: true, mode: 0o700 }); if (!existsSync(gi)) { - writeFileSync(gi, '*\n!.gitignore\n', { mode: 0o600 }); + writeFileSync(gi, "*\n!.gitignore\n", { mode: 0o600 }); } } catch { // best-effort; never block CLI on .gitignore creation @@ -93,13 +93,13 @@ export function extractCookieFromSetCookie(setCookie, cookieName) { if (!setCookie) return null; const headers = Array.isArray(setCookie) ? setCookie : [setCookie]; for (const raw of headers) { - if (typeof raw !== 'string') continue; + if (typeof raw !== "string") continue; // Set-Cookie may contain multiple cookies joined by ", " when collapsed. // Split conservatively on the first attribute pair only. for (const piece of raw.split(/,\s*(?=[^;]+=[^;]+)/)) { - const [pair] = piece.split(';'); + const [pair] = piece.split(";"); if (!pair) continue; - const eq = pair.indexOf('='); + const eq = pair.indexOf("="); if (eq === -1) continue; const name = pair.slice(0, eq).trim(); const value = pair.slice(eq + 1).trim(); diff --git a/packages/local-collector/bin/pdpp-local-collector.ts b/packages/local-collector/bin/pdpp-local-collector.ts index fdc97cd4a..e48325be4 100644 --- a/packages/local-collector/bin/pdpp-local-collector.ts +++ b/packages/local-collector/bin/pdpp-local-collector.ts @@ -30,11 +30,7 @@ import { homedir } from "node:os"; import { basename, dirname, extname, join, sep } from "node:path"; import { fileURLToPath } from "node:url"; -import { - ALLOW_CUSTOM_COMMAND_ENV, - CollectorCustomCommandRefusedError, - CollectorUsageError, -} from "../src/errors.ts"; +import { ALLOW_CUSTOM_COMMAND_ENV, CollectorCustomCommandRefusedError, CollectorUsageError } from "../src/errors.ts"; import { type BundledConnectorEntry, type BundledConnectorRegistry, @@ -74,8 +70,7 @@ import { LOCAL_COLLECTOR_DEFINITIONS } from "../../polyfill-connectors/src/colle * `@pdpp/polyfill-connectors/collectors` — this file and the runtime do not * change. */ -export const BUNDLED_CONNECTORS: BundledConnectorRegistry = - createBundledConnectorRegistry(LOCAL_COLLECTOR_DEFINITIONS); +export const BUNDLED_CONNECTORS: BundledConnectorRegistry = createBundledConnectorRegistry(LOCAL_COLLECTOR_DEFINITIONS); /** Stable list of connector ids the published `pdpp-local-collector` accepts. */ export const BUNDLED_CONNECTOR_IDS: readonly string[] = bundledConnectorIds(BUNDLED_CONNECTORS); @@ -140,10 +135,7 @@ interface LocalCollectorManifestResolution { * resolving symlinks is what lets posture classification see the repo tree. */ function resolveLocalCollectorManifest(startUrl: string | URL): LocalCollectorManifestResolution { - const startPath = - typeof startUrl === "string" && !startUrl.startsWith("file:") - ? startUrl - : fileURLToPath(startUrl); + const startPath = typeof startUrl === "string" && !startUrl.startsWith("file:") ? startUrl : fileURLToPath(startUrl); let realStart = startPath; try { realStart = realpathSync(startPath); @@ -229,10 +221,7 @@ export interface LocalCollectorDeploymentPosture { export function classifyLocalCollectorDeploymentPosture( startUrl: string | URL = import.meta.url ): LocalCollectorDeploymentPosture { - const startPath = - typeof startUrl === "string" && !startUrl.startsWith("file:") - ? startUrl - : fileURLToPath(startUrl); + const startPath = typeof startUrl === "string" && !startUrl.startsWith("file:") ? startUrl : fileURLToPath(startUrl); const moduleBasename = basename(startPath); const isSourceEntrypoint = extname(startPath) === ".ts"; @@ -934,10 +923,9 @@ export async function inspectLocalReferenceRoute( } } -function referenceRouteErrorFields(error: unknown): Pick< - LocalCollectorReferenceRouteCheck, - "error_class" | "http_status" -> { +function referenceRouteErrorFields( + error: unknown +): Pick { if (error instanceof LocalDeviceHttpError) { return { error_class: error.code ? `http_${error.status}_${error.code}` : `http_${error.status}`, @@ -983,8 +971,7 @@ export function buildLocalOutboxDoctor( referenceRoute?: LocalCollectorReferenceRouteCheck | null ): LocalOutboxDoctorOutput { const posture = status.deployment_posture; - const postureDisqualifiesEvidence = - posture.kind === "repo_dist_override" || posture.is_placeholder_version; + const postureDisqualifiesEvidence = posture.kind === "repo_dist_override" || posture.is_placeholder_version; const checks: LocalOutboxDoctorOutput["checks"] = { coverage_diagnostics: status.lifecycle_state === "coverage_missing" ? "warn" : "ok", deployment_posture: postureDisqualifiesEvidence ? "warn" : "ok", @@ -996,9 +983,7 @@ export function buildLocalOutboxDoctor( const remediation: string[] = []; if (checks.outbox_failures === "fail") { const topClass = errorSummary?.top_classes?.[0]; - const causeHint = topClass - ? ` Most common cause: ${topClass.error_class} (${topClass.count} row(s)).` - : ""; + const causeHint = topClass ? ` Most common cause: ${topClass.error_class} (${topClass.count} row(s)).` : ""; remediation.push( `${status.outbox.counts.dead_letter} dead-letter row(s) need recovery.${causeHint} ` + "Preview with `pdpp-local-collector recover --source-instance-id `, then apply with " + @@ -1042,7 +1027,7 @@ export function buildLocalOutboxDoctor( } function referenceRouteRemediation(route: LocalCollectorReferenceRouteCheck): string { - const detail = route.http_status ? `HTTP ${route.http_status}` : route.error_class ?? "route failure"; + const detail = route.http_status ? `HTTP ${route.http_status}` : (route.error_class ?? "route failure"); return ( `The configured reference route (${route.base_url}) did not accept the device source-state check (${detail}). ` + "Check `PDPP_REFERENCE_BASE_URL`, network/VPN/reverse-proxy routing, and the enrolled device token before re-running. " + @@ -1078,7 +1063,7 @@ function deploymentPostureRemediation(posture: LocalCollectorDeploymentPosture): "fixes you need before re-pinning — `pnpm release:dist-tag-check` (release " + "owner) reports the live dist-tag posture; a `repo_dist_override` that is " + "ahead of the published build is dev evidence, not a build to downgrade to. " + - "See docs/reference/local-collector.md §\"Deployment Posture: Published vs Dev\"." + 'See docs/reference/local-collector.md §"Deployment Posture: Published vs Dev".' ); return parts.join(" "); } @@ -1175,7 +1160,9 @@ export function retryLocalOutboxDeadLetters(options: CliOptions): RetryDeadLette const outbox = new LocalDeviceOutbox({ path: dbPath }); try { - const statusBefore = summaryCounts(outbox.summary(options.sourceInstanceId ? { sourceInstanceId: options.sourceInstanceId } : {})); + const statusBefore = summaryCounts( + outbox.summary(options.sourceInstanceId ? { sourceInstanceId: options.sourceInstanceId } : {}) + ); const errorSummary = outbox.deadLetterErrorSummary( options.sourceInstanceId ? { sourceInstanceId: options.sourceInstanceId } : {} ); @@ -1187,7 +1174,9 @@ export function retryLocalOutboxDeadLetters(options: CliOptions): RetryDeadLette ...(options.limit ? { limit: options.limit } : {}), ...(options.sourceInstanceId ? { sourceInstanceId: options.sourceInstanceId } : {}), }); - const statusAfter = summaryCounts(outbox.summary(options.sourceInstanceId ? { sourceInstanceId: options.sourceInstanceId } : {})); + const statusAfter = summaryCounts( + outbox.summary(options.sourceInstanceId ? { sourceInstanceId: options.sourceInstanceId } : {}) + ); return { backup_path: backupPath, db: { exists: true, path: dbPath }, @@ -1306,13 +1295,16 @@ export function findLocalCollectorProfiles(input: { profileName?: string | null; sourceInstanceId?: string | null; }): LocalCollectorProfileLookupResult { - const profileDir = input.profileDir?.trim() || process.env[LOCAL_COLLECTOR_PROFILE_DIR_ENV]?.trim() || defaultCollectorProfileDir(); + const profileDir = + input.profileDir?.trim() || process.env[LOCAL_COLLECTOR_PROFILE_DIR_ENV]?.trim() || defaultCollectorProfileDir(); const sourceInstanceId = input.sourceInstanceId?.trim() || null; const files = input.profileName ? [safeProfileFileName(input.profileName)] : (() => { try { - return readdirSync(profileDir).filter((name) => name.endsWith(".env")).sort(); + return readdirSync(profileDir) + .filter((name) => name.endsWith(".env")) + .sort(); } catch { return []; } @@ -1354,7 +1346,9 @@ function applyProfileEnv(options: CliOptions, profile: LocalCollectorProfile): C const sourceInstanceId = profile.source_instance_id ?? options.sourceInstanceId; const connector = keep("--connector") ? options.connector : env.PDPP_COLLECTOR_CONNECTOR?.trim() || options.connector; const deviceId = keep("--device-id") ? options.deviceId : env.PDPP_LOCAL_DEVICE_ID?.trim() || options.deviceId; - const deviceToken = keep("--device-token") ? options.deviceToken : env.PDPP_LOCAL_DEVICE_TOKEN?.trim() || options.deviceToken; + const deviceToken = keep("--device-token") + ? options.deviceToken + : env.PDPP_LOCAL_DEVICE_TOKEN?.trim() || options.deviceToken; if (sourceInstanceId) { next.sourceInstanceId = sourceInstanceId; } @@ -1399,8 +1393,7 @@ function resolveRecoveryOptions(options: CliOptions): { }; } - const configuredQueue = - options.queuePath !== DEFAULT_QUEUE_PATH || Boolean(process.env.PDPP_COLLECTOR_QUEUE?.trim()); + const configuredQueue = options.queuePath !== DEFAULT_QUEUE_PATH || Boolean(process.env.PDPP_COLLECTOR_QUEUE?.trim()); if (!configuredQueue) { throw new CollectorUsageError( `recover could not find a local collector profile for source_instance_id '${sourceInstanceId}'. ` + @@ -1436,8 +1429,7 @@ export function resolveInspectionOptions(options: CliOptions): CliOptions { return applyProfileEnv(options, lookup.matches[0] as LocalCollectorProfile); } - const configuredQueue = - options.queuePath !== DEFAULT_QUEUE_PATH || Boolean(process.env.PDPP_COLLECTOR_QUEUE?.trim()); + const configuredQueue = options.queuePath !== DEFAULT_QUEUE_PATH || Boolean(process.env.PDPP_COLLECTOR_QUEUE?.trim()); if (!configuredQueue) { throw new CollectorUsageError( `${options.command} could not find a local collector profile for source_instance_id '${sourceInstanceId}'. ` + @@ -1516,7 +1508,9 @@ export async function recoverLocalCollector( if (!options.apply) { return { applied: false, - db: statusBefore.db.path ? { exists: statusBefore.db.exists, path: statusBefore.db.path } : { exists: false, path: "" }, + db: statusBefore.db.path + ? { exists: statusBefore.db.exists, path: statusBefore.db.path } + : { exists: false, path: "" }, dry_run: true, note: recoverDryRunNote(statusBefore), object: "local_collector_recovery", @@ -1558,7 +1552,9 @@ export async function recoverLocalCollector( const latestRun = runs.at(-1) ?? null; return { applied: true, - db: statusAfter.db.path ? { exists: statusAfter.db.exists, path: statusAfter.db.path } : { exists: false, path: "" }, + db: statusAfter.db.path + ? { exists: statusAfter.db.exists, path: statusAfter.db.path } + : { exists: false, path: "" }, drain_attempts: runs.length, drain_stopped_reason: stoppedReason, dry_run: false, @@ -1660,7 +1656,9 @@ export function pruneSentOutboxRows(options: CliOptions): PruneSentOutput { const outbox = new LocalDeviceOutbox({ path: dbPath }); try { - const statusBefore = summaryCounts(outbox.summary(options.sourceInstanceId ? { sourceInstanceId: options.sourceInstanceId } : {})); + const statusBefore = summaryCounts( + outbox.summary(options.sourceInstanceId ? { sourceInstanceId: options.sourceInstanceId } : {}) + ); const dryRun = !options.apply; const pruneInput: LocalDeviceOutboxPruneSentInput = { @@ -1674,7 +1672,9 @@ export function pruneSentOutboxRows(options: CliOptions): PruneSentOutput { // For apply, back up first then delete. const backupPath = dryRun ? null : backupSqliteDb(outbox, dbPath, "prune-sent"); const result = outbox.pruneSent(pruneInput); - const statusAfter = summaryCounts(outbox.summary(options.sourceInstanceId ? { sourceInstanceId: options.sourceInstanceId } : {})); + const statusAfter = summaryCounts( + outbox.summary(options.sourceInstanceId ? { sourceInstanceId: options.sourceInstanceId } : {}) + ); const note = pruneSentNote(result, dryRun, reportedOlderThanDays, options.keepCount); return { @@ -1874,11 +1874,7 @@ function compactDryRunNote(stats: LocalDeviceOutboxPageStats, nonSucceeded: numb return base; } -function compactAppliedNote( - result: LocalDeviceOutboxCompactResult, - nonSucceeded: number, - force: boolean -): string { +function compactAppliedNote(result: LocalDeviceOutboxCompactResult, nonSucceeded: number, force: boolean): string { const reclaimedMb = (result.reclaimedBytes / (1024 * 1024)).toFixed(1); const forcedNote = nonSucceeded > 0 && force @@ -2125,11 +2121,7 @@ function parseCsv(value: string): string[] { .filter(Boolean); } -export function scopedDefaultQueuePath( - queuePath: string, - defaultQueuePath: string, - connectionId: string -): string { +export function scopedDefaultQueuePath(queuePath: string, defaultQueuePath: string, connectionId: string): string { if (queuePath !== defaultQueuePath) { return queuePath; } diff --git a/packages/local-collector/scripts/pack-install-run.mjs b/packages/local-collector/scripts/pack-install-run.mjs index cd05b3520..3028e02e4 100644 --- a/packages/local-collector/scripts/pack-install-run.mjs +++ b/packages/local-collector/scripts/pack-install-run.mjs @@ -18,12 +18,7 @@ const cliPackageRoot = path.join(repoRoot, "packages/cli"); const referenceServerEntry = path.join(repoRoot, "reference-implementation/server/index.js"); const referenceDbModule = path.join(repoRoot, "reference-implementation/server/db.js"); const forbiddenPackages = ["playwright", "patchright", "imapflow", "pdf-parse", "better-sqlite3", "linkedom"]; -const browserArtifactPatterns = [ - /chromium/i, - /chrome-linux/i, - /ms-playwright/i, - /patchright/i, -]; +const browserArtifactPatterns = [/chromium/i, /chrome-linux/i, /ms-playwright/i, /patchright/i]; function log(message) { process.stdout.write(`${message}\n`); @@ -69,11 +64,7 @@ async function pathExists(candidate) { async function assertPackageAbsent(projectDir, packageName) { const candidate = path.join(projectDir, "node_modules", ...packageName.split("/")); - assert.equal( - await pathExists(candidate), - false, - `unexpected browser-bound package installed: ${packageName}` - ); + assert.equal(await pathExists(candidate), false, `unexpected browser-bound package installed: ${packageName}`); } async function assertNoBrowserArtifacts(rootDir) { @@ -140,7 +131,11 @@ async function main() { } if (await pathExists(referenceServerEntry)) { - await runFixtureBackedEnrollRunSmoke({ projectDir, env, advertisedProtocolVersion: advertised.collector_protocol_version }); + await runFixtureBackedEnrollRunSmoke({ + projectDir, + env, + advertisedProtocolVersion: advertised.collector_protocol_version, + }); await runProtocolMismatchSmoke({ projectDir, env }); } else { log("SKIP fixture-backed enroll/run smoke: reference-implementation/server/index.js not present."); @@ -182,21 +177,21 @@ async function runFixtureBackedEnrollRunSmoke({ projectDir, env, advertisedProto connector_id: "codex", local_binding_name: "pack-install-run-laptop", }); - assert.equal(codeResp.status, 201, `enrollment-codes returned ${codeResp.status}: ${JSON.stringify(codeResp.body)}`); + assert.equal( + codeResp.status, + 201, + `enrollment-codes returned ${codeResp.status}: ${JSON.stringify(codeResp.body)}` + ); const enrollmentCode = codeResp.body.enrollment_code; - assert.ok(typeof enrollmentCode === "string" && enrollmentCode.length > 0, "enrollment_code must be a non-empty string"); + assert.ok( + typeof enrollmentCode === "string" && enrollmentCode.length > 0, + "enrollment_code must be a non-empty string" + ); log("Running installed pdpp-local-collector enroll against the in-process reference server..."); const enroll = await run( "npx", - [ - "pdpp-local-collector", - "enroll", - "--base-url", - baseUrl, - "--code", - enrollmentCode, - ], + ["pdpp-local-collector", "enroll", "--base-url", baseUrl, "--code", enrollmentCode], { cwd: projectDir, env } ); const enrollment = JSON.parse(enroll.stdout); @@ -250,9 +245,16 @@ async function runFixtureBackedEnrollRunSmoke({ projectDir, env, advertisedProto } ); const runOutput = JSON.parse(runResult.stdout); - assert.equal(runOutput.done?.status, "succeeded", `codex connector did not report DONE.status=succeeded: ${runResult.stdout}`); + assert.equal( + runOutput.done?.status, + "succeeded", + `codex connector did not report DONE.status=succeeded: ${runResult.stdout}` + ); assert.ok((runOutput.recordsQueued ?? 0) > 0, `codex connector did not queue any records: ${runResult.stdout}`); - assert.ok((runOutput.sentBatches ?? 0) > 0, `codex connector did not send any batches to the reference server: ${runResult.stdout}`); + assert.ok( + (runOutput.sentBatches ?? 0) > 0, + `codex connector did not send any batches to the reference server: ${runResult.stdout}` + ); // Records land under the bare canonical connector key (`codex`), NOT a // `local-device:codex` storage prefix — connection isolation is carried by @@ -305,24 +307,20 @@ async function runProtocolMismatchSmoke({ projectDir, env }) { connector_id: "codex", local_binding_name: "pack-install-run-pinned", }); - assert.equal(codeResp.status, 201, `pinned enrollment-codes returned ${codeResp.status}: ${JSON.stringify(codeResp.body)}`); + assert.equal( + codeResp.status, + 201, + `pinned enrollment-codes returned ${codeResp.status}: ${JSON.stringify(codeResp.body)}` + ); const enrollmentCode = codeResp.body.enrollment_code; log("Calling pdpp-local-collector enroll against the pinned server (expecting 409)..."); let failure = null; try { - await run( - "npx", - [ - "pdpp-local-collector", - "enroll", - "--base-url", - baseUrl, - "--code", - enrollmentCode, - ], - { cwd: projectDir, env } - ); + await run("npx", ["pdpp-local-collector", "enroll", "--base-url", baseUrl, "--code", enrollmentCode], { + cwd: projectDir, + env, + }); } catch (error) { failure = error; } @@ -397,10 +395,7 @@ async function prepareCodexFixture() { path.join(promptsDir, "hello.md"), "---\nname: hello\ndescription: greet the operator\n---\n\nHello from the pack-install-run fixture.\n" ); - await writeFile( - path.join(rulesDir, "trust.rules"), - "# trust registry\nallow shell pwd\n" - ); + await writeFile(path.join(rulesDir, "trust.rules"), "# trust registry\nallow shell pwd\n"); return codexHome; } diff --git a/packages/local-collector/src/runner.ts b/packages/local-collector/src/runner.ts index 32880ded8..4b08d6f1b 100644 --- a/packages/local-collector/src/runner.ts +++ b/packages/local-collector/src/runner.ts @@ -185,15 +185,11 @@ export type BundledConnectorRegistry = Readonly/`. */ export function resolveBundledConnectorEntry(entry: string): string { - const built = fileURLToPath( - new URL(`../../polyfill-connectors/connectors/${entry}/index.js`, import.meta.url) - ); + const built = fileURLToPath(new URL(`../../polyfill-connectors/connectors/${entry}/index.js`, import.meta.url)); if (existsSync(built)) { return built; } - return fileURLToPath( - new URL(`../../polyfill-connectors/connectors/${entry}/index.ts`, import.meta.url) - ); + return fileURLToPath(new URL(`../../polyfill-connectors/connectors/${entry}/index.ts`, import.meta.url)); } function commandForEntry(entry: string): "node" | "tsx" { @@ -255,9 +251,7 @@ export function getBundledConnectorFrom( * the map covers exactly the injected ids; the shape stays stable so * per-connector versioning can be added later without a payload migration. */ -export function bundledConnectorVersions( - registry: BundledConnectorRegistry -): Readonly> { +export function bundledConnectorVersions(registry: BundledConnectorRegistry): Readonly> { const versions: Record = {}; for (const id of Object.keys(registry)) { versions[id] = PROTOCOL_VERSION; diff --git a/packages/mcp-server/bin/pdpp-mcp-server.js b/packages/mcp-server/bin/pdpp-mcp-server.js index e25d2c4a8..cf3b7c2d5 100755 --- a/packages/mcp-server/bin/pdpp-mcp-server.js +++ b/packages/mcp-server/bin/pdpp-mcp-server.js @@ -2,7 +2,7 @@ // Copyright The PDP-Connect Contributors // SPDX-License-Identifier: Apache-2.0 -import { runMcpServerCli } from '../src/index.js'; +import { runMcpServerCli } from "../src/index.js"; runMcpServerCli(process.argv.slice(2)).then( (code) => { diff --git a/packages/mcp-server/src/credentials.js b/packages/mcp-server/src/credentials.js index aa5e90d51..7f0684d89 100644 --- a/packages/mcp-server/src/credentials.js +++ b/packages/mcp-server/src/credentials.js @@ -1,12 +1,12 @@ // Copyright The PDP-Connect Contributors // SPDX-License-Identifier: Apache-2.0 -import { readStoredCredential } from '@pdpp/cli'; +import { readStoredCredential } from "@pdpp/cli"; export class CredentialError extends Error { constructor(code, message, exitCode = 78) { super(message); - this.name = 'CredentialError'; + this.name = "CredentialError"; this.code = code; this.exitCode = exitCode; } @@ -22,8 +22,8 @@ export class CredentialError extends Error { export async function loadScopedCredential(providerUrl, options = {}) { if (!providerUrl) { throw new CredentialError( - 'no_provider_url', - 'Provider URL required. Pass --provider-url or set PDPP_PROVIDER_URL.', + "no_provider_url", + "Provider URL required. Pass --provider-url or set PDPP_PROVIDER_URL.", 64 ); } @@ -32,29 +32,29 @@ export async function loadScopedCredential(providerUrl, options = {}) { try { result = await readStoredCredential(providerUrl, { cacheRoot: options.cacheRoot }); } catch (error) { - if (error?.code === 'not_connected') { + if (error?.code === "not_connected") { throw new CredentialError( - 'not_connected', + "not_connected", `No scoped PDPP credential cached for ${providerUrl}. Run \`pdpp connect ${providerUrl}\` and try again.`, 78 ); } - if (error?.code === 'credential_expired') { + if (error?.code === "credential_expired") { throw new CredentialError( - 'credential_expired', + "credential_expired", `Cached PDPP credential for ${providerUrl} is expired. Run \`pdpp connect ${providerUrl}\` again.`, 78 ); } - if (error?.code === 'credential_invalid') { + if (error?.code === "credential_invalid") { throw new CredentialError( - 'credential_invalid', + "credential_invalid", `Cached PDPP credential for ${providerUrl} is malformed; re-run \`pdpp connect ${providerUrl}\`.`, 78 ); } - if (error?.code === 'invalid_provider_url') { - throw new CredentialError('invalid_provider_url', error.message, 64); + if (error?.code === "invalid_provider_url") { + throw new CredentialError("invalid_provider_url", error.message, 64); } throw error; } @@ -62,7 +62,7 @@ export async function loadScopedCredential(providerUrl, options = {}) { const credential = result?.credential; if (!credential?.access_token) { throw new CredentialError( - 'credential_invalid', + "credential_invalid", `Cached PDPP credential for ${providerUrl} is missing an access token.`, 78 ); @@ -70,7 +70,7 @@ export async function loadScopedCredential(providerUrl, options = {}) { if (isOwnerKind(credential)) { throw new CredentialError( - 'owner_token_refused', + "owner_token_refused", `Cached credential for ${providerUrl} is an owner token; owner credentials are refused by the MCP adapter.`, 77 ); @@ -80,23 +80,18 @@ export async function loadScopedCredential(providerUrl, options = {}) { providerUrl: result.providerUrl, cacheFile: result.cacheFile, accessToken: credential.access_token, - tokenType: credential.token_type ?? 'Bearer', + tokenType: credential.token_type ?? "Bearer", scope: credential.scope ?? result.payload?.scope ?? null, grantId: credential.grant_id ?? result.payload?.grant_id ?? null, }; } function isOwnerKind(credential) { - if (!credential || typeof credential !== 'object') { + if (!credential || typeof credential !== "object") { return false; } // The PDPP audit doc names `pdpp_token_kind=owner` as the owner-distinguishing claim // on cached credentials. Treat any kind/role-shaped owner signal as a refusal trigger. - const flagged = [ - credential.pdpp_token_kind, - credential.token_kind, - credential.kind, - credential.role, - ]; - return flagged.some((value) => typeof value === 'string' && value.toLowerCase() === 'owner'); + const flagged = [credential.pdpp_token_kind, credential.token_kind, credential.kind, credential.role]; + return flagged.some((value) => typeof value === "string" && value.toLowerCase() === "owner"); } diff --git a/packages/mcp-server/src/index.js b/packages/mcp-server/src/index.js index ac5dd2d96..d7add69a0 100644 --- a/packages/mcp-server/src/index.js +++ b/packages/mcp-server/src/index.js @@ -1,12 +1,8 @@ // Copyright The PDP-Connect Contributors // SPDX-License-Identifier: Apache-2.0 -import { CredentialError, loadScopedCredential } from './credentials.js'; -import { - DEFAULT_SERVER_NAME, - DEFAULT_SERVER_VERSION, - startStdioServer, -} from './server.js'; +import { CredentialError, loadScopedCredential } from "./credentials.js"; +import { DEFAULT_SERVER_NAME, DEFAULT_SERVER_VERSION, startStdioServer } from "./server.js"; const HELP = `pdpp-mcp-server — local stdio MCP adapter over a PDPP resource server @@ -38,11 +34,11 @@ export async function runMcpServerCli(argv, deps = {}) { const load = deps.loadScopedCredential ?? loadScopedCredential; const start = deps.startStdioServer ?? startStdioServer; - if (argv.includes('--help') || argv.includes('-h')) { + if (argv.includes("--help") || argv.includes("-h")) { stderr.write(HELP); return 0; } - if (argv.includes('--version')) { + if (argv.includes("--version")) { stderr.write(`${DEFAULT_SERVER_VERSION}\n`); return 0; } @@ -87,7 +83,7 @@ export async function runMcpServerCli(argv, deps = {}) { // Block until the transport signals close (e.g. parent harness closes our stdin). // Without this, the bin would exit immediately after wiring up the server and the // child process would terminate before any MCP request could be processed. - if (handle && typeof handle.closed?.then === 'function') { + if (handle && typeof handle.closed?.then === "function") { await handle.closed; } @@ -97,19 +93,18 @@ export async function runMcpServerCli(argv, deps = {}) { export class OptionParseError extends Error { constructor(message, exitCode = 64) { super(message); - this.name = 'OptionParseError'; + this.name = "OptionParseError"; this.exitCode = exitCode; } } export function parseOptions(argv, env) { - const providerUrl = readOption(argv, '--provider-url') ?? env.PDPP_PROVIDER_URL ?? ''; - const cacheRoot = readOption(argv, '--cache-root') ?? env.PDPP_CACHE_ROOT ?? '.pdpp'; - const serverName = - readOption(argv, '--server-name') ?? env.PDPP_MCP_SERVER_NAME ?? DEFAULT_SERVER_NAME; + const providerUrl = readOption(argv, "--provider-url") ?? env.PDPP_PROVIDER_URL ?? ""; + const cacheRoot = readOption(argv, "--cache-root") ?? env.PDPP_CACHE_ROOT ?? ".pdpp"; + const serverName = readOption(argv, "--server-name") ?? env.PDPP_MCP_SERVER_NAME ?? DEFAULT_SERVER_NAME; if (!providerUrl) { - throw new OptionParseError('Missing --provider-url (or PDPP_PROVIDER_URL).'); + throw new OptionParseError("Missing --provider-url (or PDPP_PROVIDER_URL)."); } if (env.PDPP_OWNER_TOKEN || env.PDPP_OWNER_SESSION_COOKIE) { @@ -117,7 +112,7 @@ export function parseOptions(argv, env) { // we never consult it. Exposing the owner-mode self-export surface through MCP // is the footgun the design forbids. throw new OptionParseError( - 'Refusing to start: owner credentials (PDPP_OWNER_TOKEN / PDPP_OWNER_SESSION_COOKIE) are present in the environment. Unset them before running the MCP adapter.', + "Refusing to start: owner credentials (PDPP_OWNER_TOKEN / PDPP_OWNER_SESSION_COOKIE) are present in the environment. Unset them before running the MCP adapter.", 77 ); } @@ -131,7 +126,7 @@ function readOption(argv, name) { return argv[index + 1]; } -export { CredentialError, loadScopedCredential } from './credentials.js'; +export { CredentialError, loadScopedCredential } from "./credentials.js"; export { createPdppMcpServer, handleStreamableHttpRequest, @@ -139,6 +134,6 @@ export { DEFAULT_SERVER_NAME, DEFAULT_SERVER_VERSION, PDPP_MCP_TOOL_NAMES, -} from './server.js'; -export { RsClient } from './rs-client.js'; -export { buildTools, buildResourceTemplates, buildStreamResourceTemplate, InvalidResourceUriError } from './tools.js'; +} from "./server.js"; +export { RsClient } from "./rs-client.js"; +export { buildTools, buildResourceTemplates, buildStreamResourceTemplate, InvalidResourceUriError } from "./tools.js"; diff --git a/packages/mcp-server/src/rs-client.js b/packages/mcp-server/src/rs-client.js index cfe33ad5d..dbade9474 100644 --- a/packages/mcp-server/src/rs-client.js +++ b/packages/mcp-server/src/rs-client.js @@ -7,29 +7,29 @@ */ export class RsClient { constructor({ providerUrl, accessToken, fetch = globalThis.fetch, userAgent }) { - if (typeof fetch !== 'function') { - throw new TypeError('RsClient requires a fetch implementation'); + if (typeof fetch !== "function") { + throw new TypeError("RsClient requires a fetch implementation"); } if (!providerUrl) { - throw new TypeError('RsClient requires providerUrl'); + throw new TypeError("RsClient requires providerUrl"); } if (!accessToken) { - throw new TypeError('RsClient requires accessToken'); + throw new TypeError("RsClient requires accessToken"); } - this.providerUrl = providerUrl.replace(/\/$/, ''); + this.providerUrl = providerUrl.replace(/\/$/, ""); this.accessToken = accessToken; this.fetch = fetch; - this.userAgent = userAgent ?? '@pdpp/mcp-server'; + this.userAgent = userAgent ?? "@pdpp/mcp-server"; } async getJson(path, { query, headers } = {}) { const url = this.buildUrl(path, query); const response = await this.fetch(url, { - method: 'GET', + method: "GET", headers: { - Accept: 'application/json', + Accept: "application/json", Authorization: `Bearer ${this.accessToken}`, - 'User-Agent': this.userAgent, + "User-Agent": this.userAgent, ...(headers ?? {}), }, }); @@ -39,10 +39,10 @@ export class RsClient { async getRaw(path, { query, headers } = {}) { const url = this.buildUrl(path, query); const response = await this.fetch(url, { - method: 'GET', + method: "GET", headers: { Authorization: `Bearer ${this.accessToken}`, - 'User-Agent': this.userAgent, + "User-Agent": this.userAgent, ...(headers ?? {}), }, }); @@ -50,15 +50,15 @@ export class RsClient { } async postJson(path, { body, query, headers } = {}) { - return this.sendJson('POST', path, { body, query, headers }); + return this.sendJson("POST", path, { body, query, headers }); } async patchJson(path, { body, query, headers } = {}) { - return this.sendJson('PATCH', path, { body, query, headers }); + return this.sendJson("PATCH", path, { body, query, headers }); } async deleteJson(path, { query, headers } = {}) { - return this.sendJson('DELETE', path, { body: undefined, query, headers }); + return this.sendJson("DELETE", path, { body: undefined, query, headers }); } async sendJson(method, path, { body, query, headers } = {}) { @@ -67,23 +67,23 @@ export class RsClient { const init = { method, headers: { - Accept: 'application/json', + Accept: "application/json", Authorization: `Bearer ${this.accessToken}`, - 'User-Agent': this.userAgent, - ...(hasBody ? { 'Content-Type': 'application/json' } : {}), + "User-Agent": this.userAgent, + ...(hasBody ? { "Content-Type": "application/json" } : {}), ...(headers ?? {}), }, }; if (hasBody) { - init.body = typeof body === 'string' ? body : JSON.stringify(body); + init.body = typeof body === "string" ? body : JSON.stringify(body); } const response = await this.fetch(url, init); return parseRsResponse(response, { expectJson: true }); } buildUrl(path, query) { - const url = new URL(path.startsWith('/') ? path : `/${path}`, `${this.providerUrl}/`); - if (query && typeof query === 'object') { + const url = new URL(path.startsWith("/") ? path : `/${path}`, `${this.providerUrl}/`); + if (query && typeof query === "object") { for (const [key, value] of Object.entries(query)) { appendQuery(url, key, value); } @@ -103,9 +103,9 @@ function appendQuery(url, key, value) { } return; } - if (typeof value === 'object') { + if (typeof value === "object") { throw new TypeError( - `query parameter '${key}' must be a scalar or array; encode nested query shapes explicitly before calling RsClient`, + `query parameter '${key}' must be a scalar or array; encode nested query shapes explicitly before calling RsClient` ); } url.searchParams.append(key, String(value)); @@ -113,15 +113,15 @@ function appendQuery(url, key, value) { async function parseRsResponse(response, { expectJson }) { const status = response.status; - const contentType = response.headers?.get?.('content-type') ?? ''; - const requestId = response.headers?.get?.('x-request-id') ?? null; + const contentType = response.headers?.get?.("content-type") ?? ""; + const requestId = response.headers?.get?.("x-request-id") ?? null; if (status >= 200 && status < 300) { if (expectJson) { if (status === 204) { return { ok: true, status, body: null, requestId, contentType }; } - const body = contentType.includes('application/json') ? await response.json() : await response.text(); + const body = contentType.includes("application/json") ? await response.json() : await response.text(); return { ok: true, status, body, requestId, contentType }; } const buffer = Buffer.from(await response.arrayBuffer()); @@ -130,7 +130,7 @@ async function parseRsResponse(response, { expectJson }) { let errorBody = null; try { - if (contentType.includes('application/json')) { + if (contentType.includes("application/json")) { errorBody = await response.json(); } else { errorBody = await response.text(); @@ -140,7 +140,7 @@ async function parseRsResponse(response, { expectJson }) { } const envelope = normalizeErrorEnvelope(errorBody, status); - if (requestId && envelope && typeof envelope === 'object' && !envelope.request_id) { + if (requestId && envelope && typeof envelope === "object" && !envelope.request_id) { envelope.request_id = requestId; } @@ -148,11 +148,11 @@ async function parseRsResponse(response, { expectJson }) { } function normalizeErrorEnvelope(body, status) { - if (body && typeof body === 'object') { - if (body.error && typeof body.error === 'object') { + if (body && typeof body === "object") { + if (body.error && typeof body.error === "object") { return body.error; } - if (typeof body.error === 'string') { + if (typeof body.error === "string") { return { type: body.error, code: body.error, @@ -163,8 +163,8 @@ function normalizeErrorEnvelope(body, status) { } return { - type: 'rs_error', + type: "rs_error", code: `http_${status}`, - message: typeof body === 'string' && body.length > 0 ? body : `Resource server returned HTTP ${status}`, + message: typeof body === "string" && body.length > 0 ? body : `Resource server returned HTTP ${status}`, }; } diff --git a/packages/mcp-server/src/server.js b/packages/mcp-server/src/server.js index 19233cec9..9465d8f69 100644 --- a/packages/mcp-server/src/server.js +++ b/packages/mcp-server/src/server.js @@ -1,29 +1,25 @@ // Copyright The PDP-Connect Contributors // SPDX-License-Identifier: Apache-2.0 -import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; -import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js'; +import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js"; -import { - buildResourceTemplates, - buildTools, - PDPP_MCP_TOOL_NAMES, -} from './tools.js'; -import { RsClient } from './rs-client.js'; +import { buildResourceTemplates, buildTools, PDPP_MCP_TOOL_NAMES } from "./tools.js"; +import { RsClient } from "./rs-client.js"; -export const DEFAULT_SERVER_NAME = 'pdpp-mcp-server'; -export const DEFAULT_SERVER_VERSION = '0.0.0'; +export const DEFAULT_SERVER_NAME = "pdpp-mcp-server"; +export const DEFAULT_SERVER_VERSION = "0.0.0"; // Shared MCP server instructions. The first 512 characters must be // self-contained for ChatGPT and Codex (OpenAI Apps SDK guidance). // Cross-tool details that would otherwise repeat across tool descriptions live // here; tool descriptions stay concise and routing-specific. export const PDPP_MCP_INSTRUCTIONS = - 'PDPP tools are grant-scoped. Start with `schema`, then call `schema(stream)` after choosing a stream; add `connection_id` when a stream name appears under multiple sources or before full schema. Use `connection_id` from schema results or `available_connections` errors to disambiguate sources. Filters must be typed objects, not bracket strings. Page and narrow with `limit`, `cursor`, and `fields`; prefer `aggregate` or lexical `search` for exact terms. ' + - 'The configured bearer limits every result; do not use owner or control-plane tokens for normal MCP access. Schema advertises valid fields, filter operators, expand relations, sort/count support, connection identities, and connector keys. Persist `connection_id`, not `grant_id`, across reconnects. Search result ids are self-contained handles; pass them to `fetch` for projected records or to `read_record_field` for bounded field windows. ' + - 'When a preview is not enough, follow `structuredContent.content_ladder`: call `read_record_field` with the supplied arguments. Resource-aware hosts may also read hidden/returned resource URIs, but generic resource reads are not required for ordinary text evidence. ' + - '`content[]` is the reliable model-visible guide and includes next cursors/bookmarks when present; `structuredContent` is a host-dependent machine envelope, not the only place to find next-step handles.'; + "PDPP tools are grant-scoped. Start with `schema`, then call `schema(stream)` after choosing a stream; add `connection_id` when a stream name appears under multiple sources or before full schema. Use `connection_id` from schema results or `available_connections` errors to disambiguate sources. Filters must be typed objects, not bracket strings. Page and narrow with `limit`, `cursor`, and `fields`; prefer `aggregate` or lexical `search` for exact terms. " + + "The configured bearer limits every result; do not use owner or control-plane tokens for normal MCP access. Schema advertises valid fields, filter operators, expand relations, sort/count support, connection identities, and connector keys. Persist `connection_id`, not `grant_id`, across reconnects. Search result ids are self-contained handles; pass them to `fetch` for projected records or to `read_record_field` for bounded field windows. " + + "When a preview is not enough, follow `structuredContent.content_ladder`: call `read_record_field` with the supplied arguments. Resource-aware hosts may also read hidden/returned resource URIs, but generic resource reads are not required for ordinary text evidence. " + + "`content[]` is the reliable model-visible guide and includes next cursors/bookmarks when present; `structuredContent` is a host-dependent machine envelope, not the only place to find next-step handles."; /** * Build an MCP server wired to a PDPP resource server through the supplied scoped token. @@ -65,17 +61,13 @@ export function createPdppMcpServer({ if (tool.outputSchema) { config.outputSchema = tool.outputSchema; } - server.registerTool( - tool.name, - config, - async (args) => { - try { - return await tool.handler(args ?? {}); - } catch (error) { - return toolHandlerError(error); - } + server.registerTool(tool.name, config, async (args) => { + try { + return await tool.handler(args ?? {}); + } catch (error) { + return toolHandlerError(error); } - ); + }); } for (const template of buildResourceTemplates({ rs, providerUrl })) { @@ -140,12 +132,12 @@ function toolHandlerError(error) { isError: true, content: [ { - type: 'text', + type: "text", text: JSON.stringify( { - type: 'adapter_error', - code: error?.code ?? 'tool_handler_error', - message: error?.message ?? 'Tool handler threw an error', + type: "adapter_error", + code: error?.code ?? "tool_handler_error", + message: error?.message ?? "Tool handler threw an error", }, null, 2 @@ -154,14 +146,12 @@ function toolHandlerError(error) { ], structuredContent: { error: { - type: 'adapter_error', - code: error?.code ?? 'tool_handler_error', - message: error?.message ?? 'Tool handler threw an error', + type: "adapter_error", + code: error?.code ?? "tool_handler_error", + message: error?.message ?? "Tool handler threw an error", }, }, }; } -export { - PDPP_MCP_TOOL_NAMES, -} from './tools.js'; +export { PDPP_MCP_TOOL_NAMES } from "./tools.js"; diff --git a/packages/mcp-server/src/tools.js b/packages/mcp-server/src/tools.js index 1ba55ad99..0e4220333 100644 --- a/packages/mcp-server/src/tools.js +++ b/packages/mcp-server/src/tools.js @@ -1,11 +1,8 @@ // Copyright The PDP-Connect Contributors // SPDX-License-Identifier: Apache-2.0 -import { - buildRecordContentLadder as buildSharedRecordContentLadder, - summarizeRecordEvidence, -} from '@pdpp/read-core'; -import { z } from 'zod'; +import { buildRecordContentLadder as buildSharedRecordContentLadder, summarizeRecordEvidence } from "@pdpp/read-core"; +import { z } from "zod"; const READ_ONLY_ANNOTATIONS = { readOnlyHint: true, @@ -15,12 +12,12 @@ const READ_ONLY_ANNOTATIONS = { }; export const PDPP_MCP_TOOL_NAMES = Object.freeze([ - 'schema', - 'query_records', - 'aggregate', - 'search', - 'fetch', - 'read_record_field', + "schema", + "query_records", + "aggregate", + "search", + "fetch", + "read_record_field", ]); function selectNormalTools(tools) { @@ -30,11 +27,11 @@ function selectNormalTools(tools) { const selectedNames = new Set(selected.map((tool) => tool.name)); const missing = expectedNames.filter((name) => !selectedNames.has(name)); if (missing.length > 0) { - throw new Error(`MCP normal surface is missing expected tools: ${missing.join(', ')}`); + throw new Error(`MCP normal surface is missing expected tools: ${missing.join(", ")}`); } const unexpected = tools.map((tool) => tool.name).filter((name) => !expected.has(name)); if (unexpected.length > 0) { - throw new Error(`MCP normal surface has unexpected tools: ${unexpected.join(', ')}`); + throw new Error(`MCP normal surface has unexpected tools: ${unexpected.join(", ")}`); } return selected; } @@ -44,22 +41,22 @@ function selectNormalTools(tools) { // `sort` and `count` are canonical public read primitives advertised by // `GET /v1/schema` and implemented by the reference runtime where declared. const SUPPORTED_QUERY_KEYS = new Set([ - 'limit', - 'cursor', - 'order', - 'sort', - 'count', - 'filter', - 'fields', - 'view', - 'expand', - 'expand_limit', - 'changes_since', + "limit", + "cursor", + "order", + "sort", + "count", + "filter", + "fields", + "view", + "expand", + "expand_limit", + "changes_since", // Optional public connection identity. Forwarded verbatim to the RS so // the resource server enforces grant scope; the MCP layer never invents // or rewrites a connection_id. See: // openspec/changes/expose-connection-identity-on-public-read - 'connection_id', + "connection_id", ]); // Mirror of the REST aggregate query-param vocabulary @@ -68,57 +65,57 @@ const SUPPORTED_QUERY_KEYS = new Set([ // silently drops a member. See: // openspec/changes/add-aggregate-time-buckets-and-distinct const SUPPORTED_AGGREGATE_QUERY_KEYS = new Set([ - 'metric', - 'field', - 'group_by', - 'group_by_time', - 'granularity', - 'time_zone', - 'limit', - 'filter', - 'connection_id', + "metric", + "field", + "group_by", + "group_by_time", + "granularity", + "time_zone", + "limit", + "filter", + "connection_id", ]); const CONNECTION_ID_DESCRIPTION = - 'Optional. Scope this call to one connection. Omit to fan in across all granted connections. Obtain from `schema` or the `available_connections` field in a typed 409 error — each entry includes `connector_key` and `connection_id`. Persist `connection_id` (not `grant_id`) across reconnects.'; + "Optional. Scope this call to one connection. Omit to fan in across all granted connections. Obtain from `schema` or the `available_connections` field in a typed 409 error — each entry includes `connector_key` and `connection_id`. Persist `connection_id` (not `grant_id`) across reconnects."; const LIMIT_DESCRIPTION = - 'Records per page. Omit for the default page of 25; the maximum is 100 (the spec-core §8 contract). Values above 100 are rejected here rather than silently clamped, so the page size you request is always the page size you get. Page forward with the returned `cursor` instead of asking for a larger page.'; + "Records per page. Omit for the default page of 25; the maximum is 100 (the spec-core §8 contract). Values above 100 are rejected here rather than silently clamped, so the page size you request is always the page size you get. Page forward with the returned `cursor` instead of asking for a larger page."; const SEARCH_LIMIT_DESCRIPTION = - 'Hits per page. Omit for the default page of 25; the maximum is 100 — the bound the published `/v1/search`, `/v1/search/semantic`, and `/v1/search/hybrid` contract declares and every mode honors (mirrored as `capabilities.{lexical,semantic,hybrid}_retrieval.max_limit` in `/.well-known/oauth-protected-resource` and `GET /v1/schema`). Values above 100 are rejected here rather than forwarded to be silently clamped by the RS, so the page size you request is always the page size you get. Page forward with the returned `cursor` (lexical and semantic page; hybrid does not) instead of asking for a larger page.'; + "Hits per page. Omit for the default page of 25; the maximum is 100 — the bound the published `/v1/search`, `/v1/search/semantic`, and `/v1/search/hybrid` contract declares and every mode honors (mirrored as `capabilities.{lexical,semantic,hybrid}_retrieval.max_limit` in `/.well-known/oauth-protected-resource` and `GET /v1/schema`). Values above 100 are rejected here rather than forwarded to be silently clamped by the RS, so the page size you request is always the page size you get. Page forward with the returned `cursor` (lexical and semantic page; hybrid does not) instead of asking for a larger page."; const FIELDS_DESCRIPTION = - 'Field allowlist for projection. Field paths must be declared by the stream; advertised by `GET /v1/schema` (`field_capabilities`). Unknown paths are rejected by the RS rather than silently widened.'; + "Field allowlist for projection. Field paths must be declared by the stream; advertised by `GET /v1/schema` (`field_capabilities`). Unknown paths are rejected by the RS rather than silently widened."; const VIEW_DESCRIPTION = - 'Named projection. A stream-declared view id (advertised by `GET /v1/schema` under each stream\'s `views`) that projects the returned records down to the view\'s field set. Mutually exclusive with `fields` (passing both is rejected by the RS); an unknown view id is rejected rather than silently ignored. Use `view` for a curated projection and `fields` for an ad-hoc one.'; + "Named projection. A stream-declared view id (advertised by `GET /v1/schema` under each stream's `views`) that projects the returned records down to the view's field set. Mutually exclusive with `fields` (passing both is rejected by the RS); an unknown view id is rejected rather than silently ignored. Use `view` for a curated projection and `fields` for an ad-hoc one."; const FILTER_DESCRIPTION = 'Typed per-field filter. Pass an OBJECT keyed by field name — never a pre-encoded query string. Exact match: `{ "user_id": "U123" }`. Range: `{ "created_at": { "gte": "2026-01-01T00:00:00Z", "lt": "2026-02-01T00:00:00Z" } }`, where the operator is one of `gte`, `gt`, `lte`, `lt`. Multiple fields AND together. The adapter encodes this into the RS `filter[field]=value` / `filter[field][op]=value` query shape for you. Allowed fields and operators are advertised by `GET /v1/schema` (`field_capabilities`); unsupported fields or operators are rejected by the RS rather than silently ignored.'; const EXPAND_DESCRIPTION = - 'One-hop inline expansion list. Each entry is a manifest-declared parent-to-child relation. Expandable relations and per-relation `expand_limit` caps are advertised by `GET /v1/schema` (`expand_capabilities`); unadvertised relations are rejected by the RS.'; + "One-hop inline expansion list. Each entry is a manifest-declared parent-to-child relation. Expandable relations and per-relation `expand_limit` caps are advertised by `GET /v1/schema` (`expand_capabilities`); unadvertised relations are rejected by the RS."; const EXPAND_LIMIT_DESCRIPTION = 'Typed per-relation cap for has-many expansion, keyed by relation name. Pass an object such as `{ "messages": 3 }`; the adapter encodes it into the RS `expand_limit[relation]=N` query shape. The RS clamps to the per-relation `max_limit` advertised by `GET /v1/schema`.'; const ORDER_DESCRIPTION = - 'Legacy page order for cursor-based pagination: `asc` or `desc`. Prefer canonical `sort` when `/v1/schema` advertises sortable fields; `order` remains accepted for clients that have not migrated.'; + "Legacy page order for cursor-based pagination: `asc` or `desc`. Prefer canonical `sort` when `/v1/schema` advertises sortable fields; `order` remains accepted for clients that have not migrated."; const SORT_DESCRIPTION = - 'Canonical sign-prefix sort spec advertised by `GET /v1/schema` (e.g. `sort=-emitted_at`). The reference runtime supports the advertised cursor field; unsupported fields, conflicting directions, or sort/order disagreement are rejected with typed errors rather than treated as no-ops.'; + "Canonical sign-prefix sort spec advertised by `GET /v1/schema` (e.g. `sort=-emitted_at`). The reference runtime supports the advertised cursor field; unsupported fields, conflicting directions, or sort/order disagreement are rejected with typed errors rather than treated as no-ops."; const COUNT_DESCRIPTION = 'Canonical opt-in count grade (`none`, `estimated`, `exact`). Omit or use `none` for no count. `exact` returns `meta.count.kind="exact"` when supported; `estimated` may be upgraded to an exact count. Counts are page-independent and may be more expensive than the page itself.'; const CHANGES_SINCE_DESCRIPTION = - 'Projection-safe incremental-sync bookmark. Use `beginning` for the initial changes feed, then pass the opaque `next_changes_since` value returned in the prior response. Do not pass an ISO timestamp; malformed bookmarks are rejected as `invalid_cursor`.'; + "Projection-safe incremental-sync bookmark. Use `beginning` for the initial changes feed, then pass the opaque `next_changes_since` value returned in the prior response. Do not pass an ISO timestamp; malformed bookmarks are rejected as `invalid_cursor`."; // Supported range operators, mirroring the RS (`record-filters.js` // SUPPORTED_RANGE_OPERATORS) and the published query contract // (`apps/site/content/docs/spec-data-query-api.md`). -const SUPPORTED_RANGE_OPERATORS = new Set(['gte', 'gt', 'lte', 'lt']); +const SUPPORTED_RANGE_OPERATORS = new Set(["gte", "gt", "lte", "lt"]); // A single exact-filter value. The RS coerces by the field's declared JSON // Schema type, so a scalar is the only meaningful shape; arrays/objects are not @@ -142,7 +139,7 @@ const TypedFilterInput = z.record( lt: FilterScalar.optional(), }) .strict(), - ]), + ]) ); // Thrown when a typed filter object is structurally ambiguous. Surfaced as a @@ -151,16 +148,16 @@ const TypedFilterInput = z.record( class MalformedFilterError extends Error { constructor(message) { super(message); - this.name = 'MalformedFilterError'; - this.code = 'invalid_filter'; + this.name = "MalformedFilterError"; + this.code = "invalid_filter"; } } class MalformedExpandLimitError extends Error { constructor(message) { super(message); - this.name = 'MalformedExpandLimitError'; - this.code = 'invalid_expand'; + this.name = "MalformedExpandLimitError"; + this.code = "invalid_expand"; } } @@ -170,16 +167,14 @@ class MalformedExpandLimitError extends Error { // the canonical fix (consult schema before constructing expand arguments). class UnadvertisedExpandError extends Error { constructor(stream, relations) { - const relClause = relations.length - ? `Relations requested: ${relations.join(', ')}. ` - : ''; + const relClause = relations.length ? `Relations requested: ${relations.join(", ")}. ` : ""; super( `Stream '${stream}' has no advertised expand_capabilities. ${relClause}` + "Consult GET /v1/schema (expand_capabilities) before passing expand; " + - "unadvertised relations are rejected.", + "unadvertised relations are rejected." ); - this.name = 'UnadvertisedExpandError'; - this.code = 'invalid_expand'; + this.name = "UnadvertisedExpandError"; + this.code = "invalid_expand"; } } @@ -189,9 +184,9 @@ class UnadvertisedExpandError extends Error { // to expand-using calls. The connection_id is forwarded so multi-source grants // resolve cleanly. async function assertExpandCapabilities(rs, stream, relations, connectionId) { - const schemaQuery = { view: 'compact', stream }; + const schemaQuery = { view: "compact", stream }; if (connectionId) schemaQuery.connection_id = connectionId; - const schemaResp = await rs.getJson('/v1/schema', { query: schemaQuery }); + const schemaResp = await rs.getJson("/v1/schema", { query: schemaQuery }); if (!schemaResp.ok) return; // schema unavailable — let RS enforce const schemaDoc = unwrapSchemaBody(schemaResp.body); const streams = schemaDoc?.streams ?? []; @@ -214,18 +209,18 @@ async function assertExpandCapabilities(rs, stream, relations, connectionId) { class ConflictingConnectionIdError extends Error { constructor(embedded, explicit) { super( - `id embeds connection_id '${embedded}' but the connection_id argument is '${explicit}'; pass the self-contained id alone, or make both handles agree`, + `id embeds connection_id '${embedded}' but the connection_id argument is '${explicit}'; pass the self-contained id alone, or make both handles agree` ); - this.name = 'ConflictingConnectionIdError'; - this.code = 'conflicting_connection_id'; + this.name = "ConflictingConnectionIdError"; + this.code = "conflicting_connection_id"; } } class InvalidReadRecordFieldSelectorError extends Error { constructor(message) { super(message); - this.name = 'InvalidReadRecordFieldSelectorError'; - this.code = 'invalid_field_window_selector'; + this.name = "InvalidReadRecordFieldSelectorError"; + this.code = "invalid_field_window_selector"; } } @@ -234,28 +229,28 @@ class InvalidReadRecordFieldSelectorError extends Error { function filterObjectToBracketEntries(filter) { if (Object.keys(filter).length === 0) { throw new MalformedFilterError( - 'filter object must include at least one field; omit filter entirely or pass a typed object such as filter: { "field": "value" }', + 'filter object must include at least one field; omit filter entirely or pass a typed object such as filter: { "field": "value" }' ); } const entries = []; for (const [field, spec] of Object.entries(filter)) { - if (field.includes('[') || field.includes(']')) { + if (field.includes("[") || field.includes("]")) { throw new MalformedFilterError( - `filter field '${field}' must be an advertised field name, not pre-encoded bracket syntax; pass filter: { "field": "value" }`, + `filter field '${field}' must be an advertised field name, not pre-encoded bracket syntax; pass filter: { "field": "value" }` ); } if (spec === undefined || spec === null) continue; - if (typeof spec === 'object' && !Array.isArray(spec)) { + if (typeof spec === "object" && !Array.isArray(spec)) { const opEntries = Object.entries(spec).filter(([, v]) => v !== undefined && v !== null); if (opEntries.length === 0) { throw new MalformedFilterError( - `filter range on '${field}' must include at least one of gte/gt/lte/lt; use the typed filter object, e.g. filter: { "${field}": { "gte": } }`, + `filter range on '${field}' must include at least one of gte/gt/lte/lt; use the typed filter object, e.g. filter: { "${field}": { "gte": } }` ); } for (const [op, value] of opEntries) { if (!SUPPORTED_RANGE_OPERATORS.has(op)) { throw new MalformedFilterError( - `unsupported range operator '${op}' on '${field}'; supported operators are gte, gt, lte, lt`, + `unsupported range operator '${op}' on '${field}'; supported operators are gte, gt, lte, lt` ); } entries.push([`filter[${field}][${op}]`, String(value)]); @@ -272,11 +267,11 @@ function filterObjectToBracketEntries(filter) { // RS expects. Returns [] when no filter was supplied. function resolveFilterQueryEntries(filter) { if (filter === undefined || filter === null) return []; - if (typeof filter === 'object' && !Array.isArray(filter)) { + if (typeof filter === "object" && !Array.isArray(filter)) { return filterObjectToBracketEntries(filter); } throw new MalformedFilterError( - 'filter must be a typed object, e.g. filter: { "field": "value" } or filter: { "field": { "gte": } }', + 'filter must be a typed object, e.g. filter: { "field": "value" } or filter: { "field": { "gte": } }' ); } @@ -295,13 +290,13 @@ function applyExpandLimitToQuery(query, expandLimit) { const entries = Object.entries(expandLimit); if (entries.length === 0) { throw new MalformedExpandLimitError( - 'expand_limit must include at least one relation; omit expand_limit entirely when not setting a cap', + "expand_limit must include at least one relation; omit expand_limit entirely when not setting a cap" ); } for (const [relation, limit] of entries) { - if (relation.includes('[') || relation.includes(']')) { + if (relation.includes("[") || relation.includes("]")) { throw new MalformedExpandLimitError( - `expand_limit relation '${relation}' must be a relation name, not pre-encoded bracket syntax; pass expand_limit: { "relation": 3 }`, + `expand_limit relation '${relation}' must be a relation name, not pre-encoded bracket syntax; pass expand_limit: { "relation": 3 }` ); } query[`expand_limit[${relation}]`] = String(limit); @@ -318,7 +313,7 @@ const ConnectionIdInputShape = { // lives at `GET /v1/schema` and in the OpenAPI artifacts published by the // reference-contract package. const CANONICAL_SCHEMA_HINT = - 'Per-stream filter operators, expandable relations, projection support, search modes, count support, granted `connection_id` values, and canonical `connector_key` metadata are advertised by `GET /v1/schema`. Consult it before constructing filter, sort, expand, fields, count, or source-disambiguation arguments.'; + "Per-stream filter operators, expandable relations, projection support, search modes, count support, granted `connection_id` values, and canonical `connector_key` metadata are advertised by `GET /v1/schema`. Consult it before constructing filter, sort, expand, fields, count, or source-disambiguation arguments."; // outputSchema describes the MCP wrapper around the RS response body. We do // NOT bake the RS body shape into the outputSchema because the canonical @@ -329,25 +324,27 @@ const READ_OUTPUT_SCHEMA_SHAPE = { data: z .union([z.record(z.string(), z.unknown()), z.array(z.unknown())]) .describe( - 'Canonical RS response body. Follows the public read envelope advertised by `GET /v1/schema` plus operation-specific extensions; source metadata uses canonical `connector_key` and concrete `connection_id` values when present.', + "Canonical RS response body. Follows the public read envelope advertised by `GET /v1/schema` plus operation-specific extensions; source metadata uses canonical `connector_key` and concrete `connection_id` values when present." ), content_ladder: z.unknown().optional(), - provider_url: z.string().describe('RS base URL the MCP server was configured with.'), - request_id: z.string().nullable().describe('RS x-request-id when present.'), + provider_url: z.string().describe("RS base URL the MCP server was configured with."), + request_id: z.string().nullable().describe("RS x-request-id when present."), }; const SEARCH_OUTPUT_SCHEMA_SHAPE = { ...READ_OUTPUT_SCHEMA_SHAPE, results: z .array( - z.object({ - id: z.string(), - title: z.string(), - url: z.string(), - }).passthrough(), + z + .object({ + id: z.string(), + title: z.string(), + url: z.string(), + }) + .passthrough() ) .describe( - 'ChatGPT-compatible flattened search results. Each entry carries `id` (a self-contained fetch handle, `connection_id/stream:record_id` when the hit has a connection), `title`, `url`, and available source handles such as `connection_id`. Use `data` for compact envelope metadata.', + "ChatGPT-compatible flattened search results. Each entry carries `id` (a self-contained fetch handle, `connection_id/stream:record_id` when the hit has a connection), `title`, `url`, and available source handles such as `connection_id`. Use `data` for compact envelope metadata." ), }; @@ -400,13 +397,13 @@ const DISCOVERY_STREAM_SUMMARY_LIMIT = 50; const DISCOVERY_FIELD_SUMMARY_LIMIT = 16; const DISCOVERY_CONNECTION_SUMMARY_LIMIT = 8; const FIELD_CAPABILITY_FLAG_LEGEND = { - t: 'declared type', - eq: 'exact filter supported', - r: 'range filter operators', - lex: 'lexical search field', - sem: 'semantic search field', - a: 'aggregation capabilities', - 'g=false': 'field is not granted', + t: "declared type", + eq: "exact filter supported", + r: "range filter operators", + lex: "lexical search field", + sem: "semantic search field", + a: "aggregation capabilities", + "g=false": "field is not granted", }; // The `schema` tool's default `structuredContent.data` is a COMPACT projection @@ -423,13 +420,13 @@ const FIELD_CAPABILITY_FLAG_LEGEND = { // openspec/changes/expose-connection-identity-on-public-read/tasks.md (§7 // MCP discovery/schema token-efficiency target). const SCHEMA_DETAIL_DESCRIPTION = - 'Response detail grade for `structuredContent.data`. `compact` (default) returns a token-efficient projection: per-stream field names with capability flags, expandable relation names, connection identities, and connector metadata, with the heavy per-field JSON Schema blobs dropped. `full` returns deduped exhaustive schema for one source, preserving raw per-field JSON Schema while removing duplicate top-level stream arrays; it requires `stream`, and `connection_id` when that stream name is shared. The concise `content[]` text summary is identical for both grades.'; + "Response detail grade for `structuredContent.data`. `compact` (default) returns a token-efficient projection: per-stream field names with capability flags, expandable relation names, connection identities, and connector metadata, with the heavy per-field JSON Schema blobs dropped. `full` returns deduped exhaustive schema for one source, preserving raw per-field JSON Schema while removing duplicate top-level stream arrays; it requires `stream`, and `connection_id` when that stream name is shared. The concise `content[]` text summary is identical for both grades."; const SCHEMA_STREAM_DESCRIPTION = - 'Optional stream name from the compact `schema` stream list. Omit to describe every granted stream. Stream names are not globally unique; pair with `connection_id` when you need one configured source.'; + "Optional stream name from the compact `schema` stream list. Omit to describe every granted stream. Stream names are not globally unique; pair with `connection_id` when you need one configured source."; const SCHEMA_CONNECTION_ID_DESCRIPTION = - 'Optional. Scope schema detail to one configured connection when a stream name is shared by multiple connectors or connections. Obtain from schema results or typed ambiguity errors. This is source identity, not a profile selector.'; + "Optional. Scope schema detail to one configured connection when a stream name is shared by multiple connectors or connections. Obtain from schema results or typed ambiguity errors. This is source identity, not a profile selector."; /** * Resolve the `schema` tool `detail` grade defensively. Absent → the compact @@ -437,8 +434,8 @@ const SCHEMA_CONNECTION_ID_DESCRIPTION = * silently coercing to `compact` (defense-in-depth behind the Zod enum). */ function resolveSchemaDetail(value) { - if (value == null) return 'compact'; - if (value === 'compact' || value === 'full') return value; + if (value == null) return "compact"; + if (value === "compact" || value === "full") return value; throw new Error(`Invalid schema detail: ${JSON.stringify(value)} (expected 'compact' or 'full')`); } @@ -450,22 +447,22 @@ function resolveSchemaDetail(value) { export function buildTools({ rs, providerUrl }) { const tools = [ { - name: 'schema', - title: 'Get PDPP schema', + name: "schema", + title: "Get PDPP schema", description: 'Return the grant-scoped PDPP schema document from `GET /v1/schema`. This is the canonical capability source: streams, canonical connector-type metadata (`connector_key`), per-field filter operators (`field_capabilities`), expandable relations (`expand_capabilities`), projection support, search modes, pagination support, count support, and granted connection identities (`connection_id`, `display_name`). Defaults to a compact, token-efficient projection (`detail: "compact"`) so the `schema -> schema(stream) -> schema(stream, connection_id) -> query_records` discovery path stays cheap. Stream names are not globally unique; add `connection_id` to narrow a shared stream to one configured source. `detail: "full"` is allowed only with `stream` and returns deduped exhaustive schema for matching stream rows, preserving raw per-field JSON Schema without duplicate stream arrays. Call this before issuing other tools to discover valid filter, sort, expand, fields, count, aggregate, stream, and connection-disambiguation arguments. Read-only.', annotations: READ_ONLY_ANNOTATIONS, inputSchema: z .object({ - detail: z.enum(['compact', 'full']).optional().describe(SCHEMA_DETAIL_DESCRIPTION), + detail: z.enum(["compact", "full"]).optional().describe(SCHEMA_DETAIL_DESCRIPTION), stream: z.string().min(1).optional().describe(SCHEMA_STREAM_DESCRIPTION), connection_id: z.string().min(1).optional().describe(SCHEMA_CONNECTION_ID_DESCRIPTION), }) .strict(), outputSchema: z.object(READ_OUTPUT_SCHEMA_SHAPE), handler: async (args) => { - const stream = args?.stream ? requireSafeName(args.stream, 'stream') : null; - const connectionId = args?.connection_id ? requireSafeName(args.connection_id, 'connection_id') : null; + const stream = args?.stream ? requireSafeName(args.stream, "stream") : null; + const connectionId = args?.connection_id ? requireSafeName(args.connection_id, "connection_id") : null; // `detail` is normally constrained by the Zod enum to `compact|full`, // so a direct MCP call can only land here with `'compact'`, `'full'`, // or `undefined` (→ compact default). Resolve it defensively rather @@ -473,9 +470,13 @@ export function buildTools({ rs, providerUrl }) { // (a future enum loosening, or a caller that bypassed the Zod parse) // fails loudly here instead of silently downgrading the response grade. const detail = resolveSchemaDetail(args?.detail); - if (detail === 'compact') { - const compactResponse = await rs.getJson('/v1/schema', { - query: { view: 'compact', ...(stream ? { stream } : {}), ...(connectionId ? { connection_id: connectionId } : {}) }, + if (detail === "compact") { + const compactResponse = await rs.getJson("/v1/schema", { + query: { + view: "compact", + ...(stream ? { stream } : {}), + ...(connectionId ? { connection_id: connectionId } : {}), + }, }); if (compactResponse.ok) { return toSchemaToolResult(compactResponse, providerUrl, { @@ -489,9 +490,9 @@ export function buildTools({ rs, providerUrl }) { return toSchemaToolResult(compactResponse, providerUrl, { detail, stream, connectionId }); } } - const response = await rs.getJson('/v1/schema', { + const response = await rs.getJson("/v1/schema", { query: { - ...(detail === 'full' ? { detail: 'full' } : {}), + ...(detail === "full" ? { detail: "full" } : {}), ...(stream ? { stream } : {}), ...(connectionId ? { connection_id: connectionId } : {}), }, @@ -500,105 +501,108 @@ export function buildTools({ rs, providerUrl }) { }, }, { - name: 'query_records', - title: 'Query PDPP records', + name: "query_records", + title: "Query PDPP records", description: - 'Query records in a stream via `GET /v1/streams/{stream}/records`. Default returns at most 25 records; `limit` is capped at 100 (enforced at input — a REST client that sends `limit>100` gets `limit_clamped` in `meta.warnings[]`). Page forward with `cursor`; narrow with `fields`. `structuredContent.data` carries the machine envelope; with `fields`, record payloads are narrowed to those fields plus required operational handles. `content[]` previews up to the first 5 records. Forwards all args verbatim. ' + + "Query records in a stream via `GET /v1/streams/{stream}/records`. Default returns at most 25 records; `limit` is capped at 100 (enforced at input — a REST client that sends `limit>100` gets `limit_clamped` in `meta.warnings[]`). Page forward with `cursor`; narrow with `fields`. `structuredContent.data` carries the machine envelope; with `fields`, record payloads are narrowed to those fields plus required operational handles. `content[]` previews up to the first 5 records. Forwards all args verbatim. " + CANONICAL_SCHEMA_HINT + - ' Read-only.', + " Read-only.", annotations: READ_ONLY_ANNOTATIONS, inputSchema: z .object({ - stream: z.string().min(1).describe('Stream name advertised by `schema`.'), + stream: z.string().min(1).describe("Stream name advertised by `schema`."), limit: z.number().int().positive().max(100).optional().describe(LIMIT_DESCRIPTION), cursor: z.string().optional(), order: z.string().optional().describe(ORDER_DESCRIPTION), sort: z.string().optional().describe(SORT_DESCRIPTION), - count: z.enum(['none', 'estimated', 'exact']).optional().describe(COUNT_DESCRIPTION), + count: z.enum(["none", "estimated", "exact"]).optional().describe(COUNT_DESCRIPTION), filter: TypedFilterInput.optional().describe(FILTER_DESCRIPTION), fields: z.array(z.string()).optional().describe(FIELDS_DESCRIPTION), view: z.string().optional().describe(VIEW_DESCRIPTION), expand: z.array(z.string()).optional().describe(EXPAND_DESCRIPTION), - expand_limit: z - .record(z.string(), z.number().int().positive()) - .optional() - .describe(EXPAND_LIMIT_DESCRIPTION), + expand_limit: z.record(z.string(), z.number().int().positive()).optional().describe(EXPAND_LIMIT_DESCRIPTION), changes_since: z.string().optional().describe(CHANGES_SINCE_DESCRIPTION), ...ConnectionIdInputShape, }) .strict(), outputSchema: z.object(READ_OUTPUT_SCHEMA_SHAPE), handler: async (args) => { - const stream = requireSafeName(args?.stream, 'stream'); + const stream = requireSafeName(args?.stream, "stream"); if (Array.isArray(args?.expand) && args.expand.length > 0) { await assertExpandCapabilities(rs, stream, args.expand, args?.connection_id); } const query = applyExpandLimitToQuery( applyFilterToQuery(pickQuery(args, SUPPORTED_QUERY_KEYS), args?.filter), - args?.expand_limit, + args?.expand_limit ); const response = await rs.getJson(`/v1/streams/${encodeURIComponent(stream)}/records`, { query, }); - return toToolResult(response, providerUrl, `records from stream "${stream}"`, { - previewRecords: true, - contentLadderStream: stream, - contentLadderConnectionId: args?.connection_id, - }); + return toToolResult(response, providerUrl, `records from stream "${stream}"`, { + previewRecords: true, + contentLadderStream: stream, + contentLadderConnectionId: args?.connection_id, + }); }, }, { - name: 'aggregate', - title: 'Aggregate PDPP records', + name: "aggregate", + title: "Aggregate PDPP records", description: - 'Compute a single-stream aggregation via `GET /v1/streams/{stream}/aggregate`. Prefer over `query_records` when you only need a count, sum, min/max, distinct count, or grouped/time-bucketed rollup — returns small bucket rows, never record bodies. Metrics: `count`, `sum`, `min`, `max`, `count_distinct` (`field` required for all but `count`). Group with one dimension: `group_by` XOR `group_by_time` (requires `granularity`). Grouped responses include `other_count` (records in groups beyond `limit`) so you can detect top-N truncation without a second call. Groupable fields are advertised by `GET /v1/schema`. Forwards args verbatim. ' + + "Compute a single-stream aggregation via `GET /v1/streams/{stream}/aggregate`. Prefer over `query_records` when you only need a count, sum, min/max, distinct count, or grouped/time-bucketed rollup — returns small bucket rows, never record bodies. Metrics: `count`, `sum`, `min`, `max`, `count_distinct` (`field` required for all but `count`). Group with one dimension: `group_by` XOR `group_by_time` (requires `granularity`). Grouped responses include `other_count` (records in groups beyond `limit`) so you can detect top-N truncation without a second call. Groupable fields are advertised by `GET /v1/schema`. Forwards args verbatim. " + CANONICAL_SCHEMA_HINT + - ' Read-only.', + " Read-only.", annotations: READ_ONLY_ANNOTATIONS, inputSchema: z .object({ - stream: z.string().min(1).describe('Stream name advertised by `schema`.'), + stream: z.string().min(1).describe("Stream name advertised by `schema`."), metric: z - .enum(['count', 'sum', 'min', 'max', 'count_distinct']) - .describe('Aggregation metric. `field` is required for sum, min, max, and count_distinct.'), + .enum(["count", "sum", "min", "max", "count_distinct"]) + .describe("Aggregation metric. `field` is required for sum, min, max, and count_distinct."), field: z .string() .min(1) .optional() - .describe('Target field for sum/min/max/count_distinct. Must be declared for the metric in `GET /v1/schema`.'), + .describe( + "Target field for sum/min/max/count_distinct. Must be declared for the metric in `GET /v1/schema`." + ), group_by: z .string() .min(1) .optional() - .describe('Scalar field to group counts by. Mutually exclusive with `group_by_time`.'), + .describe("Scalar field to group counts by. Mutually exclusive with `group_by_time`."), group_by_time: z .string() .min(1) .optional() - .describe('Declared date/date-time field to bucket counts by. Requires `granularity`. Mutually exclusive with `group_by`.'), + .describe( + "Declared date/date-time field to bucket counts by. Requires `granularity`. Mutually exclusive with `group_by`." + ), granularity: z - .enum(['minute', 'hour', 'day', 'week', 'month', 'quarter', 'year']) + .enum(["minute", "hour", "day", "week", "month", "quarter", "year"]) .optional() - .describe('Calendar bucket unit for `group_by_time`. Required with `group_by_time`, forbidden otherwise.'), + .describe("Calendar bucket unit for `group_by_time`. Required with `group_by_time`, forbidden otherwise."), time_zone: z .string() .min(1) .optional() - .describe('IANA time zone for `group_by_time` bucket boundaries. Defaults to UTC; the response echoes the effective zone.'), + .describe( + "IANA time zone for `group_by_time` bucket boundaries. Defaults to UTC; the response echoes the effective zone." + ), limit: z .number() .int() .positive() .max(100) .optional() - .describe('Maximum number of group buckets (1-100). Only valid with `group_by` or `group_by_time`.'), + .describe("Maximum number of group buckets (1-100). Only valid with `group_by` or `group_by_time`."), filter: TypedFilterInput.optional().describe(FILTER_DESCRIPTION), ...ConnectionIdInputShape, }) .strict(), outputSchema: z.object(READ_OUTPUT_SCHEMA_SHAPE), handler: async (args) => { - const stream = requireSafeName(args?.stream, 'stream'); + const stream = requireSafeName(args?.stream, "stream"); const query = applyFilterToQuery(pickQuery(args, SUPPORTED_AGGREGATE_QUERY_KEYS), args?.filter); const response = await rs.getJson(`/v1/streams/${encodeURIComponent(stream)}/aggregate`, { query, @@ -607,18 +611,18 @@ export function buildTools({ rs, providerUrl }) { }, }, { - name: 'search', - title: 'Search PDPP records', + name: "search", + title: "Search PDPP records", description: - 'Search records via `GET /v1/search` (lexical), `/v1/search/semantic`, or `/v1/search/hybrid` per `mode`. Use lexical for exact known terms; semantic is approximate retrieval for conceptual matches. `structuredContent.results` carries the flattened page; `structuredContent.data` carries compact envelope metadata, not a duplicate hit array. Hit ids are self-contained `fetch` handles (the connection is encoded in the id); hits also carry `connection_id` and `connector_key`. Pass `connection_id` to scope, omit to fan in. Page default is 25 hits; `limit` is capped at 100 (enforced at input, and fan-in packages apply it globally). Page forward with `cursor` (lexical/semantic; hybrid does not page). Per-mode capability support is advertised by `GET /v1/schema`. Read-only.', + "Search records via `GET /v1/search` (lexical), `/v1/search/semantic`, or `/v1/search/hybrid` per `mode`. Use lexical for exact known terms; semantic is approximate retrieval for conceptual matches. `structuredContent.results` carries the flattened page; `structuredContent.data` carries compact envelope metadata, not a duplicate hit array. Hit ids are self-contained `fetch` handles (the connection is encoded in the id); hits also carry `connection_id` and `connector_key`. Pass `connection_id` to scope, omit to fan in. Page default is 25 hits; `limit` is capped at 100 (enforced at input, and fan-in packages apply it globally). Page forward with `cursor` (lexical/semantic; hybrid does not page). Per-mode capability support is advertised by `GET /v1/schema`. Read-only.", annotations: READ_ONLY_ANNOTATIONS, inputSchema: z .object({ - q: z.string().min(1).describe('Search query string.'), + q: z.string().min(1).describe("Search query string."), streams: z.array(z.string()).optional(), limit: z.number().int().positive().max(100).optional().describe(SEARCH_LIMIT_DESCRIPTION), cursor: z.string().optional(), - mode: z.enum(['lexical', 'semantic', 'hybrid']).optional(), + mode: z.enum(["lexical", "semantic", "hybrid"]).optional(), filter: TypedFilterInput.optional().describe(FILTER_DESCRIPTION), ...ConnectionIdInputShape, }) @@ -634,29 +638,26 @@ export function buildTools({ rs, providerUrl }) { cursor: args.cursor, connection_id: args.connection_id, }, - args.filter, + args.filter ); const response = await rs.getJson(path, { query }); - return toSearchToolResult(response, providerUrl, { limit: args?.limit, q: args?.q }); + return toSearchToolResult(response, providerUrl, { limit: args?.limit, q: args?.q }); }, }, { - name: 'fetch', - title: 'Fetch PDPP search result', + name: "fetch", + title: "Fetch PDPP search result", description: - 'Fetch a single OpenAI-compatible document by a result id from `search`. Id formats: self-contained `connection_id/stream:record_id` (pass it unchanged — no other argument is needed) or legacy `stream:record_id` plus an optional `connection_id` argument. Both resolve to `GET /v1/streams/{stream}/records/{record_id}`. Returns document fields only (`id`, `title`, `text`, `url`, `metadata`); use `query_records` for canonical PDPP record envelopes. Use `fields` to project the source record before rendering document text/metadata; if the projection excludes every text-like field (`text`, `content`, `body`, `summary`), `text` contains compact JSON for the projected record rather than the full document body. Operational source handles (`id`, stream, `connection_id`, `connector_key`) remain available in `metadata`. On `ambiguous_connection` (409), pick a `connection_id` from `available_connections` in the error and retry. Read-only.', + "Fetch a single OpenAI-compatible document by a result id from `search`. Id formats: self-contained `connection_id/stream:record_id` (pass it unchanged — no other argument is needed) or legacy `stream:record_id` plus an optional `connection_id` argument. Both resolve to `GET /v1/streams/{stream}/records/{record_id}`. Returns document fields only (`id`, `title`, `text`, `url`, `metadata`); use `query_records` for canonical PDPP record envelopes. Use `fields` to project the source record before rendering document text/metadata; if the projection excludes every text-like field (`text`, `content`, `body`, `summary`), `text` contains compact JSON for the projected record rather than the full document body. Operational source handles (`id`, stream, `connection_id`, `connector_key`) remain available in `metadata`. On `ambiguous_connection` (409), pick a `connection_id` from `available_connections` in the error and retry. Read-only.", annotations: READ_ONLY_ANNOTATIONS, inputSchema: z .object({ - id: z - .string() - .min(1) - .describe('ID: `connection_id/stream:record_id`, `stream:record_id`, or `pdpp://record/...` URI.'), + id: z + .string() + .min(1) + .describe("ID: `connection_id/stream:record_id`, `stream:record_id`, or `pdpp://record/...` URI."), expand: z.array(z.string()).optional().describe(EXPAND_DESCRIPTION), - expand_limit: z - .record(z.string(), z.number().int().positive()) - .optional() - .describe(EXPAND_LIMIT_DESCRIPTION), + expand_limit: z.record(z.string(), z.number().int().positive()).optional().describe(EXPAND_LIMIT_DESCRIPTION), fields: z.array(z.string()).optional().describe(FIELDS_DESCRIPTION), ...ConnectionIdInputShape, }) @@ -664,7 +665,7 @@ export function buildTools({ rs, providerUrl }) { outputSchema: z.object(FETCH_OUTPUT_SCHEMA_SHAPE), handler: async (args) => { const ref = parseRecordResultId(args.id); - if (ref.connectionId && typeof args?.connection_id === 'string' && args.connection_id !== ref.connectionId) { + if (ref.connectionId && typeof args?.connection_id === "string" && args.connection_id !== ref.connectionId) { throw new ConflictingConnectionIdError(ref.connectionId, args.connection_id); } if (Array.isArray(args?.expand) && args.expand.length > 0) { @@ -685,32 +686,24 @@ export function buildTools({ rs, providerUrl }) { }, }, { - name: 'read_record_field', - title: 'Read PDPP record field window', - description: 'Read bounded field text by record id or `pdpp://record/...` URI; page by offset, cursor, or q.', + name: "read_record_field", + title: "Read PDPP record field window", + description: "Read bounded field text by record id or `pdpp://record/...` URI; page by offset, cursor, or q.", annotations: READ_ONLY_ANNOTATIONS, inputSchema: z .object({ - id: z - .string() - .min(1) - .optional() - .describe('ID: `connection_id/stream:record_id`, `stream:record_id`, or `pdpp://record/...` URI.'), + id: z + .string() + .min(1) + .optional() + .describe("ID: `connection_id/stream:record_id`, `stream:record_id`, or `pdpp://record/...` URI."), connection_id: z.string().min(1).optional(), stream: z.string().min(1).optional(), record_id: z.string().min(1).optional(), field_path: z.string().min(1), - cursor: z - .string() - .min(1) - .optional(), + cursor: z.string().min(1).optional(), offset_chars: z.number().int().min(0).optional(), - limit_chars: z - .number() - .int() - .min(1) - .max(16384) - .optional(), + limit_chars: z.number().int().min(1).max(16384).optional(), q: z.string().min(1).optional(), before_chars: z.number().int().min(0).max(8192).optional(), after_chars: z.number().int().min(0).max(8192).optional(), @@ -719,7 +712,7 @@ export function buildTools({ rs, providerUrl }) { outputSchema: z.object(READ_RECORD_FIELD_OUTPUT_SCHEMA_SHAPE), handler: async (args) => { const ref = resolveReadRecordFieldRef(args); - const fieldPath = requireSafeName(args?.field_path, 'field_path'); + const fieldPath = requireSafeName(args?.field_path, "field_path"); const query = readRecordFieldQuery(args, ref.connectionId, fieldPath); const response = await rs.getJson( `/v1/streams/${encodeURIComponent(ref.stream)}/records/${encodeURIComponent(ref.recordId)}/field-window`, @@ -739,49 +732,49 @@ export function buildTools({ rs, providerUrl }) { } function searchPathForMode(mode) { - if (mode === 'semantic') return '/v1/search/semantic'; - if (mode === 'hybrid') return '/v1/search/hybrid'; - return '/v1/search'; + if (mode === "semantic") return "/v1/search/semantic"; + if (mode === "hybrid") return "/v1/search/hybrid"; + return "/v1/search"; } function resolveReadRecordFieldRef(args) { - const hasId = typeof args?.id === 'string' && args.id.length > 0; + const hasId = typeof args?.id === "string" && args.id.length > 0; const hasExplicit = args?.connection_id !== undefined || args?.stream !== undefined || args?.record_id !== undefined; if (hasId && hasExplicit) { throw new InvalidReadRecordFieldSelectorError( - '`id` is exclusive with explicit `connection_id`, `stream`, and `record_id`; pass one record identity form' + "`id` is exclusive with explicit `connection_id`, `stream`, and `record_id`; pass one record identity form" ); } if (hasId) { const ref = parseRecordResultId(args.id); if (!ref.connectionId) { throw new InvalidReadRecordFieldSelectorError( - '`id` for read_record_field must include connection_id (`connection_id/stream:record_id`) or use explicit connection_id + stream + record_id' + "`id` for read_record_field must include connection_id (`connection_id/stream:record_id`) or use explicit connection_id + stream + record_id" ); } return ref; } if (!args?.connection_id || !args?.stream || !args?.record_id) { throw new InvalidReadRecordFieldSelectorError( - 'read_record_field requires either `id` + `field_path` or `connection_id` + `stream` + `record_id` + `field_path`' + "read_record_field requires either `id` + `field_path` or `connection_id` + `stream` + `record_id` + `field_path`" ); } return { - connectionId: requireSafeName(args.connection_id, 'connection_id'), - stream: requireSafeName(args.stream, 'stream'), - recordId: requireSafeName(args.record_id, 'record_id'), + connectionId: requireSafeName(args.connection_id, "connection_id"), + stream: requireSafeName(args.stream, "stream"), + recordId: requireSafeName(args.record_id, "record_id"), }; } function readRecordFieldQuery(args, connectionId, fieldPath) { if (args?.cursor !== undefined && args?.offset_chars !== undefined) { - throw new InvalidReadRecordFieldSelectorError('`cursor` is exclusive with `offset_chars`'); + throw new InvalidReadRecordFieldSelectorError("`cursor` is exclusive with `offset_chars`"); } if (args?.q !== undefined && (args?.cursor !== undefined || args?.offset_chars !== undefined)) { - throw new InvalidReadRecordFieldSelectorError('`q` is exclusive with `cursor` and `offset_chars`'); + throw new InvalidReadRecordFieldSelectorError("`q` is exclusive with `cursor` and `offset_chars`"); } if ((args?.before_chars !== undefined || args?.after_chars !== undefined) && args?.q === undefined) { - throw new InvalidReadRecordFieldSelectorError('`before_chars` and `after_chars` require `q`'); + throw new InvalidReadRecordFieldSelectorError("`before_chars` and `after_chars` require `q`"); } const query = { connection_id: connectionId, @@ -808,8 +801,10 @@ function readRecordFieldQuery(args, connectionId, fieldPath) { } function parseFieldWindowCursor(cursor) { - if (typeof cursor !== 'string' || !/^\d+$/.test(cursor)) { - throw new InvalidReadRecordFieldSelectorError('field-window cursor must be a non-negative integer offset returned by read_record_field'); + if (typeof cursor !== "string" || !/^\d+$/.test(cursor)) { + throw new InvalidReadRecordFieldSelectorError( + "field-window cursor must be a non-negative integer offset returned by read_record_field" + ); } return Number.parseInt(cursor, 10); } @@ -824,11 +819,11 @@ export function buildResourceTemplates({ rs, providerUrl }) { function buildRecordResourceTemplate({ rs, providerUrl }) { return { - uriTemplate: 'pdpp://record/{handle}', - name: 'pdpp-record', - title: 'PDPP record', - description: 'Returns one grant-scoped PDPP record through the resource server. Read-only.', - mimeType: 'application/json', + uriTemplate: "pdpp://record/{handle}", + name: "pdpp-record", + title: "PDPP record", + description: "Returns one grant-scoped PDPP record through the resource server. Read-only.", + mimeType: "application/json", read: async (uri, variables) => { const ref = resolveRecordResourceRef(uri, variables); const query = {}; @@ -842,7 +837,7 @@ function buildRecordResourceTemplate({ rs, providerUrl }) { contents: [ { uri, - mimeType: 'application/json', + mimeType: "application/json", text: JSON.stringify(response.body, null, 2), }, ], @@ -855,11 +850,11 @@ function buildRecordResourceTemplate({ rs, providerUrl }) { function buildRecordFieldResourceTemplate({ rs, providerUrl }) { return { - uriTemplate: 'pdpp://field-window/{handle}', - name: 'pdpp-field-window', - title: 'PDPP record field window', - description: 'Returns one bounded text window from an authorized PDPP record field. Read-only.', - mimeType: 'text/plain', + uriTemplate: "pdpp://field-window/{handle}", + name: "pdpp-field-window", + title: "PDPP record field window", + description: "Returns one bounded text window from an authorized PDPP record field. Read-only.", + mimeType: "text/plain", read: async (uri, variables) => { const ref = resolveFieldWindowResourceRef(uri, variables); const query = readRecordFieldQuery(ref, ref.connectionId, ref.field_path); @@ -880,8 +875,8 @@ function buildRecordFieldResourceTemplate({ rs, providerUrl }) { contents: [ { uri, - mimeType: 'text/plain', - text: result.content[0]?.text ?? '', + mimeType: "text/plain", + text: result.content[0]?.text ?? "", }, ], }; @@ -891,12 +886,11 @@ function buildRecordFieldResourceTemplate({ rs, providerUrl }) { export function buildStreamResourceTemplate({ rs, providerUrl }) { return { - uriTemplate: 'pdpp://stream/{name}', - name: 'pdpp-stream', - title: 'PDPP stream metadata', - description: - 'Returns the stream metadata document for a single stream (GET /v1/streams/{name}). Read-only.', - mimeType: 'application/json', + uriTemplate: "pdpp://stream/{name}", + name: "pdpp-stream", + title: "PDPP stream metadata", + description: "Returns the stream metadata document for a single stream (GET /v1/streams/{name}). Read-only.", + mimeType: "application/json", read: async (uri, variables) => { const streamName = resolveStreamName(uri, variables); const response = await rs.getJson(`/v1/streams/${encodeURIComponent(streamName)}`); @@ -905,18 +899,18 @@ export function buildStreamResourceTemplate({ rs, providerUrl }) { contents: [ { uri, - mimeType: 'application/json', + mimeType: "application/json", text: JSON.stringify(response.body, null, 2), }, ], }; } - const error = response.error ?? { type: 'rs_error', code: 'unknown', message: 'Unknown RS error' }; + const error = response.error ?? { type: "rs_error", code: "unknown", message: "Unknown RS error" }; return { contents: [ { uri, - mimeType: 'application/json', + mimeType: "application/json", text: JSON.stringify({ error, provider_url: providerUrl, http_status: response.status }, null, 2), }, ], @@ -926,12 +920,12 @@ export function buildStreamResourceTemplate({ rs, providerUrl }) { } function resourceErrorContents(uri, response, providerUrl) { - const error = response.error ?? { type: 'rs_error', code: 'unknown', message: 'Unknown RS error' }; + const error = response.error ?? { type: "rs_error", code: "unknown", message: "Unknown RS error" }; return { contents: [ { uri, - mimeType: 'application/json', + mimeType: "application/json", text: JSON.stringify({ error, provider_url: providerUrl, http_status: response.status }, null, 2), }, ], @@ -939,28 +933,28 @@ function resourceErrorContents(uri, response, providerUrl) { } function resolveRecordResourceRef(uri, variables) { - const handle = resolveResourceHandle(uri, variables, 'record'); + const handle = resolveResourceHandle(uri, variables, "record"); let payload; try { - payload = decodeResourceHandle(handle, 'record'); + payload = decodeResourceHandle(handle, "record"); } catch (error) { const ref = parseRecordResultId(decodeURIComponent(handle)); return { connectionId: ref.connectionId, stream: ref.stream, recordId: ref.recordId }; } return { - connectionId: requireSafeName(payload.connection_id, 'connection_id'), - stream: requireSafeName(payload.stream, 'stream'), - recordId: requireSafeName(payload.record_id, 'record_id'), + connectionId: requireSafeName(payload.connection_id, "connection_id"), + stream: requireSafeName(payload.stream, "stream"), + recordId: requireSafeName(payload.record_id, "record_id"), }; } function resolveFieldWindowResourceRef(uri, variables) { - const payload = decodeResourceHandle(resolveResourceHandle(uri, variables, 'field-window'), 'field-window'); + const payload = decodeResourceHandle(resolveResourceHandle(uri, variables, "field-window"), "field-window"); const ref = { - connectionId: requireSafeName(payload.connection_id, 'connection_id'), - stream: requireSafeName(payload.stream, 'stream'), - recordId: requireSafeName(payload.record_id, 'record_id'), - field_path: requireSafeName(payload.field_path, 'field_path'), + connectionId: requireSafeName(payload.connection_id, "connection_id"), + stream: requireSafeName(payload.stream, "stream"), + recordId: requireSafeName(payload.record_id, "record_id"), + field_path: requireSafeName(payload.field_path, "field_path"), }; if (payload.cursor !== undefined) ref.cursor = String(payload.cursor); if (payload.offset_chars !== undefined) ref.offset_chars = payload.offset_chars; @@ -973,7 +967,7 @@ function resolveFieldWindowResourceRef(uri, variables) { function resolveResourceHandle(uri, variables, kind) { const rawFromVariables = variables?.handle; - if (typeof rawFromVariables === 'string' && rawFromVariables.length > 0) { + if (typeof rawFromVariables === "string" && rawFromVariables.length > 0) { return decodeIfEncoded(rawFromVariables); } const match = new RegExp(`^pdpp://${kind}/([^/]+)$`).exec(uri); @@ -985,14 +979,14 @@ function resolveResourceHandle(uri, variables, kind) { function resolveStreamName(uri, variables) { const rawFromVariables = variables?.name; - if (typeof rawFromVariables === 'string' && rawFromVariables.length > 0) { - return requireSafeName(decodeIfEncoded(rawFromVariables), 'stream'); + if (typeof rawFromVariables === "string" && rawFromVariables.length > 0) { + return requireSafeName(decodeIfEncoded(rawFromVariables), "stream"); } const match = /^pdpp:\/\/stream\/([^/]+)$/.exec(uri); if (!match) { throw new InvalidResourceUriError(`Resource URI ${uri} does not match pdpp://stream/{name}.`); } - return requireSafeName(decodeURIComponent(match[1]), 'stream'); + return requireSafeName(decodeURIComponent(match[1]), "stream"); } function decodeIfEncoded(value) { @@ -1008,17 +1002,17 @@ function encodeResourceUri(kind, payload) { } function encodeResourceHandle(payload) { - return Buffer.from(JSON.stringify(payload), 'utf8').toString('base64url'); + return Buffer.from(JSON.stringify(payload), "utf8").toString("base64url"); } function decodeResourceHandle(handle, expectedKind) { let payload; try { - payload = JSON.parse(Buffer.from(handle, 'base64url').toString('utf8')); + payload = JSON.parse(Buffer.from(handle, "base64url").toString("utf8")); } catch { throw new InvalidResourceUriError(`Resource handle for ${expectedKind} is malformed.`); } - if (!payload || typeof payload !== 'object' || Array.isArray(payload)) { + if (!payload || typeof payload !== "object" || Array.isArray(payload)) { throw new InvalidResourceUriError(`Resource handle for ${expectedKind} is malformed.`); } if (payload.v !== 1 || payload.kind !== expectedKind) { @@ -1030,38 +1024,38 @@ function decodeResourceHandle(handle, expectedKind) { export class InvalidResourceUriError extends Error { constructor(message) { super(message); - this.name = 'InvalidResourceUriError'; + this.name = "InvalidResourceUriError"; } } function requireSafeName(value, label) { - if (typeof value !== 'string' || value.length === 0) { + if (typeof value !== "string" || value.length === 0) { throw new Error(`${label} is required`); } // Reject path-traversal and slash-bearing inputs. The RS validates names too, but a // defensive check here keeps the resource template URI surface narrow. - if (value.includes('/') || value.includes('\\') || value === '.' || value === '..' || value.includes('..')) { + if (value.includes("/") || value.includes("\\") || value === "." || value === ".." || value.includes("..")) { throw new Error(`${label} contains invalid characters`); } return value; } function pickQuery(args, supportedKeys) { - if (!args || typeof args !== 'object') { + if (!args || typeof args !== "object") { return {}; } const out = {}; for (const key of Object.keys(args)) { - if (key === 'stream') continue; + if (key === "stream") continue; // `filter` is never forwarded as a flat param: the RS expects bracketed // `filter[field]=value` query keys, which callers build via // `applyFilterToQuery`. Forwarding the raw value here would re-introduce the // silent bare-`filter=` no-op this change fixes. - if (key === 'filter') continue; + if (key === "filter") continue; // `expand_limit` mirrors the same nested REST query shape: // `expand_limit[relation]=N`. Forwarding the raw object would become a JSON // string under URLSearchParams instead of the query key the RS parses. - if (key === 'expand_limit') continue; + if (key === "expand_limit") continue; if (!supportedKeys.has(key)) continue; out[key] = args[key]; } @@ -1073,14 +1067,14 @@ function pickQuery(args, supportedKeys) { // See: // openspec/changes/canonicalize-public-read-contract (5.3 prose content[] is // a concise summary only and not a second divergent JSON contract). -function toToolResult(response, providerUrl, label = 'response', options = {}) { +function toToolResult(response, providerUrl, label = "response", options = {}) { if (response.ok) { const body = response.body; const contentLadder = buildResponseContentLadder(body, options); return { content: [ { - type: 'text', + type: "text", text: summarizeBody(body, label, options), }, ], @@ -1101,23 +1095,27 @@ function toToolResult(response, providerUrl, label = 'response', options = {}) { // When `stream`/`connection_id` are supplied, both the summary and the // structured payload are scoped so an agent can fetch one source's capabilities // without pulling the whole document. -function toSchemaToolResult(response, providerUrl, { detail = 'compact', stream = null, connectionId = null, alreadyCompact = false } = {}) { +function toSchemaToolResult( + response, + providerUrl, + { detail = "compact", stream = null, connectionId = null, alreadyCompact = false } = {} +) { if (!response.ok) { return errorToolResult(response, providerUrl); } - const data = detail === 'full' - ? dedupeFullSchemaDocument(response.body) - : compactSchemaDocument(response.body, { includeFieldDetail: Boolean(stream) }); + const data = + detail === "full" + ? dedupeFullSchemaDocument(response.body) + : compactSchemaDocument(response.body, { includeFieldDetail: Boolean(stream) }); const schemaDocument = unwrapSchemaBody(data); return { content: [ { - type: 'text', - text: summarizeSchemaDiscovery( - schemaDocument, - 'PDPP schema', - { includeFieldDetail: Boolean(stream), ...(connectionId ? { connectionId } : {}) }, - ), + type: "text", + text: summarizeSchemaDiscovery(schemaDocument, "PDPP schema", { + includeFieldDetail: Boolean(stream), + ...(connectionId ? { connectionId } : {}), + }), }, ], structuredContent: { data: schemaDocument, provider_url: providerUrl, request_id: response.requestId }, @@ -1125,13 +1123,13 @@ function toSchemaToolResult(response, providerUrl, { detail = 'compact', stream } function isCompactSchemaBody(body) { - return unwrapSchemaBody(body)?.detail === 'compact'; + return unwrapSchemaBody(body)?.detail === "compact"; } function shouldFallbackFromCompactSchemaRequest(response) { if (response.ok) return false; - const code = response.error?.code ?? response.error?.type ?? ''; - return response.status === 400 && ['bad_request', 'invalid_request', 'unsupported_query'].includes(code); + const code = response.error?.code ?? response.error?.type ?? ""; + return response.status === 400 && ["bad_request", "invalid_request", "unsupported_query"].includes(code); } // Compact projection of the schema document. Drops the heavy per-field JSON @@ -1145,9 +1143,9 @@ function shouldFallbackFromCompactSchemaRequest(response) { // schema document, just lighter. function compactSchemaDocument(body, { includeFieldDetail = false } = {}) { const wrapped = - body && typeof body === 'object' && body.data && typeof body.data === 'object' && !Array.isArray(body.data); + body && typeof body === "object" && body.data && typeof body.data === "object" && !Array.isArray(body.data); const schema = unwrapSchemaBody(body); - if (!schema || typeof schema !== 'object') { + if (!schema || typeof schema !== "object") { return body; } const connectors = extractSchemaConnectors(schema); @@ -1167,15 +1165,15 @@ function compactSchemaDocument(body, { includeFieldDetail = false } = {}) { } else { compactSchema = schema; } - compactSchema = { ...compactSchema, detail: 'compact' }; + compactSchema = { ...compactSchema, detail: "compact" }; return wrapped ? { ...body, data: compactSchema } : compactSchema; } function dedupeFullSchemaDocument(body) { const wrapped = - body && typeof body === 'object' && body.data && typeof body.data === 'object' && !Array.isArray(body.data); + body && typeof body === "object" && body.data && typeof body.data === "object" && !Array.isArray(body.data); const schema = unwrapSchemaBody(body); - if (!schema || typeof schema !== 'object') return body; + if (!schema || typeof schema !== "object") return body; const connectors = extractSchemaConnectors(schema); if (connectors.length === 0) return body; const deduped = stripSchemaStreamArrays(schema); @@ -1188,7 +1186,7 @@ function stripSchemaStreamArrays(schema) { } function compactSchemaConnector(connector, { includeFieldDetail = false } = {}) { - if (!connector || typeof connector !== 'object') return connector; + if (!connector || typeof connector !== "object") return connector; const streams = Array.isArray(connector.streams) ? connector.streams : []; const { shared, sharedKey } = pickSharedGrantedConnections(streams); const hasShared = shared !== null; @@ -1203,24 +1201,24 @@ function compactSchemaConnector(connector, { includeFieldDetail = false } = {}) // identity/metadata fields pass through verbatim; `field_capabilities` and // `expand_capabilities` are compacted; everything else is dropped to keep the // payload bounded. -function compactSchemaStream(entry, { hasShared = false, sharedKey = '', includeFieldDetail = false } = {}) { - if (!entry || typeof entry !== 'object') return entry; +function compactSchemaStream(entry, { hasShared = false, sharedKey = "", includeFieldDetail = false } = {}) { + if (!entry || typeof entry !== "object") return entry; const out = {}; const passthrough = [ - 'name', - 'stream', - 'stream_name', - 'connector_key', - 'connector_id', - 'connector_display_name', - 'display_name', - 'connection_display_name', - 'connection_id', - 'record_count', - 'granted', - 'primary_key', - 'cursor_field', - 'source', + "name", + "stream", + "stream_name", + "connector_key", + "connector_id", + "connector_display_name", + "display_name", + "connection_display_name", + "connection_id", + "record_count", + "granted", + "primary_key", + "cursor_field", + "source", ]; for (const key of passthrough) { if (entry[key] !== undefined) out[key] = entry[key]; @@ -1269,20 +1267,20 @@ function compactFieldCapabilities(fieldCapabilities) { function compactExpandCapabilities(expandCapabilities) { if (!Array.isArray(expandCapabilities)) return expandCapabilities; return expandCapabilities.map((relation) => { - if (!relation || typeof relation !== 'object') return relation; + if (!relation || typeof relation !== "object") return relation; const out = {}; for (const key of [ - 'name', - 'relation', - 'stream', - 'target_stream', - 'cardinality', - 'granted', - 'usable', - 'foreign_key', - 'max_limit', - 'default_limit', - 'reason', + "name", + "relation", + "stream", + "target_stream", + "cardinality", + "granted", + "usable", + "foreign_key", + "max_limit", + "default_limit", + "reason", ]) { if (relation[key] !== undefined) out[key] = relation[key]; } @@ -1291,21 +1289,21 @@ function compactExpandCapabilities(expandCapabilities) { } function grantedConnectionsKey(value) { - if (!Array.isArray(value)) return ''; + if (!Array.isArray(value)) return ""; const entries = value.map((entry) => { - if (!entry || typeof entry !== 'object') return JSON.stringify(entry); - const id = typeof entry.connection_id === 'string' ? entry.connection_id : ''; - const label = typeof entry.display_name === 'string' ? entry.display_name : ''; + if (!entry || typeof entry !== "object") return JSON.stringify(entry); + const id = typeof entry.connection_id === "string" ? entry.connection_id : ""; + const label = typeof entry.display_name === "string" ? entry.display_name : ""; return JSON.stringify([id, label]); }); entries.sort(); - return entries.join('\n'); + return entries.join("\n"); } function compactSchemaGrantedConnections(value) { if (!Array.isArray(value)) return value; return value.map((entry) => { - if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return entry; + if (!entry || typeof entry !== "object" || Array.isArray(entry)) return entry; const { connector_instance_id: _deprecatedAlias, ...rest } = entry; return rest; }); @@ -1314,7 +1312,7 @@ function compactSchemaGrantedConnections(value) { function pickSharedGrantedConnections(streams) { const byKey = new Map(); for (const stream of streams) { - if (!stream || typeof stream !== 'object' || !Array.isArray(stream.granted_connections)) continue; + if (!stream || typeof stream !== "object" || !Array.isArray(stream.granted_connections)) continue; if (stream.granted_connections.length === 0) continue; const key = grantedConnectionsKey(stream.granted_connections); const existing = byKey.get(key); @@ -1324,7 +1322,7 @@ function pickSharedGrantedConnections(streams) { byKey.set(key, { value: compactSchemaGrantedConnections(stream.granted_connections), count: 1 }); } } - let bestKey = ''; + let bestKey = ""; let best = null; for (const [key, candidate] of byKey) { if (!best || candidate.count > best.count) { @@ -1332,7 +1330,7 @@ function pickSharedGrantedConnections(streams) { bestKey = key; } } - return { shared: best ? best.value : null, sharedKey: best ? bestKey : '' }; + return { shared: best ? best.value : null, sharedKey: best ? bestKey : "" }; } function toSearchToolResult(response, providerUrl, options = {}) { @@ -1342,15 +1340,13 @@ function toSearchToolResult(response, providerUrl, options = {}) { const allResults = normalizeSearchResults(response.body, { q: options.q, providerUrl }); const limit = requestedSearchLimit(options.limit); const results = allResults.slice(0, limit); - const summaryBody = allResults.length > results.length - ? { ...response.body, has_more: true } - : response.body; + const summaryBody = allResults.length > results.length ? { ...response.body, has_more: true } : response.body; const data = compactSearchEnvelope(summaryBody, { resultCount: results.length }); const contentLadder = buildSearchContentLadder(results); return { content: [ { - type: 'text', + type: "text", text: summarizeSearch(summaryBody, results), }, ], @@ -1382,7 +1378,10 @@ function toReadRecordFieldToolResult(response, providerUrl, identity) { }; const window = { ...rawWindow, - next_cursor: rawWindow.next_offset_chars === null || rawWindow.next_offset_chars === undefined ? null : String(rawWindow.next_offset_chars), + next_cursor: + rawWindow.next_offset_chars === null || rawWindow.next_offset_chars === undefined + ? null + : String(rawWindow.next_offset_chars), previous_cursor: rawWindow.previous_offset_chars === null || rawWindow.previous_offset_chars === undefined ? null @@ -1398,12 +1397,12 @@ function toReadRecordFieldToolResult(response, providerUrl, identity) { `field=${fieldInfo.path}`, `chars=${formatScalar(window.start_chars)}..${formatScalar(window.end_chars)}`, `total_chars=${formatScalar(window.total_chars)}`, - `complete=${window.complete === true ? 'true' : 'false'}`, + `complete=${window.complete === true ? "true" : "false"}`, window.next_cursor ? `next_cursor=${window.next_cursor}` : null, window.previous_cursor ? `previous_cursor=${window.previous_cursor}` : null, ] .filter(Boolean) - .join(' '); + .join(" "); const continuationArgs = (offsetChars) => { const args = { id: record.id, @@ -1429,12 +1428,12 @@ function toReadRecordFieldToolResult(response, providerUrl, identity) { ? `previous read_record_field args=${JSON.stringify(continuationArgs(previousOffsetChars))}` : null, ].filter(Boolean); - const text = typeof window.text === 'string' ? window.text : ''; + const text = typeof window.text === "string" ? window.text : ""; return { content: [ { - type: 'text', - text: [header, ...continuationLines, text].join('\n'), + type: "text", + text: [header, ...continuationLines, text].join("\n"), }, ], structuredContent: { @@ -1453,22 +1452,22 @@ const CONTENT_LADDER_WINDOW_LIMIT_CHARS = 4096; const CONTENT_LADDER_BINARY_FIELD_LIMIT = 5; const BINARY_INLINE_STRING_CHAR_LIMIT = 256; const CONTENT_LADDER_OMIT_FIELD_KEYS = new Set([ - 'id', - 'record_id', - 'recordId', - 'record_key', - 'recordKey', - 'stream', - 'stream_name', - 'streamName', - 'connection_id', - 'connector_instance_id', - 'connector_key', - 'connector_id', - 'display_name', - 'emitted_at', - 'updated_at', - 'created_at', + "id", + "record_id", + "recordId", + "record_key", + "recordKey", + "stream", + "stream_name", + "streamName", + "connection_id", + "connector_instance_id", + "connector_key", + "connector_id", + "display_name", + "emitted_at", + "updated_at", + "created_at", ]); function buildResponseContentLadder(body, options = {}) { @@ -1483,8 +1482,8 @@ function buildResponseContentLadder(body, options = {}) { .slice(0, CONTENT_LADDER_RECORD_LIMIT); if (records.length === 0) return null; return { - kind: 'record_set', - read_tool: 'read_record_field', + kind: "record_set", + read_tool: "read_record_field", records, }; } @@ -1510,8 +1509,8 @@ function buildSearchContentLadder(results) { .slice(0, CONTENT_LADDER_RECORD_LIMIT); if (records.length === 0) return null; return { - kind: 'search_results', - read_tool: 'read_record_field', + kind: "search_results", + read_tool: "read_record_field", records, }; } @@ -1524,8 +1523,8 @@ function mergeSearchMatchWindows(existing, matchWindows, recordId) { field_path: window.field_path, text_like: true, ...(previewText ? { preview_text: previewText } : {}), - preview_status: window.complete === true ? 'complete' : 'truncated', - size_chars: typeof window.text === 'string' ? window.text.length : undefined, + preview_status: window.complete === true ? "complete" : "truncated", + size_chars: typeof window.text === "string" ? window.text.length : undefined, ...(read ? { read } : {}), }; }); @@ -1543,7 +1542,7 @@ function searchEvidenceExcerpts(matchWindows, recordId) { return { field_path: window.field_path, preview_text: previewText, - preview_status: window.complete === true ? 'complete' : 'truncated', + preview_status: window.complete === true ? "complete" : "truncated", ...(read ? { read } : {}), }; }) @@ -1551,8 +1550,8 @@ function searchEvidenceExcerpts(matchWindows, recordId) { } function searchEvidencePreviewText(window) { - if (typeof window?.preview_text === 'string') return window.preview_text; - return typeof window?.text === 'string' ? truncateText(window.text, SEARCH_TEXT_SNIPPET_CHAR_LIMIT) : null; + if (typeof window?.preview_text === "string") return window.preview_text; + return typeof window?.text === "string" ? truncateText(window.text, SEARCH_TEXT_SNIPPET_CHAR_LIMIT) : null; } function searchMatchWindowRead(window, recordId, fieldPath) { @@ -1563,14 +1562,14 @@ function searchMatchWindowRead(window, recordId, fieldPath) { if (!read) { if (!recordId || !fieldPath) return null; return { - tool: 'read_record_field', + tool: "read_record_field", args: searchMatchWindowReadArgs({}, recordId, fieldPath), }; } const args = searchMatchWindowReadArgs(read.args, recordId, fieldPath); if (!firstString(args.id, args.record_uri)) return null; return { - tool: read.tool ?? 'read_record_field', + tool: read.tool ?? "read_record_field", args, }; } @@ -1581,7 +1580,8 @@ function searchMatchWindowReadArgs(args, recordId, fieldPath) { const connectionId = firstString(rawArgs.connection_id, rawArgs.connector_instance_id); const stream = firstString(rawArgs.stream); const rawRecordId = firstString(rawArgs.record_id, rawArgs.recordId, rawArgs.record_key, rawArgs.recordKey); - const id = connectionId && stream && rawRecordId ? selfContainedResultId(`${stream}:${rawRecordId}`, connectionId) : recordId; + const id = + connectionId && stream && rawRecordId ? selfContainedResultId(`${stream}:${rawRecordId}`, connectionId) : recordId; return { id, field_path: firstString(rawArgs.field_path, rawArgs.fieldPath, rawArgs.path) ?? fieldPath, @@ -1597,7 +1597,9 @@ function searchMatchWindowReadArgs(args, recordId, fieldPath) { } function definedObject(values) { - return Object.fromEntries(Object.entries(values).filter(([, value]) => value !== undefined && value !== null && value !== '')); + return Object.fromEntries( + Object.entries(values).filter(([, value]) => value !== undefined && value !== null && value !== "") + ); } function hideModelVisibleResourceUris(record) { @@ -1606,7 +1608,9 @@ function hideModelVisibleResourceUris(record) { if (!Array.isArray(rest.field_windows)) return rest; return { ...rest, - field_windows: rest.field_windows.map(({ resource_uri: _resourceUri, resourceUri: _resourceUriCamel, ...field }) => field), + field_windows: rest.field_windows.map( + ({ resource_uri: _resourceUri, resourceUri: _resourceUriCamel, ...field }) => field + ), }; } @@ -1617,8 +1621,8 @@ function buildFetchContentLadder(recordBody, document) { }); if (!record) return null; return { - kind: 'record', - read_tool: 'read_record_field', + kind: "record", + read_tool: "read_record_field", ...record, }; } @@ -1644,7 +1648,7 @@ function toFetchToolResult(response, providerUrl, requestedId) { return { content: [ { - type: 'text', + type: "text", text, }, ], @@ -1667,7 +1671,7 @@ function toAggregateToolResult(response, providerUrl, stream) { return { content: [ { - type: 'text', + type: "text", text: summarizeAggregate(response.body, stream), }, ], @@ -1679,14 +1683,14 @@ const AGGREGATE_GROUP_PREVIEW_LIMIT = 5; function summarizeAggregate(body, stream) { const agg = unwrapAggregateBody(body); - const metric = typeof agg.metric === 'string' && agg.metric.length > 0 ? agg.metric : 'aggregate'; - const field = typeof agg.field === 'string' && agg.field.length > 0 ? ` field=${agg.field}` : ''; + const metric = typeof agg.metric === "string" && agg.metric.length > 0 ? agg.metric : "aggregate"; + const field = typeof agg.field === "string" && agg.field.length > 0 ? ` field=${agg.field}` : ""; const head = `${metric}(${stream})${field}`; const groups = Array.isArray(agg.groups) ? agg.groups : null; if (groups) { const timeZone = firstString(agg.effective_time_zone, agg.time_zone); - const timeZoneSuffix = agg.group_by_time && timeZone ? ` time_zone=${formatScalar(timeZone)}` : ''; + const timeZoneSuffix = agg.group_by_time && timeZone ? ` time_zone=${formatScalar(timeZone)}` : ""; const dimension = agg.group_by_time ? `group_by_time=${formatScalar(agg.group_by_time)} granularity=${formatScalar(agg.granularity)}${timeZoneSuffix}` : `group_by=${formatScalar(agg.group_by)}`; @@ -1694,13 +1698,16 @@ function summarizeAggregate(body, stream) { return `${head} ${dimension}: 0 group(s). See structuredContent.data for the canonical envelope.`; } const shown = groups.slice(0, AGGREGATE_GROUP_PREVIEW_LIMIT).map((g) => { - const key = g && typeof g === 'object' ? g.key : g; - const count = g && typeof g === 'object' ? g.count : undefined; - return `${formatScalar(key)}=${count == null ? '?' : count}`; + const key = g && typeof g === "object" ? g.key : g; + const count = g && typeof g === "object" ? g.count : undefined; + return `${formatScalar(key)}=${count == null ? "?" : count}`; }); - const more = groups.length > AGGREGATE_GROUP_PREVIEW_LIMIT ? ` more_groups=${groups.length - AGGREGATE_GROUP_PREVIEW_LIMIT};` : ''; - const otherCount = typeof agg.other_count === 'number' ? ` other_count=${agg.other_count};` : ''; - return `${head} ${dimension}: ${groups.length} group(s) [${shown.join(', ')}]${more}${otherCount} canonical envelope in structuredContent.data`; + const more = + groups.length > AGGREGATE_GROUP_PREVIEW_LIMIT + ? ` more_groups=${groups.length - AGGREGATE_GROUP_PREVIEW_LIMIT};` + : ""; + const otherCount = typeof agg.other_count === "number" ? ` other_count=${agg.other_count};` : ""; + return `${head} ${dimension}: ${groups.length} group(s) [${shown.join(", ")}]${more}${otherCount} canonical envelope in structuredContent.data`; } // Ungrouped: the scalar answer lives in `value`. Fall back to @@ -1713,25 +1720,25 @@ function summarizeAggregate(body, stream) { // count/sum/min/max case) so the text reads as the numeric result; strings are // quoted for disambiguation. function formatAggregateValue(value) { - if (value === undefined || value === null) return 'null'; - if (typeof value === 'number' && Number.isFinite(value)) return String(value); + if (value === undefined || value === null) return "null"; + if (typeof value === "number" && Number.isFinite(value)) return String(value); return formatScalar(value); } function unwrapAggregateBody(body) { - if (!body || typeof body !== 'object') return {}; - if (body.object === 'aggregation') return body; - if (body.data && typeof body.data === 'object' && !Array.isArray(body.data)) { + if (!body || typeof body !== "object") return {}; + if (body.object === "aggregation") return body; + if (body.data && typeof body.data === "object" && !Array.isArray(body.data)) { return body.data; } return body; } function summarizeBody(body, label, options = {}) { - if (label === 'PDPP schema') { + if (label === "PDPP schema") { return summarizeSchemaDiscovery(body, label); } - if (label === 'PDPP streams') { + if (label === "PDPP streams") { return summarizeStreamsDiscovery(body, label); } if (options.previewRecords) { @@ -1740,7 +1747,7 @@ function summarizeBody(body, label, options = {}) { if (Array.isArray(body)) { return `${label}: ${body.length} item(s). See structuredContent.data for the canonical envelope.`; } - if (body && typeof body === 'object') { + if (body && typeof body === "object") { const dataLen = Array.isArray(body.data) ? body.data.length : Array.isArray(body.records) @@ -1748,7 +1755,7 @@ function summarizeBody(body, label, options = {}) { : Array.isArray(body.streams) ? body.streams.length : null; - const hasMore = body.has_more === true ? ' has_more=true.' : ''; + const hasMore = body.has_more === true ? " has_more=true." : ""; if (dataLen !== null) { return `${label}: ${dataLen} item(s).${hasMore} See structuredContent.data for the canonical envelope.`; } @@ -1764,12 +1771,11 @@ const RECORD_PREVIEW_LIMIT = 5; const RECORD_PREVIEW_CHAR_LIMIT = 1792; const RECORD_PREVIEW_FOOTER_RESERVE = 96; const RECORD_PREVIEW_MIN_RECORD_CHARS = 24; -const RECORD_PREVIEW_TRUNCATED_MARKER = - 'record_preview_truncated=true; machine envelope in structuredContent.data'; +const RECORD_PREVIEW_TRUNCATED_MARKER = "record_preview_truncated=true; machine envelope in structuredContent.data"; function summarizeRecordEnvelope(body, label) { const records = extractRecordRows(body); - const hasMore = envelopeField(body, 'has_more') === true ? ' has_more=true.' : ''; + const hasMore = envelopeField(body, "has_more") === true ? " has_more=true." : ""; const handles = formatRecordEnvelopeHandles(body); if (records.length === 0) { return `${label}: 0 record(s).${handles}`; @@ -1795,26 +1801,26 @@ function summarizeRecordEnvelope(body, label) { } else if (records.length > RECORD_PREVIEW_LIMIT) { lines.push(`more_records=${records.length - RECORD_PREVIEW_LIMIT}; machine envelope in structuredContent.data`); } - return lines.join('\n'); + return lines.join("\n"); } function formatRecordEnvelopeHandles(body) { const parts = []; - const nextCursor = envelopeStringField(body, 'next_cursor'); - const nextChangesSince = envelopeStringField(body, 'next_changes_since'); + const nextCursor = envelopeStringField(body, "next_cursor"); + const nextChangesSince = envelopeStringField(body, "next_changes_since"); if (nextCursor) parts.push(`next_cursor=${formatScalar(nextCursor)}`); if (nextChangesSince) parts.push(`next_changes_since=${formatScalar(nextChangesSince)}`); const count = envelopeCount(body); if (count) parts.push(`count=${count}`); - return parts.length > 0 ? ` ${parts.join(' ')}.` : ''; + return parts.length > 0 ? ` ${parts.join(" ")}.` : ""; } function extractRecordRows(body) { if (Array.isArray(body)) return body; - if (!body || typeof body !== 'object') return []; + if (!body || typeof body !== "object") return []; if (Array.isArray(body.records)) return body.records; if (Array.isArray(body.data)) return body.data; - if (body.data && typeof body.data === 'object' && Array.isArray(body.data.records)) { + if (body.data && typeof body.data === "object" && Array.isArray(body.data.records)) { return body.data.records; } return []; @@ -1826,13 +1832,11 @@ function summarizeStreamsDiscovery(body, label) { return `${label}: 0 stream(s)`; } - const lines = streams - .slice(0, DISCOVERY_STREAM_SUMMARY_LIMIT) - .map((stream) => formatStreamListSummary(stream)); + const lines = streams.slice(0, DISCOVERY_STREAM_SUMMARY_LIMIT).map((stream) => formatStreamListSummary(stream)); if (streams.length > DISCOVERY_STREAM_SUMMARY_LIMIT) { lines.push(`more_streams=${streams.length - DISCOVERY_STREAM_SUMMARY_LIMIT}`); } - return `${label}: ${streams.length} stream(s)\n${lines.join('\n')}`; + return `${label}: ${streams.length} stream(s)\n${lines.join("\n")}`; } // When the package-level schema spans many streams, the per-field flag segment @@ -1854,15 +1858,13 @@ function summarizeSchemaDiscovery(body, label, { includeFieldDetail, connectionI return `${label}: connectors=${connectorCount} streams=${streamNames.length}\n${streamNames .slice(0, DISCOVERY_STREAM_SUMMARY_LIMIT) .map((name) => `stream name=${formatScalar(name)}`) - .join('\n')}`; + .join("\n")}`; } return `${label}: connectors=${connectorCount} streams=0`; } const withFields = includeFieldDetail ?? streamRefs.length <= 1; - const indexLines = streamRefs.length > DISCOVERY_STREAM_SUMMARY_LIMIT - ? formatSchemaStreamIndex(streamRefs) - : []; + const indexLines = streamRefs.length > DISCOVERY_STREAM_SUMMARY_LIMIT ? formatSchemaStreamIndex(streamRefs) : []; const legendLines = withFields ? [formatFieldCapabilityLegend()] : []; const scopedLines = connectionId ? [`schema_scope connection_id=${formatScalar(connectionId)}`] : []; const lines = streamRefs @@ -1872,24 +1874,24 @@ function summarizeSchemaDiscovery(body, label, { includeFieldDetail, connectionI lines.push(`more_streams=${streamRefs.length - DISCOVERY_STREAM_SUMMARY_LIMIT}`); } const hint = withFields - ? '' - : '\ncall schema(stream, connection_id?) for per-field capability flags (filter/sort/expand/fields/count/aggregate)'; + ? "" + : "\ncall schema(stream, connection_id?) for per-field capability flags (filter/sort/expand/fields/count/aggregate)"; return `${label}: connectors=${connectorCount} streams=${streamRefs.length}\n${[ ...legendLines, ...scopedLines, ...indexLines, ...lines, - ].join('\n')}${hint}`; + ].join("\n")}${hint}`; } function formatFieldCapabilityLegend() { - return 'field_capability_legend t=declared_type eq=exact_filter r=range_filter_ops lex=lexical_search sem=semantic_search a=aggregation_caps g=false=not_granted'; + return "field_capability_legend t=declared_type eq=exact_filter r=range_filter_ops lex=lexical_search sem=semantic_search a=aggregation_caps g=false=not_granted"; } function formatSchemaStreamIndex(streamRefs) { const byConnector = new Map(); for (const { stream, connector } of streamRefs) { - const connectorKey = connectorKeyFor(stream, connector) || 'unknown'; + const connectorKey = connectorKeyFor(stream, connector) || "unknown"; const name = streamName(stream); if (!name) continue; if (!byConnector.has(connectorKey)) { @@ -1900,31 +1902,32 @@ function formatSchemaStreamIndex(streamRefs) { names.push(name); } } - return [...byConnector.entries()].map(([connectorKey, names]) => - `stream_index connector_key=${formatScalar(connectorKey)} stream_count=${names.length} streams=${names.map(formatInlineValue).join('|')}`, + return [...byConnector.entries()].map( + ([connectorKey, names]) => + `stream_index connector_key=${formatScalar(connectorKey)} stream_count=${names.length} streams=${names.map(formatInlineValue).join("|")}` ); } function extractListRows(body) { if (Array.isArray(body)) return body; - if (!body || typeof body !== 'object') return []; + if (!body || typeof body !== "object") return []; if (Array.isArray(body.data)) return body.data; if (Array.isArray(body.streams)) return body.streams; - if (body.data && typeof body.data === 'object' && Array.isArray(body.data.streams)) { + if (body.data && typeof body.data === "object" && Array.isArray(body.data.streams)) { return body.data.streams; } return []; } function unwrapSchemaBody(body) { - if (!body || typeof body !== 'object') return {}; - if (body.data && typeof body.data === 'object' && !Array.isArray(body.data)) { + if (!body || typeof body !== "object") return {}; + if (body.data && typeof body.data === "object" && !Array.isArray(body.data)) { const data = body.data; if ( Array.isArray(data.connectors) || Array.isArray(data.streams) || Array.isArray(data.granted_connections) || - data.object === 'schema' + data.object === "schema" ) { return data; } @@ -1933,7 +1936,7 @@ function unwrapSchemaBody(body) { } function extractSchemaConnectors(schema) { - return Array.isArray(schema?.connectors) ? schema.connectors.filter((item) => item && typeof item === 'object') : []; + return Array.isArray(schema?.connectors) ? schema.connectors.filter((item) => item && typeof item === "object") : []; } function extractSchemaStreamRefs(schema) { @@ -1946,31 +1949,25 @@ function extractSchemaStreamRefs(schema) { } const streams = Array.isArray(schema?.streams) ? schema.streams : []; return streams - .filter((stream) => stream && typeof stream === 'object') + .filter((stream) => stream && typeof stream === "object") .map((stream) => ({ stream, connector: null })); } function extractSchemaStreamNames(schema) { const streams = Array.isArray(schema?.streams) ? schema.streams : []; - return streams - .map((stream) => streamName(stream)) - .filter(Boolean); + return streams.map((stream) => streamName(stream)).filter(Boolean); } function formatStreamListSummary(stream) { const source = objectValue(stream?.source); - const name = streamName(stream) || 'unknown'; - const connectionId = firstString( - stream?.connection_id, - stream?.connector_instance_id, - source?.connection_id, - ); + const name = streamName(stream) || "unknown"; + const connectionId = firstString(stream?.connection_id, stream?.connector_instance_id, source?.connection_id); const connectorKey = connectorKeyFor(stream, null); const displayName = firstString( stream?.display_name, stream?.connection_display_name, source?.display_name, - stream?.connector_display_name, + stream?.connector_display_name ); const parts = [ `stream name=${formatScalar(name)}`, @@ -1982,11 +1979,11 @@ function formatStreamListSummary(stream) { if (recordCount !== null) { parts.push(`record_count=${recordCount}`); } - return parts.join(' '); + return parts.join(" "); } function formatSchemaStreamSummary(stream, connector, { includeFieldDetail = true } = {}) { - const name = streamName(stream) || 'unknown'; + const name = streamName(stream) || "unknown"; const connectorKey = connectorKeyFor(stream, connector); const displayName = displayNameFor(stream, connector); const connections = grantedConnectionsFor(stream, connector); @@ -1999,15 +1996,15 @@ function formatSchemaStreamSummary(stream, connector, { includeFieldDetail = tru if (includeFieldDetail) { parts.push(`fields=${formatFieldCapabilities(stream?.field_capabilities)}`); const aggregations = formatAggregationCapabilities(stream?.field_capabilities); - if (aggregations !== 'none') { + if (aggregations !== "none") { parts.push(`aggregations=${aggregations}`); } } - return parts.join(' '); + return parts.join(" "); } function streamName(stream) { - if (typeof stream === 'string' && stream.length > 0) return stream; + if (typeof stream === "string" && stream.length > 0) return stream; return firstString(stream?.name, stream?.stream, stream?.stream_name, stream?.streamName); } @@ -2024,7 +2021,7 @@ function connectorKeyFor(stream, connector) { connector?.connector_id, connectorSource?.connector_key, connectorSource?.connector_id, - connectorSource?.id, + connectorSource?.id ); } @@ -2038,18 +2035,18 @@ function displayNameFor(stream, connector) { connector?.display_name, connectorSource?.display_name, connector?.connector_display_name, - stream?.connector_display_name, + stream?.connector_display_name ); } function grantedConnectionsFor(stream, connector) { const explicit = Array.isArray(stream?.granted_connections) ? stream.granted_connections : []; if (explicit.length > 0) { - return explicit.filter((connection) => connection && typeof connection === 'object'); + return explicit.filter((connection) => connection && typeof connection === "object"); } const shared = Array.isArray(connector?.granted_connections) ? connector.granted_connections : []; if (shared.length > 0) { - return shared.filter((connection) => connection && typeof connection === 'object'); + return shared.filter((connection) => connection && typeof connection === "object"); } const source = objectValue(stream?.source); @@ -2065,27 +2062,29 @@ function grantedConnectionsFor(stream, connector) { } function formatConnections(connections) { - if (!connections || connections.length === 0) return 'none'; - const rendered = connections - .slice(0, DISCOVERY_CONNECTION_SUMMARY_LIMIT) - .map((connection) => { - const id = firstString(connection?.connection_id, connection?.connector_instance_id); - const displayName = firstString(connection?.display_name, connection?.name); - const connectorKey = firstString(connection?.connector_key, connection?.connector_id, objectValue(connection?.source)?.connector_key); - const parts = [`connection_id:${formatInlineValue(id)}`]; - if (displayName) parts.push(`display_name:${formatInlineValue(displayName)}`); - if (connectorKey) parts.push(`connector_key:${formatInlineValue(connectorKey)}`); - return `{${parts.join(',')}}`; - }); + if (!connections || connections.length === 0) return "none"; + const rendered = connections.slice(0, DISCOVERY_CONNECTION_SUMMARY_LIMIT).map((connection) => { + const id = firstString(connection?.connection_id, connection?.connector_instance_id); + const displayName = firstString(connection?.display_name, connection?.name); + const connectorKey = firstString( + connection?.connector_key, + connection?.connector_id, + objectValue(connection?.source)?.connector_key + ); + const parts = [`connection_id:${formatInlineValue(id)}`]; + if (displayName) parts.push(`display_name:${formatInlineValue(displayName)}`); + if (connectorKey) parts.push(`connector_key:${formatInlineValue(connectorKey)}`); + return `{${parts.join(",")}}`; + }); if (connections.length > DISCOVERY_CONNECTION_SUMMARY_LIMIT) { rendered.push(`more:${connections.length - DISCOVERY_CONNECTION_SUMMARY_LIMIT}`); } - return rendered.join('|'); + return rendered.join("|"); } function formatFieldCapabilities(fieldCapabilities) { const entries = fieldCapabilityEntries(fieldCapabilities); - if (entries.length === 0) return 'none'; + if (entries.length === 0) return "none"; const rendered = entries .slice(0, DISCOVERY_FIELD_SUMMARY_LIMIT) @@ -2093,11 +2092,11 @@ function formatFieldCapabilities(fieldCapabilities) { if (entries.length > DISCOVERY_FIELD_SUMMARY_LIMIT) { rendered.push(`more:${entries.length - DISCOVERY_FIELD_SUMMARY_LIMIT}`); } - return rendered.join(';'); + return rendered.join(";"); } function fieldCapabilityEntries(fieldCapabilities) { - if (!fieldCapabilities || typeof fieldCapabilities !== 'object') return []; + if (!fieldCapabilities || typeof fieldCapabilities !== "object") return []; if (Array.isArray(fieldCapabilities)) { return fieldCapabilities .map((entry) => { @@ -2110,27 +2109,28 @@ function fieldCapabilityEntries(fieldCapabilities) { } function formatFieldCapabilityFlags(capabilities) { - if (typeof capabilities === 'string' && capabilities.length > 0) return capabilities; - if (!capabilities || typeof capabilities !== 'object') return 'declared'; - if (typeof capabilities.flags === 'string' && capabilities.flags.length > 0) return capabilities.flags; + if (typeof capabilities === "string" && capabilities.length > 0) return capabilities; + if (!capabilities || typeof capabilities !== "object") return "declared"; + if (typeof capabilities.flags === "string" && capabilities.flags.length > 0) return capabilities.flags; const flags = []; const schema = objectValue(capabilities.schema); const type = firstString(capabilities.type, schemaType(schema)); if (type) flags.push(`t=${formatInlineValue(type)}`); - if (typeof capabilities.role === 'string' && capabilities.role.length > 0) flags.push(`role=${formatInlineValue(capabilities.role)}`); + if (typeof capabilities.role === "string" && capabilities.role.length > 0) + flags.push(`role=${formatInlineValue(capabilities.role)}`); if (capabilities.granted === false) { - flags.push('g=false'); + flags.push("g=false"); } - addCapabilityFlag(flags, 'eq', capabilities.exact_filter); + addCapabilityFlag(flags, "eq", capabilities.exact_filter); addRangeCapabilityFlag(flags, capabilities.range_filter); - addCapabilityFlag(flags, 'lex', capabilities.lexical_search); - addCapabilityFlag(flags, 'sem', capabilities.semantic_search); + addCapabilityFlag(flags, "lex", capabilities.lexical_search); + addCapabilityFlag(flags, "sem", capabilities.semantic_search); addAggregationCapabilityFlags(flags, capabilities.aggregation); - return flags.length > 0 ? flags.join(',') : 'declared'; + return flags.length > 0 ? flags.join(",") : "declared"; } function addCapabilityFlag(flags, name, capability) { - if (!capability || typeof capability !== 'object') return; + if (!capability || typeof capability !== "object") return; if (capability.usable === true) { flags.push(name); } else if (capability.declared === true && capability.usable === false) { @@ -2139,33 +2139,34 @@ function addCapabilityFlag(flags, name, capability) { } function addRangeCapabilityFlag(flags, capability) { - if (!capability || typeof capability !== 'object') return; - const operators = Array.isArray(capability.operators) && capability.operators.length > 0 - ? capability.operators.join('|') - : null; + if (!capability || typeof capability !== "object") return; + const operators = + Array.isArray(capability.operators) && capability.operators.length > 0 ? capability.operators.join("|") : null; if (capability.usable === true) { - flags.push(operators ? `r=${formatInlineValue(operators)}` : 'r'); + flags.push(operators ? `r=${formatInlineValue(operators)}` : "r"); } else if (capability.declared === true && capability.usable === false) { flags.push(`r=unusable${reasonSuffix(capability.reason)}`); } } function addAggregationCapabilityFlags(flags, aggregation) { - if (!aggregation || typeof aggregation !== 'object') return; - const usable = orderedAggregationKinds(Object.entries(aggregation) - .filter(([, capability]) => capability && typeof capability === 'object' && capability.usable === true) - .map(([name]) => name)); + if (!aggregation || typeof aggregation !== "object") return; + const usable = orderedAggregationKinds( + Object.entries(aggregation) + .filter(([, capability]) => capability && typeof capability === "object" && capability.usable === true) + .map(([name]) => name) + ); if (usable.length > 0) { - flags.push(`a=${formatInlineValue(usable.join('|'))}`); + flags.push(`a=${formatInlineValue(usable.join("|"))}`); } } -const AGGREGATION_SUMMARY_KINDS = ['count_distinct', 'group_by', 'group_by_time', 'sum', 'min', 'max']; +const AGGREGATION_SUMMARY_KINDS = ["count_distinct", "group_by", "group_by_time", "sum", "min", "max"]; const AGGREGATION_FIELD_SUMMARY_LIMIT = 12; function formatAggregationCapabilities(fieldCapabilities) { const entries = fieldCapabilityEntries(fieldCapabilities); - if (entries.length === 0) return 'none'; + if (entries.length === 0) return "none"; const byKind = new Map(AGGREGATION_SUMMARY_KINDS.map((kind) => [kind, []])); for (const [field, capabilities] of entries) { for (const kind of aggregationKindsForField(capabilities)) { @@ -2179,27 +2180,35 @@ function formatAggregationCapabilities(fieldCapabilities) { const fields = byKind.get(kind) || []; if (fields.length === 0) continue; const shown = fields.slice(0, AGGREGATION_FIELD_SUMMARY_LIMIT); - const more = fields.length > AGGREGATION_FIELD_SUMMARY_LIMIT ? `|more:${fields.length - AGGREGATION_FIELD_SUMMARY_LIMIT}` : ''; - parts.push(`${kind}=${shown.join('|')}${more}`); + const more = + fields.length > AGGREGATION_FIELD_SUMMARY_LIMIT ? `|more:${fields.length - AGGREGATION_FIELD_SUMMARY_LIMIT}` : ""; + parts.push(`${kind}=${shown.join("|")}${more}`); } - return parts.length > 0 ? parts.join(';') : 'none'; + return parts.length > 0 ? parts.join(";") : "none"; } function aggregationKindsForField(capabilities) { - if (typeof capabilities === 'string') return aggregationKindsFromFlags(capabilities); - if (!capabilities || typeof capabilities !== 'object') return []; - if (typeof capabilities.flags === 'string') return aggregationKindsFromFlags(capabilities.flags); + if (typeof capabilities === "string") return aggregationKindsFromFlags(capabilities); + if (!capabilities || typeof capabilities !== "object") return []; + if (typeof capabilities.flags === "string") return aggregationKindsFromFlags(capabilities.flags); const aggregation = objectValue(capabilities.aggregation); if (!aggregation) return []; - return orderedAggregationKinds(Object.entries(aggregation) - .filter(([, capability]) => capability && typeof capability === 'object' && capability.usable === true) - .map(([kind]) => kind)); + return orderedAggregationKinds( + Object.entries(aggregation) + .filter(([, capability]) => capability && typeof capability === "object" && capability.usable === true) + .map(([kind]) => kind) + ); } function aggregationKindsFromFlags(flags) { const match = /(?:^|,)a=([^,]+)/.exec(flags); if (!match) return []; - return orderedAggregationKinds(match[1].split('|').map((part) => part.trim()).filter(Boolean)); + return orderedAggregationKinds( + match[1] + .split("|") + .map((part) => part.trim()) + .filter(Boolean) + ); } function orderedAggregationKinds(kinds) { @@ -2211,42 +2220,44 @@ function orderedAggregationKinds(kinds) { } function reasonSuffix(reason) { - return typeof reason === 'string' && reason.length > 0 ? `:${reason}` : ''; + return typeof reason === "string" && reason.length > 0 ? `:${reason}` : ""; } function schemaType(schema) { - if (!schema || typeof schema !== 'object') return undefined; - if (typeof schema.type === 'string') return schema.type; - if (Array.isArray(schema.type)) return schema.type.filter((item) => typeof item === 'string').join('|') || undefined; + if (!schema || typeof schema !== "object") return undefined; + if (typeof schema.type === "string") return schema.type; + if (Array.isArray(schema.type)) return schema.type.filter((item) => typeof item === "string").join("|") || undefined; return undefined; } function objectValue(value) { - return value && typeof value === 'object' && !Array.isArray(value) ? value : null; + return value && typeof value === "object" && !Array.isArray(value) ? value : null; } function firstString(...values) { for (const value of values) { - if (typeof value === 'string' && value.length > 0) return value; + if (typeof value === "string" && value.length > 0) return value; } return undefined; } function numberValue(value) { - return typeof value === 'number' && Number.isFinite(value) ? value : null; + return typeof value === "number" && Number.isFinite(value) ? value : null; } function formatScalar(value) { - return value === undefined || value === null ? 'null' : JSON.stringify(String(value)); + return value === undefined || value === null ? "null" : JSON.stringify(String(value)); } function formatInlineValue(value) { - if (value === undefined || value === null) return 'null'; - return String(value).replace(/[;,\[\]{}]/g, '_').replace(/\s+/g, '_'); + if (value === undefined || value === null) return "null"; + return String(value) + .replace(/[;,\[\]{}]/g, "_") + .replace(/\s+/g, "_"); } function formatFieldName(value) { - return String(value).replace(/[;,\[\]{}]/g, '_'); + return String(value).replace(/[;,\[\]{}]/g, "_"); } function stableInlineJson(value) { @@ -2258,14 +2269,19 @@ function stableInlineJson(value) { } function sanitizeRecordForPreview(record) { - if (!record || typeof record !== 'object' || Array.isArray(record)) return record; + if (!record || typeof record !== "object" || Array.isArray(record)) return record; const out = {}; for (const [key, value] of Object.entries(record)) { const blob = blobRefMetadata(value); if (blob) { - out[key] = { object: 'binary_field', preview_status: 'binary-only', ...blob }; + out[key] = { object: "binary_field", preview_status: "binary-only", ...blob }; } else if (isLargeBase64Field(key, value)) { - out[key] = { object: 'binary_field', preview_status: 'binary-only', encoding: 'base64', size_chars: value.length }; + out[key] = { + object: "binary_field", + preview_status: "binary-only", + encoding: "base64", + size_chars: value.length, + }; } else { out[key] = value; } @@ -2276,7 +2292,7 @@ function sanitizeRecordForPreview(record) { function truncateText(value, limit) { const safeLimit = Math.max(0, limit); if (value.length <= safeLimit) return value; - if (safeLimit <= 1) return '…'; + if (safeLimit <= 1) return "…"; return `${value.slice(0, safeLimit - 1)}…`; } @@ -2289,22 +2305,22 @@ const SEARCH_RESULT_SNIPPET_CHAR_LIMIT = 320; const SEARCH_TEXT_ID_CHAR_LIMIT = 200; function summarizeSearch(body, results) { - const hasMore = envelopeField(body, 'has_more') === true ? ' has_more=true.' : ''; - const nextCursor = envelopeStringField(body, 'next_cursor'); - const cursorText = nextCursor ? ` next_cursor=${formatScalar(nextCursor)}.` : ''; - const firstFetchText = results[0]?.id ? ` first_fetch_id=${formatInlineValue(results[0].id)}` : ''; + const hasMore = envelopeField(body, "has_more") === true ? " has_more=true." : ""; + const nextCursor = envelopeStringField(body, "next_cursor"); + const cursorText = nextCursor ? ` next_cursor=${formatScalar(nextCursor)}.` : ""; + const firstFetchText = results[0]?.id ? ` first_fetch_id=${formatInlineValue(results[0].id)}` : ""; const sourceMixText = formatSearchSourceMix(body); const recallText = formatSearchRecallWarning(body); const previews = results.slice(0, SEARCH_TEXT_PREVIEW_LIMIT).map(formatSearchPreviewLine); const matchPreviews = results .slice(0, SEARCH_TEXT_PREVIEW_LIMIT) .flatMap((result, index) => formatSearchMatchWindowLines(result, index)); - const matchPreviewText = - matchPreviews.length > 0 ? `\nEvidence excerpts:\n${matchPreviews.join('\n')}` : ''; - const previewText = previews.length > 0 ? `\nTop results:\n${previews.join('\n')}` : ''; - const fetchHint = previews.length > 0 - ? '\nFetch a hit with `fetch` using the shown id as-is; ids are self-contained. Pass connection_id only when shown separately.' - : ''; + const matchPreviewText = matchPreviews.length > 0 ? `\nEvidence excerpts:\n${matchPreviews.join("\n")}` : ""; + const previewText = previews.length > 0 ? `\nTop results:\n${previews.join("\n")}` : ""; + const fetchHint = + previews.length > 0 + ? "\nFetch a hit with `fetch` using the shown id as-is; ids are self-contained. Pass connection_id only when shown separately." + : ""; return `search: ${results.length} hit(s).${hasMore}${cursorText}${firstFetchText}${sourceMixText}${recallText}${matchPreviewText}${previewText}${fetchHint} Search envelope metadata: structuredContent.data; flattened results: structuredContent.results.`; } @@ -2312,31 +2328,34 @@ function formatSearchMatchWindowLines(result, index) { const windows = Array.isArray(result.match_windows) ? result.match_windows : []; return windows .slice(0, 2) - .filter((window) => typeof window.text === 'string' && window.text.length > 0) + .filter((window) => typeof window.text === "string" && window.text.length > 0) .map((window) => { const fieldPath = firstString(window.field_path, window.field?.path); const read = objectValue(window.read); const readArgs = objectValue(read?.args); - const complete = window.complete === true ? 'complete=true' : 'complete=false'; - const cursor = firstString(window.next_cursor) ? ` next_cursor=${formatInlineValue(window.next_cursor)}` : ''; - const offset = Number.isInteger(readArgs?.offset_chars) ? ` offset_chars=${readArgs.offset_chars}` : ''; - const limit = Number.isInteger(readArgs?.limit_chars) ? ` limit_chars=${readArgs.limit_chars}` : ''; - const readHint = read?.tool || readArgs ? ` read=${formatInlineValue(read.tool ?? 'read_record_field')}${offset}${limit}` : ''; + const complete = window.complete === true ? "complete=true" : "complete=false"; + const cursor = firstString(window.next_cursor) ? ` next_cursor=${formatInlineValue(window.next_cursor)}` : ""; + const offset = Number.isInteger(readArgs?.offset_chars) ? ` offset_chars=${readArgs.offset_chars}` : ""; + const limit = Number.isInteger(readArgs?.limit_chars) ? ` limit_chars=${readArgs.limit_chars}` : ""; + const readHint = + read?.tool || readArgs ? ` read=${formatInlineValue(read.tool ?? "read_record_field")}${offset}${limit}` : ""; const visibleId = firstString(readArgs?.id, result.id); const previewText = - typeof window.preview_text === 'string' ? window.preview_text : truncateText(window.text, SEARCH_TEXT_SNIPPET_CHAR_LIMIT); - return `${index + 1}. result=${index + 1} field_path=${formatFieldName(fieldPath ?? 'unknown')} snippet=${formatScalar( + typeof window.preview_text === "string" + ? window.preview_text + : truncateText(window.text, SEARCH_TEXT_SNIPPET_CHAR_LIMIT); + return `${index + 1}. result=${index + 1} field_path=${formatFieldName(fieldPath ?? "unknown")} snippet=${formatScalar( previewText - )} id=${formatInlineValue(truncateText(visibleId ?? '', SEARCH_TEXT_ID_CHAR_LIMIT))} ${complete}${cursor}${readHint}`; + )} id=${formatInlineValue(truncateText(visibleId ?? "", SEARCH_TEXT_ID_CHAR_LIMIT))} ${complete}${cursor}${readHint}`; }); } function formatSearchReadArgs(args) { - if (!args) return ''; + if (!args) return ""; return Object.entries(args) - .filter(([, value]) => value !== undefined && value !== null && value !== '') + .filter(([, value]) => value !== undefined && value !== null && value !== "") .map(([key, value]) => `${formatFieldName(key)}=${formatInlineValue(truncateText(String(value), 120))}`) - .join(','); + .join(","); } // Mirror — never reinterpret — the RS recall disclosure. The warning is driven @@ -2345,42 +2364,40 @@ function formatSearchReadArgs(args) { // (`all_matches`) recall emits no extra warning, keeping the common case terse. // Spec: openspec/changes/disclose-lexical-recall-windows. function formatSearchRecallWarning(body) { - const meta = objectValue(envelopeField(body, 'meta')); + const meta = objectValue(envelopeField(body, "meta")); const recall = objectValue(meta?.recall); - if (!recall) return ''; - if (recall.ranking_scope === 'candidate_window' || recall.complete === false) { + if (!recall) return ""; + if (recall.ranking_scope === "candidate_window" || recall.complete === false) { const facts = []; - if (typeof recall.ranked_candidate_count === 'number') { + if (typeof recall.ranked_candidate_count === "number") { facts.push(`ranked_candidate_count=${recall.ranked_candidate_count}`); } - if (typeof recall.candidate_window_limit === 'number') { + if (typeof recall.candidate_window_limit === "number") { facts.push(`candidate_window_limit=${recall.candidate_window_limit}`); } - if (typeof recall.truncated_source_count === 'number') { + if (typeof recall.truncated_source_count === "number") { facts.push(`truncated_source_count=${recall.truncated_source_count}`); } - const factsText = facts.length > 0 ? ` (${facts.join(', ')})` : ''; + const factsText = facts.length > 0 ? ` (${facts.join(", ")})` : ""; return ` Recall: results were ranked over a bounded candidate window, not all matches — more matches may exist${factsText}; do not treat this page as exhaustive.`; } - return ''; + return ""; } function formatSearchSourceMix(body) { const sourceMix = body?.meta?.package?.source_mix; - if (!Array.isArray(sourceMix) || sourceMix.length === 0) return ''; - const rendered = sourceMix - .slice(0, 8) - .map((entry) => { - const parts = [ - `connection_id:${formatInlineValue(entry?.connection_id)}`, - `connector_key:${formatInlineValue(entry?.connector_key)}`, - `count:${formatInlineValue(entry?.count)}`, - ]; - if (entry?.display_name) parts.push(`display_name:${formatInlineValue(entry.display_name)}`); - return `{${parts.join(',')}}`; - }); + if (!Array.isArray(sourceMix) || sourceMix.length === 0) return ""; + const rendered = sourceMix.slice(0, 8).map((entry) => { + const parts = [ + `connection_id:${formatInlineValue(entry?.connection_id)}`, + `connector_key:${formatInlineValue(entry?.connector_key)}`, + `count:${formatInlineValue(entry?.count)}`, + ]; + if (entry?.display_name) parts.push(`display_name:${formatInlineValue(entry.display_name)}`); + return `{${parts.join(",")}}`; + }); if (sourceMix.length > 8) rendered.push(`more:${sourceMix.length - 8}`); - return ` source_mix=${rendered.join('|')}.`; + return ` source_mix=${rendered.join("|")}.`; } function formatSearchPreviewLine(result, index) { @@ -2394,14 +2411,15 @@ function formatSearchPreviewLine(result, index) { if (result.stream) parts.push(`stream=${formatInlineValue(truncateText(result.stream, 60))}`); if (result.title && result.title !== result.id) parts.push(`title=${formatScalar(truncateText(result.title, 80))}`); if (result.display_name) parts.push(`display_name=${formatScalar(truncateText(result.display_name, 60))}`); - if (result.snippet) parts.push(`snippet=${formatScalar(truncateText(result.snippet, SEARCH_TEXT_SNIPPET_CHAR_LIMIT))}`); - return parts.join(' '); + if (result.snippet) + parts.push(`snippet=${formatScalar(truncateText(result.snippet, SEARCH_TEXT_SNIPPET_CHAR_LIMIT))}`); + return parts.join(" "); } function envelopeField(body, field) { - if (!body || typeof body !== 'object') return undefined; + if (!body || typeof body !== "object") return undefined; if (Object.prototype.hasOwnProperty.call(body, field)) return body[field]; - if (body.data && typeof body.data === 'object' && !Array.isArray(body.data)) { + if (body.data && typeof body.data === "object" && !Array.isArray(body.data)) { return body.data[field]; } return undefined; @@ -2409,11 +2427,11 @@ function envelopeField(body, field) { function envelopeStringField(body, field) { const value = envelopeField(body, field); - return typeof value === 'string' && value.length > 0 ? value : null; + return typeof value === "string" && value.length > 0 ? value : null; } function envelopeCount(body) { - const meta = objectValue(envelopeField(body, 'meta')); + const meta = objectValue(envelopeField(body, "meta")); const count = objectValue(meta?.count); if (!count) return null; const kind = firstString(count.kind, count.type); @@ -2435,7 +2453,7 @@ function normalizeSearchResults(body, options = {}) { hit?.connection_id, hit?.connector_instance_id, source.connection_id, - recordUriRef?.connectionId, + recordUriRef?.connectionId ); // The id is the single opaque handle a model carries into `fetch`; encode // the hit's connection so multi-source grants resolve without a second @@ -2510,14 +2528,16 @@ function normalizeSearchMatchWindow(window, options = {}) { text: normalizedText, preview_text: previewText, complete, - ...(firstString(value.next_cursor, value.window?.next_cursor) ? { next_cursor: firstString(value.next_cursor, value.window?.next_cursor) } : {}), + ...(firstString(value.next_cursor, value.window?.next_cursor) + ? { next_cursor: firstString(value.next_cursor, value.window?.next_cursor) } + : {}), ...(read ? { read } : {}), }; } function centeredSearchText(text, query, limit) { - if (typeof text !== 'string' || text.length <= limit) return text; - const q = typeof query === 'string' ? query.trim() : ''; + if (typeof text !== "string" || text.length <= limit) return text; + const q = typeof query === "string" ? query.trim() : ""; if (!q) return truncateText(text, limit); const index = text.toLowerCase().indexOf(q.toLowerCase()); if (index < 0) return truncateText(text, limit); @@ -2526,49 +2546,49 @@ function centeredSearchText(text, query, limit) { let start = Math.max(0, index - Math.floor((available - q.length) / 2)); let end = Math.min(text.length, start + available); start = Math.max(0, end - available); - const prefix = start > 0 ? '...' : ''; - const suffix = end < text.length ? '...' : ''; + const prefix = start > 0 ? "..." : ""; + const suffix = end < text.length ? "..." : ""; return `${prefix}${text.slice(start, end)}${suffix}`; } function searchCandidatesFromBody(body) { - if (!body || typeof body !== 'object') return []; + if (!body || typeof body !== "object") return []; if (Array.isArray(body.results)) return body.results; if (Array.isArray(body.hits)) return body.hits; if (Array.isArray(body.data)) return body.data; - if (body.data && typeof body.data === 'object' && Array.isArray(body.data.results)) return body.data.results; - if (body.data && typeof body.data === 'object' && Array.isArray(body.data.data)) return body.data.data; + if (body.data && typeof body.data === "object" && Array.isArray(body.data.results)) return body.data.results; + if (body.data && typeof body.data === "object" && Array.isArray(body.data.data)) return body.data.data; return []; } function requestedSearchLimit(value) { - if (typeof value === 'number' && Number.isInteger(value) && value > 0) { + if (typeof value === "number" && Number.isInteger(value) && value > 0) { return Math.min(value, 100); } return 25; } function compactSearchEnvelope(body, { resultCount } = {}) { - if (!body || typeof body !== 'object') return body; + if (!body || typeof body !== "object") return body; if (Array.isArray(body)) { - return { object: 'list', results_ref: 'structuredContent.results', result_count: resultCount ?? body.length }; + return { object: "list", results_ref: "structuredContent.results", result_count: resultCount ?? body.length }; } const out = { ...body }; if (Array.isArray(out.results)) { out.result_count = resultCount ?? out.results.length; delete out.results; - out.results_ref = 'structuredContent.results'; + out.results_ref = "structuredContent.results"; } if (Array.isArray(out.hits)) { out.result_count = resultCount ?? out.hits.length; delete out.hits; - out.results_ref = 'structuredContent.results'; + out.results_ref = "structuredContent.results"; } if (Array.isArray(out.data)) { out.result_count = resultCount ?? out.data.length; delete out.data; - out.results_ref = 'structuredContent.results'; - } else if (out.data && typeof out.data === 'object') { + out.results_ref = "structuredContent.results"; + } else if (out.data && typeof out.data === "object") { out.data = compactSearchEnvelopeDataObject(out.data, { resultCount }); } return out; @@ -2579,17 +2599,17 @@ function compactSearchEnvelopeDataObject(data, { resultCount } = {}) { if (Array.isArray(out.results)) { out.result_count = resultCount ?? out.results.length; delete out.results; - out.results_ref = 'structuredContent.results'; + out.results_ref = "structuredContent.results"; } if (Array.isArray(out.hits)) { out.result_count = resultCount ?? out.hits.length; delete out.hits; - out.results_ref = 'structuredContent.results'; + out.results_ref = "structuredContent.results"; } if (Array.isArray(out.data)) { out.result_count = resultCount ?? out.data.length; delete out.data; - out.results_ref = 'structuredContent.results'; + out.results_ref = "structuredContent.results"; } return out; } @@ -2611,8 +2631,7 @@ function resultIdForHit(hit, index, fallback = {}) { const stream = fallback.stream ?? streamForHit(hit); const recordId = - fallback.recordKey ?? - stringValue(hit?.id ?? hit?.record_id ?? hit?.recordId ?? hit?.record_key ?? hit?.recordKey); + fallback.recordKey ?? stringValue(hit?.id ?? hit?.record_id ?? hit?.recordId ?? hit?.record_key ?? hit?.recordKey); if (stream && recordId) { return `${stream}:${recordId}`; } @@ -2624,7 +2643,7 @@ function resultIdForHit(hit, index, fallback = {}) { function recordUriRefForHit(hit) { for (const value of [hit?.record_uri, hit?.recordUri, hit?.id]) { const raw = stringValue(value); - if (!raw || !raw.startsWith('pdpp://record/')) continue; + if (!raw || !raw.startsWith("pdpp://record/")) continue; const parsed = parseRecordResultIdOrNull(raw); if (parsed) return parsed; } @@ -2647,10 +2666,10 @@ function snippetForSearchHit(hit) { const snippet = objectValue(hit?.snippet); return firstString( snippet?.text, - typeof hit?.snippet === 'string' ? hit.snippet : undefined, + typeof hit?.snippet === "string" ? hit.snippet : undefined, hit?.snippet_text, hit?.summary, - hit?.text, + hit?.text ); } @@ -2663,22 +2682,22 @@ function snippetForSearchHit(hit) { // parsing exactly as before. See: // openspec/changes/make-mcp-result-ids-self-contained function parseRecordResultId(id) { - if (typeof id !== 'string' || id.length === 0) { - throw new Error('id is required'); + if (typeof id !== "string" || id.length === 0) { + throw new Error("id is required"); } - if (id.startsWith('pdpp://record/')) { - const handle = decodeURIComponent(id.slice('pdpp://record/'.length)); + if (id.startsWith("pdpp://record/")) { + const handle = decodeURIComponent(id.slice("pdpp://record/".length)); // The canonical record_uri the model sees in content ladders and resource // templates is `pdpp://record/{base64url-JSON}`. Accept that first so a // visible handle is never a dead fetch/read_record_field id; fall back to // the human-readable `connection_id/stream:record_id` form for legacy/test // URIs that embed the self-contained grammar directly. try { - const payload = decodeResourceHandle(handle, 'record'); + const payload = decodeResourceHandle(handle, "record"); return { - connectionId: requireSafeName(payload.connection_id, 'connection_id'), - stream: requireSafeName(payload.stream, 'stream'), - recordId: requireSafeName(payload.record_id, 'record_id'), + connectionId: requireSafeName(payload.connection_id, "connection_id"), + stream: requireSafeName(payload.stream, "stream"), + recordId: requireSafeName(payload.record_id, "record_id"), }; } catch (error) { if (error instanceof InvalidResourceUriError) { @@ -2687,11 +2706,11 @@ function parseRecordResultId(id) { throw error; } } - const connectionSeparator = id.indexOf('/'); + const connectionSeparator = id.indexOf("/"); if (connectionSeparator === -1) { return { ...parseStreamRecordId(id), connectionId: null }; } - const connectionId = requireSafeName(id.slice(0, connectionSeparator), 'connection_id'); + const connectionId = requireSafeName(id.slice(0, connectionSeparator), "connection_id"); return { ...parseStreamRecordId(id.slice(connectionSeparator + 1)), connectionId, @@ -2699,14 +2718,14 @@ function parseRecordResultId(id) { } function parseStreamRecordId(id) { - const value = requireSafeName(id, 'id'); - const separator = value.indexOf(':'); + const value = requireSafeName(id, "id"); + const separator = value.indexOf(":"); if (separator <= 0 || separator === value.length - 1) { - throw new Error('id must use connection_id/stream:record_id or stream:record_id format'); + throw new Error("id must use connection_id/stream:record_id or stream:record_id format"); } return { - stream: requireSafeName(value.slice(0, separator), 'stream'), - recordId: requireSafeName(value.slice(separator + 1), 'record_id'), + stream: requireSafeName(value.slice(0, separator), "stream"), + recordId: requireSafeName(value.slice(separator + 1), "record_id"), }; } @@ -2716,10 +2735,10 @@ function parseStreamRecordId(id) { // and connection ids that could not survive the grammar (`/` or `:` inside) // never produce a malformed handle. function selfContainedResultId(baseId, connectionId) { - if (typeof baseId !== 'string' || baseId.length === 0) return baseId; - if (typeof connectionId !== 'string' || connectionId.length === 0) return baseId; - if (connectionId.includes('/') || connectionId.includes(':')) return baseId; - if (baseId.includes('/')) return baseId; + if (typeof baseId !== "string" || baseId.length === 0) return baseId; + if (typeof connectionId !== "string" || connectionId.length === 0) return baseId; + if (connectionId.includes("/") || connectionId.includes(":")) return baseId; + if (baseId.includes("/")) return baseId; if (!parseRecordResultIdOrNull(baseId)) return baseId; return `${connectionId}/${baseId}`; } @@ -2733,7 +2752,7 @@ function normalizeFetchedDocument(record, requestedId, providerUrl) { const stream = stringValue(record?.stream ?? record?.stream_name ?? record?.streamName) || stringValue(payload?.stream ?? payload?.stream_name ?? payload?.streamName); - const resultId = stream && id && !requestedId.includes(':') ? `${stream}:${id}` : requestedId; + const resultId = stream && id && !requestedId.includes(":") ? `${stream}:${id}` : requestedId; const title = titleForFetchedRecord(record, payload, resultId); const text = textForFetchedRecord(record, payload); const url = urlForFetchedRecord(record, payload, resultId, providerUrl); @@ -2742,8 +2761,8 @@ function normalizeFetchedDocument(record, requestedId, providerUrl) { } function titleForFetchedRecord(record, payload, fallbackId) { - const payloadTitle = payload ? titleForRecord(payload, '') : ''; - return payloadTitle || titleForRecord(record, '') || titleFromSourceIdentity(record, payload, fallbackId); + const payloadTitle = payload ? titleForRecord(payload, "") : ""; + return payloadTitle || titleForRecord(record, "") || titleFromSourceIdentity(record, payload, fallbackId); } function titleForRecord(record, fallbackId) { @@ -2757,12 +2776,12 @@ function titleForRecord(record, fallbackId) { } function titleForSearchHit(record, fallbackId, source = {}) { - const explicit = titleForRecord(record, ''); + const explicit = titleForRecord(record, ""); if (explicit) return explicit; const timestamp = titleTimestampForRecord(record); const label = source.displayName || source.connectorKey || source.connectionId; const parts = [label, source.stream, timestamp].filter(Boolean); - return parts.length > 0 ? parts.join(' / ') : fallbackId; + return parts.length > 0 ? parts.join(" / ") : fallbackId; } function titleFromSourceIdentity(record, payload, fallbackId) { @@ -2777,12 +2796,12 @@ function titleFromSourceIdentity(record, payload, fallbackId) { source.display_name, source.connector_key, source.connector_id, - source.connection_id, + source.connection_id ); const stream = firstString(record?.stream, record?.stream_name, payload?.stream, payload?.stream_name, source.stream); const timestamp = titleTimestampForRecord(payload) || titleTimestampForRecord(record); const parts = [label, stream, timestamp].filter(Boolean); - return parts.length > 0 ? parts.join(' / ') : fallbackId; + return parts.length > 0 ? parts.join(" / ") : fallbackId; } function titleTimestampForRecord(record) { @@ -2818,16 +2837,13 @@ function titleTimestampForRecord(record) { value.occurredAt, value.updated_at, value.updatedAt, - ]), + ]) ); if (authored) return authored; return firstString( record?.emitted_at, record?.emittedAt, - ...nested.flatMap((value) => [ - value.emitted_at, - value.emittedAt, - ]), + ...nested.flatMap((value) => [value.emitted_at, value.emittedAt]) ); } @@ -2848,7 +2864,7 @@ function textForFetchedRecord(record, payload) { // truncated and no field an agent needs is dropped. const FETCH_TEXT_FALLBACK_CHAR_LIMIT = 1024; const FETCH_TEXT_FALLBACK_POINTER = - '… [record has no text/content/body/summary field; use query_records or fetch(fields) for structured records]'; + "… [record has no text/content/body/summary field; use query_records or fetch(fields) for structured records]"; function textForRecord(record) { const declared = declaredTextForRecord(record); @@ -2885,19 +2901,21 @@ function urlForFetchedRecord(record, payload, fallbackId, providerUrl) { record?.recordUrl, record?.href, record?.source_url, - record?.sourceUrl, + record?.sourceUrl ); if (directUrl) return directUrl; return urlForRecord(record, fallbackId, providerUrl); } function urlForRecord(record, fallbackId, providerUrl) { - const directUrl = stringValue(record?.url ?? record?.record_url ?? record?.recordUrl ?? record?.href ?? record?.source_url ?? record?.sourceUrl); + const directUrl = stringValue( + record?.url ?? record?.record_url ?? record?.recordUrl ?? record?.href ?? record?.source_url ?? record?.sourceUrl + ); if (directUrl) return directUrl; if (providerUrl && fallbackId) { const recordRef = parseRecordResultIdOrNull(fallbackId); if (recordRef) { - const base = providerUrl.replace(/\/$/, ''); + const base = providerUrl.replace(/\/$/, ""); const recordUrl = `${base}/v1/streams/${encodeURIComponent(recordRef.stream)}/records/${encodeURIComponent(recordRef.recordId)}`; return recordRef.connectionId ? `${recordUrl}?connection_id=${encodeURIComponent(recordRef.connectionId)}` @@ -2916,11 +2934,11 @@ function parseRecordResultIdOrNull(id) { } function metadataForRecord(record, omitted) { - if (!record || typeof record !== 'object') { + if (!record || typeof record !== "object") { return {}; } const metadata = {}; - if (record.metadata && typeof record.metadata === 'object' && !Array.isArray(record.metadata)) { + if (record.metadata && typeof record.metadata === "object" && !Array.isArray(record.metadata)) { for (const [key, value] of Object.entries(record.metadata)) { if (isDocumentMetadataValue(value)) metadata[key] = value; } @@ -2928,13 +2946,13 @@ function metadataForRecord(record, omitted) { const payload = objectValue(record.data); if (payload) { for (const key of [ - 'stream', - 'stream_name', - 'streamName', - 'connection_id', - 'connector_key', - 'connector_id', - 'display_name', + "stream", + "stream_name", + "streamName", + "connection_id", + "connector_key", + "connector_id", + "display_name", ]) { if (metadata[key] === undefined && payload[key] !== undefined) { metadata[key] = payload[key]; @@ -2942,7 +2960,7 @@ function metadataForRecord(record, omitted) { } } for (const [key, value] of Object.entries(record)) { - if (['metadata', 'data', 'text', 'content', 'body'].includes(key)) continue; + if (["metadata", "data", "text", "content", "body"].includes(key)) continue; if (isOmittedDocumentField(key, value, omitted)) continue; if (!FETCH_METADATA_RECORD_KEYS.has(key)) continue; if (!isDocumentMetadataValue(value)) continue; @@ -2952,45 +2970,45 @@ function metadataForRecord(record, omitted) { } function isOmittedDocumentField(key, value, omitted) { - if (['id', 'record_id', 'recordId'].includes(key)) return value === omitted.id; - if (key === 'title') return value === omitted.title; - if (['url', 'record_url', 'recordUrl', 'href', 'source_url', 'sourceUrl'].includes(key)) return value === omitted.url; + if (["id", "record_id", "recordId"].includes(key)) return value === omitted.id; + if (key === "title") return value === omitted.title; + if (["url", "record_url", "recordUrl", "href", "source_url", "sourceUrl"].includes(key)) return value === omitted.url; return false; } const FETCH_METADATA_RECORD_KEYS = new Set([ - 'object', - 'id', - 'record_id', - 'recordId', - 'stream', - 'stream_name', - 'streamName', - 'connection_id', - 'connector_key', - 'connector_id', - 'display_name', - 'emitted_at', - 'emittedAt', - 'sent_at', - 'sentAt', - 'created_at', - 'createdAt', - 'updated_at', - 'updatedAt', + "object", + "id", + "record_id", + "recordId", + "stream", + "stream_name", + "streamName", + "connection_id", + "connector_key", + "connector_id", + "display_name", + "emitted_at", + "emittedAt", + "sent_at", + "sentAt", + "created_at", + "createdAt", + "updated_at", + "updatedAt", ]); function isDocumentMetadataValue(value) { - return value === null || ['string', 'number', 'boolean'].includes(typeof value); + return value === null || ["string", "number", "boolean"].includes(typeof value); } function stringValue(value) { - return typeof value === 'string' && value.length > 0 ? value : undefined; + return typeof value === "string" && value.length > 0 ? value : undefined; } function errorToolResult(response, providerUrl) { const error = response.error ?? { - type: 'rs_error', + type: "rs_error", code: `http_${response.status}`, message: `Resource server returned HTTP ${response.status}`, }; @@ -2998,7 +3016,7 @@ function errorToolResult(response, providerUrl) { isError: true, content: [ { - type: 'text', + type: "text", text: JSON.stringify(error, null, 2), }, ], diff --git a/packages/mcp-server/test/smoke-stdio.mjs b/packages/mcp-server/test/smoke-stdio.mjs index d3e69255c..ea2f38348 100644 --- a/packages/mcp-server/test/smoke-stdio.mjs +++ b/packages/mcp-server/test/smoke-stdio.mjs @@ -7,49 +7,49 @@ // // Usage: // node packages/mcp-server/test/smoke-stdio.mjs > tmp/workstreams/mcp-stdio-smoke.json -import { spawn } from 'node:child_process'; -import { mkdtemp, mkdir, writeFile } from 'node:fs/promises'; -import { tmpdir } from 'node:os'; -import { join, dirname } from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { createServer } from 'node:http'; +import { spawn } from "node:child_process"; +import { mkdtemp, mkdir, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { createServer } from "node:http"; const __dirname = dirname(fileURLToPath(import.meta.url)); -const binPath = join(__dirname, '..', 'bin', 'pdpp-mcp-server.js'); +const binPath = join(__dirname, "..", "bin", "pdpp-mcp-server.js"); -const cacheRoot = await mkdtemp(join(tmpdir(), 'pdpp-mcp-smoke-')); +const cacheRoot = await mkdtemp(join(tmpdir(), "pdpp-mcp-smoke-")); const rs = createServer((req, res) => { - if (req.url === '/v1/schema') { - res.writeHead(200, { 'content-type': 'application/json' }); - res.end(JSON.stringify({ version: '1', streams: ['orders'] })); + if (req.url === "/v1/schema") { + res.writeHead(200, { "content-type": "application/json" }); + res.end(JSON.stringify({ version: "1", streams: ["orders"] })); return; } - res.writeHead(404, { 'content-type': 'application/json' }); - res.end(JSON.stringify({ error: { code: 'not_found' } })); + res.writeHead(404, { "content-type": "application/json" }); + res.end(JSON.stringify({ error: { code: "not_found" } })); }); -await new Promise((resolve) => rs.listen(0, '127.0.0.1', resolve)); +await new Promise((resolve) => rs.listen(0, "127.0.0.1", resolve)); const port = rs.address().port; const providerUrl = `http://127.0.0.1:${port}`; -const host = new URL(providerUrl).host.replace(/[^a-zA-Z0-9.-]/g, '_'); -await mkdir(join(cacheRoot, 'clients'), { recursive: true }); +const host = new URL(providerUrl).host.replace(/[^a-zA-Z0-9.-]/g, "_"); +await mkdir(join(cacheRoot, "clients"), { recursive: true }); await writeFile( - join(cacheRoot, 'clients', `${host}.json`), - JSON.stringify({ credential: { access_token: 'smoke-token' } }) + join(cacheRoot, "clients", `${host}.json`), + JSON.stringify({ credential: { access_token: "smoke-token" } }) ); -const proc = spawn(process.execPath, [binPath, '--provider-url', providerUrl, '--cache-root', cacheRoot], { - env: { ...process.env, PDPP_OWNER_TOKEN: '', PDPP_OWNER_SESSION_COOKIE: '' }, +const proc = spawn(process.execPath, [binPath, "--provider-url", providerUrl, "--cache-root", cacheRoot], { + env: { ...process.env, PDPP_OWNER_TOKEN: "", PDPP_OWNER_SESSION_COOKIE: "" }, }); -let stdoutBuf = ''; -let stderrBuf = ''; -proc.stdout.on('data', (chunk) => { - stdoutBuf += chunk.toString('utf8'); +let stdoutBuf = ""; +let stderrBuf = ""; +proc.stdout.on("data", (chunk) => { + stdoutBuf += chunk.toString("utf8"); }); -proc.stderr.on('data', (chunk) => { - stderrBuf += chunk.toString('utf8'); +proc.stderr.on("data", (chunk) => { + stderrBuf += chunk.toString("utf8"); }); function sendMessage(msg) { @@ -59,25 +59,25 @@ function sendMessage(msg) { await new Promise((resolve) => setTimeout(resolve, 500)); sendMessage({ - jsonrpc: '2.0', + jsonrpc: "2.0", id: 1, - method: 'initialize', + method: "initialize", params: { - protocolVersion: '2025-06-18', + protocolVersion: "2025-06-18", capabilities: {}, - clientInfo: { name: 'smoke', version: '0' }, + clientInfo: { name: "smoke", version: "0" }, }, }); // Wait for initialize response. await waitFor(() => stdoutBuf.includes('"id":1'), 3000); -sendMessage({ jsonrpc: '2.0', method: 'notifications/initialized' }); -sendMessage({ jsonrpc: '2.0', id: 2, method: 'tools/list' }); +sendMessage({ jsonrpc: "2.0", method: "notifications/initialized" }); +sendMessage({ jsonrpc: "2.0", id: 2, method: "tools/list" }); await waitFor(() => stdoutBuf.includes('"id":2'), 3000); -proc.kill('SIGTERM'); +proc.kill("SIGTERM"); rs.close(); async function waitFor(predicate, timeoutMs) { @@ -88,7 +88,7 @@ async function waitFor(predicate, timeoutMs) { } } -const lines = stdoutBuf.split('\n').filter((line) => line.length > 0); +const lines = stdoutBuf.split("\n").filter((line) => line.length > 0); const parsed = lines.map((line, index) => { try { return JSON.parse(line); @@ -100,10 +100,8 @@ const parsed = lines.map((line, index) => { const result = { ok: true, stdout_lines: parsed.length, - stderr_excerpt: stderrBuf.split('\n').slice(0, 4), - tool_names: parsed - .flatMap((msg) => (msg?.result?.tools ?? []).map((tool) => tool.name)) - .sort(), + stderr_excerpt: stderrBuf.split("\n").slice(0, 4), + tool_names: parsed.flatMap((msg) => (msg?.result?.tools ?? []).map((tool) => tool.name)).sort(), }; process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); diff --git a/packages/pdpp-brand/base.css b/packages/pdpp-brand/base.css index 3a3785ddb..a00334e6f 100644 --- a/packages/pdpp-brand/base.css +++ b/packages/pdpp-brand/base.css @@ -8,7 +8,8 @@ :root { --font-sans: "Geist Variable", "Geist", ui-sans-serif, system-ui, sans-serif; - --font-mono: "JetBrains Mono Variable", "JetBrains Mono", ui-monospace, monospace; + --font-mono: + "JetBrains Mono Variable", "JetBrains Mono", ui-monospace, monospace; --background: oklch(0.99 0.002 95); --foreground: oklch(0.13 0 0); @@ -16,19 +17,19 @@ --card-foreground: oklch(0.13 0 0); --popover: oklch(1 0 0); --popover-foreground: oklch(0.13 0 0); - --primary: oklch(0.580 0.172 253.7); + --primary: oklch(0.58 0.172 253.7); --primary-foreground: oklch(0.99 0 0); --secondary: oklch(0.96 0 0); - --secondary-foreground: oklch(0.20 0 0); + --secondary-foreground: oklch(0.2 0 0); --muted: oklch(0.96 0 0); - --muted-foreground: oklch(0.50 0 0); + --muted-foreground: oklch(0.5 0 0); --accent: oklch(0.96 0 0); - --accent-foreground: oklch(0.20 0 0); - --destructive: oklch(0.55 0.20 27); + --accent-foreground: oklch(0.2 0 0); + --destructive: oklch(0.55 0.2 27); --destructive-foreground: oklch(0.99 0 0); --border: oklch(0.94 0 0); --input: oklch(0.91 0 0); - --ring: oklch(0.580 0.172 253.7); + --ring: oklch(0.58 0.172 253.7); /* ─── Radius scale ────────────────────────────────────────────────────────── One intentional step per component class, not a single radius everywhere. @@ -46,15 +47,15 @@ values. Mirrors the Tailwind 4px grid, so named tokens and utility classes stay coherent. Compose padding/gap from these steps. */ --space-0: 0; - --space-1: 0.25rem; /* 4 */ - --space-2: 0.5rem; /* 8 */ - --space-3: 0.75rem; /* 12 */ - --space-4: 1rem; /* 16 */ - --space-5: 1.25rem; /* 20 */ - --space-6: 1.5rem; /* 24 */ - --space-8: 2rem; /* 32 */ - --space-10: 2.5rem; /* 40 */ - --space-12: 3rem; /* 48 */ + --space-1: 0.25rem; /* 4 */ + --space-2: 0.5rem; /* 8 */ + --space-3: 0.75rem; /* 12 */ + --space-4: 1rem; /* 16 */ + --space-5: 1.25rem; /* 20 */ + --space-6: 1.5rem; /* 24 */ + --space-8: 2rem; /* 32 */ + --space-10: 2.5rem; /* 40 */ + --space-12: 3rem; /* 48 */ /* ─── Surface-elevation ladder ────────────────────────────────────────────── Elevation = a step up in surface fill + a paired border, NOT a resting drop @@ -67,8 +68,9 @@ --surface-card: var(--card); --surface-raised: oklch(0.975 0.003 95); --surface-overlay: var(--popover); - --border-strong: oklch(0.90 0 0); - --shadow-overlay: 0 8px 24px -8px oklch(0 0 0 / 0.18), 0 2px 6px -2px oklch(0 0 0 / 0.10); + --border-strong: oklch(0.9 0 0); + --shadow-overlay: + 0 8px 24px -8px oklch(0 0 0 / 0.18), 0 2px 6px -2px oklch(0 0 0 / 0.1); /* Numeric treatment — every amount, count, timestamp aligns in columns. */ --numeric: tabular-nums lining-nums; @@ -85,7 +87,7 @@ --success-wash: oklch(0.52 0.15 150 / 0.1); --success-wash-strong: oklch(0.52 0.15 150 / 0.15); - --primary-wash: oklch(0.580 0.172 253.7 / 0.06); + --primary-wash: oklch(0.58 0.172 253.7 / 0.06); --verified-wash: oklch(0.52 0.15 150 / 0.1); --warning-wash: oklch(0.62 0.15 70 / 0.1); @@ -104,15 +106,15 @@ bg tokens use low-alpha fills (readable on --card and --background alike). fg tokens are readable body text on those fills, not the vivid badge colors. */ --status-success-bg: oklch(0.52 0.15 150 / 0.08); - --status-success-fg: oklch(0.30 0.12 150); + --status-success-fg: oklch(0.3 0.12 150); - --status-progress-bg: oklch(0.580 0.172 253.7 / 0.07); - --status-progress-fg: oklch(0.40 0.14 253.7); + --status-progress-bg: oklch(0.58 0.172 253.7 / 0.07); + --status-progress-fg: oklch(0.4 0.14 253.7); --status-warning-bg: oklch(0.62 0.15 70 / 0.09); --status-warning-fg: oklch(0.38 0.12 70); - --status-danger-bg: oklch(0.55 0.20 27 / 0.08); + --status-danger-bg: oklch(0.55 0.2 27 / 0.08); --status-danger-fg: oklch(0.38 0.16 27); --status-neutral-bg: oklch(0.94 0 0); @@ -122,18 +124,18 @@ --status-disabled-fg: oklch(0.58 0 0); /* Callout informational / warning tone tokens */ - --callout-info-bg: oklch(0.580 0.172 253.7 / 0.06); + --callout-info-bg: oklch(0.58 0.172 253.7 / 0.06); --callout-info-fg: oklch(0.35 0.14 253.7); - --callout-info-border: oklch(0.580 0.172 253.7 / 0.22); + --callout-info-border: oklch(0.58 0.172 253.7 / 0.22); --callout-warning-bg: oklch(0.62 0.15 70 / 0.08); --callout-warning-fg: oklch(0.38 0.12 70); - --callout-warning-border: oklch(0.62 0.15 70 / 0.30); + --callout-warning-border: oklch(0.62 0.15 70 / 0.3); --human: oklch(0.52 0.09 45); --human-wash: oklch(0.52 0.09 45 / 0.07); -/* ─── Authorship trust tier ────────────────────────────────────────────────── + /* ─── Authorship trust tier ────────────────────────────────────────────────── Trust-provenance surfaces. Three authorship classes; each gets a hue temperature so a standards reviewer can *see* the authorship boundary. protocol — cool blue (H253.7): enforced, verifiable protocol facts @@ -141,17 +143,17 @@ client — neutral grey + dashed: client claims, never trusted Roles: -fg (label ink) · -accent (vivid marker) · -border (hairline) · -wash (low-alpha tint; use for accents/labels, not fill blocks) */ - --authorship-protocol-fg: oklch(0.40 0.14 253.7); - --authorship-protocol-accent: oklch(0.580 0.172 253.7); - --authorship-protocol-border: oklch(0.580 0.172 253.7 / 0.30); - --authorship-protocol-wash: oklch(0.580 0.172 253.7 / 0.06); + --authorship-protocol-fg: oklch(0.4 0.14 253.7); + --authorship-protocol-accent: oklch(0.58 0.172 253.7); + --authorship-protocol-border: oklch(0.58 0.172 253.7 / 0.3); + --authorship-protocol-wash: oklch(0.58 0.172 253.7 / 0.06); - --authorship-manifest-fg: oklch(0.46 0.10 45); + --authorship-manifest-fg: oklch(0.46 0.1 45); --authorship-manifest-accent: oklch(0.52 0.12 45); --authorship-manifest-border: oklch(0.52 0.12 45 / 0.35); --authorship-manifest-wash: oklch(0.52 0.12 45 / 0.06); - --authorship-client-fg: oklch(0.50 0 0); + --authorship-client-fg: oklch(0.5 0 0); --authorship-client-accent: oklch(0.62 0 0); --authorship-client-border: oklch(0.72 0 0); --authorship-client-wash: oklch(0.62 0 0 / 0.05); @@ -224,9 +226,9 @@ html[data-density="compact"] { :is(html.dark, html[data-theme="dark"]) { --background: oklch(0.16 0.005 260); --foreground: oklch(0.96 0.005 260); - --card: oklch(0.20 0.006 260); + --card: oklch(0.2 0.006 260); --card-foreground: oklch(0.96 0.005 260); - --popover: oklch(0.20 0.006 260); + --popover: oklch(0.2 0.006 260); --popover-foreground: oklch(0.96 0.005 260); --primary: oklch(0.72 0.16 253.7); @@ -236,12 +238,12 @@ html[data-density="compact"] { --secondary-foreground: oklch(0.96 0.005 260); --muted: oklch(0.24 0.006 260); - --muted-foreground: oklch(0.70 0.01 260); + --muted-foreground: oklch(0.7 0.01 260); --accent: oklch(0.26 0.006 260); --accent-foreground: oklch(0.96 0.005 260); - --destructive: oklch(0.70 0.18 25); + --destructive: oklch(0.7 0.18 25); --destructive-foreground: oklch(0.14 0.01 260); --border: oklch(0.28 0.006 260); @@ -249,7 +251,7 @@ html[data-density="compact"] { --ring: oklch(0.72 0.16 253.7); --success: oklch(0.74 0.16 152); - --warning: oklch(0.80 0.15 75); + --warning: oklch(0.8 0.15 75); --success-badge-foreground: var(--success); --warning-badge-foreground: var(--warning); --danger-badge-foreground: var(--destructive); @@ -263,7 +265,7 @@ html[data-density="compact"] { --success-wash-strong: oklch(0.74 0.16 152 / 0.22); --primary-wash: oklch(0.72 0.16 253.7 / 0.12); --verified-wash: oklch(0.74 0.16 152 / 0.16); - --warning-wash: oklch(0.80 0.15 75 / 0.14); + --warning-wash: oklch(0.8 0.15 75 / 0.14); /* Status surface tier — dark overrides. Higher alpha than light (charcoal absorbs the fill) and lighter fg (near-black background needs more contrast). */ @@ -273,10 +275,10 @@ html[data-density="compact"] { --status-progress-bg: oklch(0.72 0.16 253.7 / 0.13); --status-progress-fg: oklch(0.82 0.14 253.7); - --status-warning-bg: oklch(0.80 0.15 75 / 0.14); + --status-warning-bg: oklch(0.8 0.15 75 / 0.14); --status-warning-fg: oklch(0.86 0.13 75); - --status-danger-bg: oklch(0.70 0.18 25 / 0.15); + --status-danger-bg: oklch(0.7 0.18 25 / 0.15); --status-danger-fg: oklch(0.82 0.16 25); --status-neutral-bg: oklch(0.24 0.006 260); @@ -285,13 +287,13 @@ html[data-density="compact"] { --status-disabled-bg: oklch(0.22 0.005 260); --status-disabled-fg: oklch(0.55 0.008 260); - --callout-info-bg: oklch(0.72 0.16 253.7 / 0.10); + --callout-info-bg: oklch(0.72 0.16 253.7 / 0.1); --callout-info-fg: oklch(0.82 0.14 253.7); --callout-info-border: oklch(0.72 0.16 253.7 / 0.28); - --callout-warning-bg: oklch(0.80 0.15 75 / 0.12); + --callout-warning-bg: oklch(0.8 0.15 75 / 0.12); --callout-warning-fg: oklch(0.86 0.13 75); - --callout-warning-border: oklch(0.80 0.15 75 / 0.32); + --callout-warning-border: oklch(0.8 0.15 75 / 0.32); /* The warm "human" accent as a true sibling of the blue primary: comparable chroma magnitude (0.14 vs primary 0.16) so it carries equal visual weight, @@ -300,43 +302,44 @@ html[data-density="compact"] { as a muted warning. Its own semantic lane between destructive-red (H25) and warning-amber (H75). */ --human: oklch(0.75 0.14 45); - --human-wash: oklch(0.75 0.14 45 / 0.10); + --human-wash: oklch(0.75 0.14 45 / 0.1); /* Authorship trust tier — dark overrides. Lighter ink and accents to clear the charcoal surface; the client tier stays dashed so "claimed, not enforced" survives dark mode. */ --authorship-protocol-fg: oklch(0.82 0.14 253.7); --authorship-protocol-accent: oklch(0.72 0.16 253.7); - --authorship-protocol-border: oklch(0.72 0.16 253.7 / 0.40); + --authorship-protocol-border: oklch(0.72 0.16 253.7 / 0.4); --authorship-protocol-wash: oklch(0.72 0.16 253.7 / 0.12); --authorship-manifest-fg: oklch(0.82 0.12 45); --authorship-manifest-accent: oklch(0.75 0.14 45); - --authorship-manifest-border: oklch(0.75 0.14 45 / 0.40); + --authorship-manifest-border: oklch(0.75 0.14 45 / 0.4); --authorship-manifest-wash: oklch(0.75 0.14 45 / 0.12); - --authorship-client-fg: oklch(0.70 0.01 260); + --authorship-client-fg: oklch(0.7 0.01 260); --authorship-client-accent: oklch(0.62 0.008 260); --authorship-client-border: oklch(0.45 0.006 260); - --authorship-client-wash: oklch(0.70 0.01 260 / 0.07); + --authorship-client-wash: oklch(0.7 0.01 260 / 0.07); /* Lift mixers go *down* toward the page surface in dark mode. */ - --surface-tint: oklch(0.10 0.005 260); + --surface-tint: oklch(0.1 0.005 260); /* Surface ladder as discrete charcoal steps. Replacing resting shadows with fill-steps means each layer must be perceptibly lighter than the one it sits on. overlay is a FULL step above raised (not equal) so a floating layer over a raised card stays legible even before --shadow-overlay. */ --surface-page: oklch(0.16 0.005 260); - --surface-card: oklch(0.20 0.006 260); + --surface-card: oklch(0.2 0.006 260); --surface-raised: oklch(0.235 0.006 260); --surface-overlay: oklch(0.275 0.006 260); --border-strong: oklch(0.34 0.006 260); /* One luminance step above --border-strong, dedicated to dense-list row dividers so they chunk rows on charcoal without nudging every --border-strong consumer. */ - --data-list-divider: oklch(0.40 0.006 260); - --shadow-overlay: 0 10px 28px -10px oklch(0 0 0 / 0.55), 0 3px 8px -3px oklch(0 0 0 / 0.40); + --data-list-divider: oklch(0.4 0.006 260); + --shadow-overlay: + 0 10px 28px -10px oklch(0 0 0 / 0.55), 0 3px 8px -3px oklch(0 0 0 / 0.4); color-scheme: dark; } @@ -345,9 +348,9 @@ html[data-density="compact"] { html[data-theme="system"] { --background: oklch(0.16 0.005 260); --foreground: oklch(0.96 0.005 260); - --card: oklch(0.20 0.006 260); + --card: oklch(0.2 0.006 260); --card-foreground: oklch(0.96 0.005 260); - --popover: oklch(0.20 0.006 260); + --popover: oklch(0.2 0.006 260); --popover-foreground: oklch(0.96 0.005 260); --primary: oklch(0.72 0.16 253.7); @@ -357,12 +360,12 @@ html[data-density="compact"] { --secondary-foreground: oklch(0.96 0.005 260); --muted: oklch(0.24 0.006 260); - --muted-foreground: oklch(0.70 0.01 260); + --muted-foreground: oklch(0.7 0.01 260); --accent: oklch(0.26 0.006 260); --accent-foreground: oklch(0.96 0.005 260); - --destructive: oklch(0.70 0.18 25); + --destructive: oklch(0.7 0.18 25); --destructive-foreground: oklch(0.14 0.01 260); --border: oklch(0.28 0.006 260); @@ -370,7 +373,7 @@ html[data-density="compact"] { --ring: oklch(0.72 0.16 253.7); --success: oklch(0.74 0.16 152); - --warning: oklch(0.80 0.15 75); + --warning: oklch(0.8 0.15 75); --success-badge-foreground: var(--success); --warning-badge-foreground: var(--warning); --danger-badge-foreground: var(--destructive); @@ -382,7 +385,7 @@ html[data-density="compact"] { --success-wash-strong: oklch(0.74 0.16 152 / 0.22); --primary-wash: oklch(0.72 0.16 253.7 / 0.12); --verified-wash: oklch(0.74 0.16 152 / 0.16); - --warning-wash: oklch(0.80 0.15 75 / 0.14); + --warning-wash: oklch(0.8 0.15 75 / 0.14); /* Status surface tier — dark overrides. */ --status-success-bg: oklch(0.74 0.16 152 / 0.14); @@ -391,10 +394,10 @@ html[data-density="compact"] { --status-progress-bg: oklch(0.72 0.16 253.7 / 0.13); --status-progress-fg: oklch(0.82 0.14 253.7); - --status-warning-bg: oklch(0.80 0.15 75 / 0.14); + --status-warning-bg: oklch(0.8 0.15 75 / 0.14); --status-warning-fg: oklch(0.86 0.13 75); - --status-danger-bg: oklch(0.70 0.18 25 / 0.15); + --status-danger-bg: oklch(0.7 0.18 25 / 0.15); --status-danger-fg: oklch(0.82 0.16 25); --status-neutral-bg: oklch(0.24 0.006 260); @@ -403,53 +406,54 @@ html[data-density="compact"] { --status-disabled-bg: oklch(0.22 0.005 260); --status-disabled-fg: oklch(0.55 0.008 260); - --callout-info-bg: oklch(0.72 0.16 253.7 / 0.10); + --callout-info-bg: oklch(0.72 0.16 253.7 / 0.1); --callout-info-fg: oklch(0.82 0.14 253.7); --callout-info-border: oklch(0.72 0.16 253.7 / 0.28); - --callout-warning-bg: oklch(0.80 0.15 75 / 0.12); + --callout-warning-bg: oklch(0.8 0.15 75 / 0.12); --callout-warning-fg: oklch(0.86 0.13 75); - --callout-warning-border: oklch(0.80 0.15 75 / 0.32); + --callout-warning-border: oklch(0.8 0.15 75 / 0.32); /* The warm "human" accent as a true sibling of the blue primary: comparable chroma magnitude (0.14 vs primary 0.16), hue held in the terracotta lane (H45) — same hue as light mode and a full 30° clear of warning amber (H75) so a "human" marker never reads as a muted warning. */ --human: oklch(0.75 0.14 45); - --human-wash: oklch(0.75 0.14 45 / 0.10); + --human-wash: oklch(0.75 0.14 45 / 0.1); /* Authorship trust tier — system-dark overrides (mirrors html[data-theme="dark"]). */ --authorship-protocol-fg: oklch(0.82 0.14 253.7); --authorship-protocol-accent: oklch(0.72 0.16 253.7); - --authorship-protocol-border: oklch(0.72 0.16 253.7 / 0.40); + --authorship-protocol-border: oklch(0.72 0.16 253.7 / 0.4); --authorship-protocol-wash: oklch(0.72 0.16 253.7 / 0.12); --authorship-manifest-fg: oklch(0.82 0.12 45); --authorship-manifest-accent: oklch(0.75 0.14 45); - --authorship-manifest-border: oklch(0.75 0.14 45 / 0.40); + --authorship-manifest-border: oklch(0.75 0.14 45 / 0.4); --authorship-manifest-wash: oklch(0.75 0.14 45 / 0.12); - --authorship-client-fg: oklch(0.70 0.01 260); + --authorship-client-fg: oklch(0.7 0.01 260); --authorship-client-accent: oklch(0.62 0.008 260); --authorship-client-border: oklch(0.45 0.006 260); - --authorship-client-wash: oklch(0.70 0.01 260 / 0.07); + --authorship-client-wash: oklch(0.7 0.01 260 / 0.07); /* Lift mixers go *down* toward the page surface in dark mode. */ - --surface-tint: oklch(0.10 0.005 260); + --surface-tint: oklch(0.1 0.005 260); /* Surface ladder as discrete charcoal steps. overlay is a FULL step above raised (not equal) so a floating layer over a raised card stays legible even before --shadow-overlay applies. */ --surface-page: oklch(0.16 0.005 260); - --surface-card: oklch(0.20 0.006 260); + --surface-card: oklch(0.2 0.006 260); --surface-raised: oklch(0.235 0.006 260); --surface-overlay: oklch(0.275 0.006 260); --border-strong: oklch(0.34 0.006 260); /* One luminance step above --border-strong, dedicated to dense-list row dividers so they chunk rows on charcoal without nudging every --border-strong consumer. */ - --data-list-divider: oklch(0.40 0.006 260); - --shadow-overlay: 0 10px 28px -10px oklch(0 0 0 / 0.55), 0 3px 8px -3px oklch(0 0 0 / 0.40); + --data-list-divider: oklch(0.4 0.006 260); + --shadow-overlay: + 0 10px 28px -10px oklch(0 0 0 / 0.55), 0 3px 8px -3px oklch(0 0 0 / 0.4); color-scheme: dark; } @@ -473,7 +477,9 @@ body { color: var(--foreground); } -*, *::before, *::after { +*, +*::before, +*::after { box-sizing: border-box; } @@ -508,7 +514,7 @@ samp { } ::-webkit-scrollbar-thumb:hover { - background: oklch(0 0 0 / 0.20); + background: oklch(0 0 0 / 0.2); } :is(html.dark, html[data-theme="dark"]) ::-webkit-scrollbar-thumb { @@ -557,17 +563,29 @@ samp { [data-surface="stage"] { background-color: var(--muted); - background-image: radial-gradient(circle, oklch(0 0 0 / 0.06) 1px, transparent 1px); + background-image: radial-gradient( + circle, + oklch(0 0 0 / 0.06) 1px, + transparent 1px + ); background-size: 20px 20px; } :is(html.dark, html[data-theme="dark"]) [data-surface="stage"] { - background-image: radial-gradient(circle, oklch(1 0 0 / 0.07) 1px, transparent 1px); + background-image: radial-gradient( + circle, + oklch(1 0 0 / 0.07) 1px, + transparent 1px + ); } @media (prefers-color-scheme: dark) { html[data-theme="system"] [data-surface="stage"] { - background-image: radial-gradient(circle, oklch(1 0 0 / 0.07) 1px, transparent 1px); + background-image: radial-gradient( + circle, + oklch(1 0 0 / 0.07) 1px, + transparent 1px + ); } } @@ -604,7 +622,7 @@ samp { .pdpp-display-lg { font-family: var(--font-sans); - font-size: 3.75rem; /* 60px */ + font-size: 3.75rem; /* 60px */ font-weight: 600; line-height: 1.05; letter-spacing: -0.03em; @@ -612,7 +630,7 @@ samp { .pdpp-display { font-family: var(--font-sans); - font-size: 2.5rem; /* 40px */ + font-size: 2.5rem; /* 40px */ font-weight: 600; line-height: 1.08; letter-spacing: -0.025em; @@ -620,7 +638,7 @@ samp { .pdpp-heading { font-family: var(--font-sans); - font-size: 1.25rem; /* 20px */ + font-size: 1.25rem; /* 20px */ font-weight: 600; line-height: 1.3; letter-spacing: -0.01em; @@ -628,7 +646,7 @@ samp { .pdpp-title { font-family: var(--font-sans); - font-size: 0.875rem; /* 14px */ + font-size: 0.875rem; /* 14px */ font-weight: 600; line-height: 1.4; letter-spacing: 0; @@ -636,7 +654,7 @@ samp { .pdpp-body-lg { font-family: var(--font-sans); - font-size: 1.125rem; /* 18px */ + font-size: 1.125rem; /* 18px */ font-weight: 400; line-height: 1.625; letter-spacing: 0; @@ -644,7 +662,7 @@ samp { .pdpp-body { font-family: var(--font-sans); - font-size: 0.875rem; /* 14px */ + font-size: 0.875rem; /* 14px */ font-weight: 400; line-height: 1.6; letter-spacing: 0; @@ -652,7 +670,7 @@ samp { .pdpp-label { font-family: var(--font-sans); - font-size: 0.75rem; /* 12px */ + font-size: 0.75rem; /* 12px */ font-weight: 500; line-height: 1.4; letter-spacing: 0; @@ -660,7 +678,7 @@ samp { .pdpp-caption { font-family: var(--font-sans); - font-size: 0.75rem; /* 12px */ + font-size: 0.75rem; /* 12px */ font-weight: 400; line-height: 1.5; letter-spacing: 0; @@ -695,7 +713,9 @@ samp { border-top-color: var(--data-list-divider); border-bottom-color: var(--data-list-divider); } -:is(html.dark, html[data-theme="dark"]) .pdpp-data-list > :where(:not(:last-child)) { +:is(html.dark, html[data-theme="dark"]) + .pdpp-data-list + > :where(:not(:last-child)) { border-color: var(--data-list-divider); } @media (prefers-color-scheme: dark) { @@ -734,19 +754,26 @@ samp { color: var(--status-neutral-fg); } -:is(html.dark, html[data-theme="dark"]) .pdpp-status-badge[data-status-tone="success"] { +:is(html.dark, html[data-theme="dark"]) + .pdpp-status-badge[data-status-tone="success"] { background-color: var(--status-success-bg); - box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--success) 30%, transparent); + box-shadow: inset 0 0 0 1px + color-mix(in oklab, var(--success) 30%, transparent); } -:is(html.dark, html[data-theme="dark"]) .pdpp-status-badge[data-status-tone="danger"] { +:is(html.dark, html[data-theme="dark"]) + .pdpp-status-badge[data-status-tone="danger"] { background-color: var(--status-danger-bg); - box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--destructive) 32%, transparent); + box-shadow: inset 0 0 0 1px + color-mix(in oklab, var(--destructive) 32%, transparent); } -:is(html.dark, html[data-theme="dark"]) .pdpp-status-badge[data-status-tone="warning"] { +:is(html.dark, html[data-theme="dark"]) + .pdpp-status-badge[data-status-tone="warning"] { background-color: var(--status-warning-bg); - box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--warning) 30%, transparent); + box-shadow: inset 0 0 0 1px + color-mix(in oklab, var(--warning) 30%, transparent); } -:is(html.dark, html[data-theme="dark"]) .pdpp-status-badge[data-status-tone="neutral"] { +:is(html.dark, html[data-theme="dark"]) + .pdpp-status-badge[data-status-tone="neutral"] { background-color: var(--status-neutral-bg); box-shadow: inset 0 0 0 1px var(--border-strong); } @@ -754,15 +781,18 @@ samp { @media (prefers-color-scheme: dark) { html[data-theme="system"] .pdpp-status-badge[data-status-tone="success"] { background-color: var(--status-success-bg); - box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--success) 30%, transparent); + box-shadow: inset 0 0 0 1px + color-mix(in oklab, var(--success) 30%, transparent); } html[data-theme="system"] .pdpp-status-badge[data-status-tone="danger"] { background-color: var(--status-danger-bg); - box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--destructive) 32%, transparent); + box-shadow: inset 0 0 0 1px + color-mix(in oklab, var(--destructive) 32%, transparent); } html[data-theme="system"] .pdpp-status-badge[data-status-tone="warning"] { background-color: var(--status-warning-bg); - box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--warning) 30%, transparent); + box-shadow: inset 0 0 0 1px + color-mix(in oklab, var(--warning) 30%, transparent); } html[data-theme="system"] .pdpp-status-badge[data-status-tone="neutral"] { background-color: var(--status-neutral-bg); diff --git a/packages/pdpp-brand/docs.css b/packages/pdpp-brand/docs.css index d14664d24..67cca66b6 100644 --- a/packages/pdpp-brand/docs.css +++ b/packages/pdpp-brand/docs.css @@ -60,17 +60,22 @@ #nd-subnav { height: 3rem; - background: color-mix(in oklab, var(--background) 92%, var(--surface-tint) / 78%); + background: color-mix( + in oklab, + var(--background) 92%, + var(--surface-tint) / + 78% + ); border-bottom-color: var(--border); backdrop-filter: blur(10px); } @media (min-width: 768px) { - #nd-subnav button[aria-label='Open Sidebar'] { + #nd-subnav button[aria-label="Open Sidebar"] { display: none; } - #nd-sidebar button[aria-label='Open Sidebar'] { + #nd-sidebar button[aria-label="Open Sidebar"] { display: none; } } @@ -124,7 +129,11 @@ #nd-sidebar { background: - linear-gradient(to bottom, color-mix(in oklab, var(--human-wash) 58%, var(--surface-tint)), transparent 18%), + linear-gradient( + to bottom, + color-mix(in oklab, var(--human-wash) 58%, var(--surface-tint)), + transparent 18% + ), color-mix(in oklab, var(--background) 97%, var(--surface-tint)); border-right-color: var(--border); box-shadow: none; @@ -141,17 +150,17 @@ #nd-sidebar a, #nd-sidebar button, #nd-sidebar summary, -#nd-sidebar [role='button'] { +#nd-sidebar [role="button"] { border-radius: 0.5rem; } -#nd-sidebar a[data-active='true'], -#nd-sidebar button[data-active='true'] { +#nd-sidebar a[data-active="true"], +#nd-sidebar button[data-active="true"] { background: var(--muted); color: var(--foreground); } -#nd-sidebar [data-active='true']::before { +#nd-sidebar [data-active="true"]::before { background: var(--human); } @@ -164,7 +173,7 @@ font-size: 0.8125rem; } -#nd-sidebar [class*='mb-1 px-2 mt-6'] { +#nd-sidebar [class*="mb-1 px-2 mt-6"] { font-family: var(--font-mono); font-size: 0.62rem; font-weight: 600; @@ -323,7 +332,9 @@ border-radius: 1rem; background: var(--muted) !important; color: var(--muted-foreground) !important; - box-shadow: 0 1px 2px rgb(0 0 0 / 0.04), 0 1px 3px rgb(0 0 0 / 0.02); + box-shadow: + 0 1px 2px rgb(0 0 0 / 0.04), + 0 1px 3px rgb(0 0 0 / 0.02); } .pdpp-docs-body figure.shiki pre, @@ -392,11 +403,16 @@ color: var(--muted-foreground); } -#nd-toc a[data-active='true'] { +#nd-toc a[data-active="true"] { color: var(--foreground); } [data-toc-popover] header { border-bottom-color: var(--border); - background: color-mix(in oklab, var(--background) 90%, var(--surface-tint) / 78%); + background: color-mix( + in oklab, + var(--background) 90%, + var(--surface-tint) / + 78% + ); } diff --git a/packages/pdpp-brand/ink-carbon.css b/packages/pdpp-brand/ink-carbon.css index 8b14e0a4b..02f428f26 100644 --- a/packages/pdpp-brand/ink-carbon.css +++ b/packages/pdpp-brand/ink-carbon.css @@ -55,7 +55,7 @@ This import provides the fallback for contexts where the Next.js font variable is not available (static exports, storybook, jest). The @import is ignored if the variable font is already loaded. */ -@import url('https://fonts.googleapis.com/css2?family=Schibsted+Grotesk:ital,wght@0,400..900;1,400..700&display=swap'); +@import url("https://fonts.googleapis.com/css2?family=Schibsted+Grotesk:ital,wght@0,400..900;1,400..700&display=swap"); /* ─── Light mode (paper) ──────────────────────────────────────────── */ :root { @@ -64,8 +64,11 @@ in apps/console/src/app/layout.tsx. It falls back to the Google Fonts CDN import in tokens.css if the font variable is not present (e.g. in static export or test environments). */ - --font-sans: var(--ink-carbon-sans, "Schibsted Grotesk"), ui-sans-serif, system-ui, sans-serif; - --font-mono: "JetBrains Mono", "JetBrains Mono Variable", ui-monospace, monospace; + --font-sans: + var(--ink-carbon-sans, "Schibsted Grotesk"), ui-sans-serif, system-ui, + sans-serif; + --font-mono: + "JetBrains Mono", "JetBrains Mono Variable", ui-monospace, monospace; /* ─── Paper + ink ────────────────────────────────────────────── */ --background: oklch(0.985 0.004 90); @@ -86,7 +89,7 @@ --human-tint: oklch(0.55 0.11 45 / 0.08); /* ─── Carbon — the duplicate the server keeps ────────────────── */ - --carbon-outline: oklch(0.46 0.11 255 / 0.40); + --carbon-outline: oklch(0.46 0.11 255 / 0.4); --carbon-fill: oklch(0.46 0.11 255 / 0.09); --carbon-offset: 9px; @@ -103,20 +106,20 @@ --ring: oklch(0.46 0.11 255); /* ─── State (spent, never sprayed) ──────────────────────────── */ - --success: oklch(0.50 0.11 158); /* recorded / active */ - --success-tint: oklch(0.50 0.11 158 / 0.10); - --warning: oklch(0.58 0.13 70); /* expiring */ - --warning-tint: oklch(0.58 0.13 70 / 0.10); - --destructive: oklch(0.52 0.16 27); /* destructive acts only */ + --success: oklch(0.5 0.11 158); /* recorded / active */ + --success-tint: oklch(0.5 0.11 158 / 0.1); + --warning: oklch(0.58 0.13 70); /* expiring */ + --warning-tint: oklch(0.58 0.13 70 / 0.1); + --destructive: oklch(0.52 0.16 27); /* destructive acts only */ --destructive-foreground: oklch(0.99 0.002 90); --destructive-tint: oklch(0.52 0.16 27 / 0.08); /* ─── Geometry — square paper ────────────────────────────────── */ - --radius: 0px; /* sheets, cards, panels = no rounding */ - --radius-sm: 0px; /* override brand sm */ - --radius-md: 0px; /* override brand md */ - --radius-lg: 0px; /* override brand lg */ - --radius-control: 2px; /* the ONLY rounding: inputs, buttons */ + --radius: 0px; /* sheets, cards, panels = no rounding */ + --radius-sm: 0px; /* override brand sm */ + --radius-md: 0px; /* override brand md */ + --radius-lg: 0px; /* override brand lg */ + --radius-control: 2px; /* the ONLY rounding: inputs, buttons */ /* ─── Motion ─────────────────────────────────────────────────── */ --duration-instant: 0ms; @@ -124,7 +127,7 @@ --duration-base: 200ms; --duration-moderate: 300ms; --duration-slow: 500ms; - --motion-state: var(--duration-fast) ease; /* button/badge state transitions */ + --motion-state: var(--duration-fast) ease; /* button/badge state transitions */ /* ─── Layout ─────────────────────────────────────────────────── */ --pdpp-sidebar-width: 220px; @@ -133,7 +136,7 @@ --primary-wash: var(--primary-tint); --human-wash: var(--human-tint); --success-wash: var(--success-tint); - --success-wash-strong: oklch(0.50 0.11 158 / 0.16); + --success-wash-strong: oklch(0.5 0.11 158 / 0.16); --warning-wash: var(--warning-tint); --verified-wash: var(--success-tint); --edu-fg: oklch(0.45 0.06 210); @@ -146,7 +149,9 @@ } @media (min-width: 64rem) { - :root { --pdpp-sidebar-width: 240px; } + :root { + --pdpp-sidebar-width: 240px; + } } /* ─── Reduced motion ─────────────────────────────────────────────── */ @@ -199,22 +204,22 @@ --accent: oklch(0.26 0.007 262); --accent-foreground: oklch(0.95 0.005 262); --border: oklch(0.29 0.008 262); - --border-strong: oklch(0.40 0.008 262); + --border-strong: oklch(0.4 0.008 262); --input: oklch(0.34 0.008 262); --ring: oklch(0.74 0.13 255); --success: oklch(0.76 0.13 158); --success-tint: oklch(0.76 0.13 158 / 0.14); - --warning: oklch(0.80 0.14 75); - --warning-tint: oklch(0.80 0.14 75 / 0.13); - --destructive: oklch(0.70 0.16 27); + --warning: oklch(0.8 0.14 75); + --warning-tint: oklch(0.8 0.14 75 / 0.13); + --destructive: oklch(0.7 0.16 27); --destructive-foreground: oklch(0.15 0.01 262); - --destructive-tint: oklch(0.70 0.16 27 / 0.12); + --destructive-tint: oklch(0.7 0.16 27 / 0.12); --primary-wash: var(--primary-tint); --human-wash: var(--human-tint); --success-wash: var(--success-tint); - --success-wash-strong: oklch(0.76 0.13 158 / 0.20); + --success-wash-strong: oklch(0.76 0.13 158 / 0.2); --warning-wash: var(--warning-tint); --verified-wash: var(--success-tint); --edu-fg: oklch(0.78 0.06 210); diff --git a/packages/read-core/src/index.js b/packages/read-core/src/index.js index 02d5d9188..7dfab9dea 100644 --- a/packages/read-core/src/index.js +++ b/packages/read-core/src/index.js @@ -6,7 +6,7 @@ const DEFAULT_RECORD_PREVIEW_CHAR_LIMIT = 1792; const DEFAULT_RECORD_PREVIEW_FOOTER_RESERVE = 96; const DEFAULT_RECORD_PREVIEW_MIN_RECORD_CHARS = 24; const DEFAULT_RECORD_PREVIEW_TRUNCATED_MARKER = - 'record_preview_truncated=true; followup=rerun_limit; machine envelope in structuredContent.data'; + "record_preview_truncated=true; followup=rerun_limit; machine envelope in structuredContent.data"; const DEFAULT_FIELD_WINDOW_LIMIT_CHARS = 2048; const DEFAULT_FIELD_WINDOW_LIMIT = 8; const DEFAULT_BINARY_FIELD_LIMIT = 8; @@ -14,15 +14,15 @@ const DEFAULT_JSON_FIELD_LIMIT = 8; const DEFAULT_JSON_PREVIEW_CHAR_LIMIT = 512; const OMIT_FIELD_KEYS = new Set([ - 'id', - 'record_id', - 'recordId', - 'connection_id', - 'connector_instance_id', - 'connector_key', - 'stream', - 'metadata', - '_meta', + "id", + "record_id", + "recordId", + "connection_id", + "connector_instance_id", + "connector_key", + "stream", + "metadata", + "_meta", ]); export function stableInlineJson(value) { @@ -30,10 +30,10 @@ export function stableInlineJson(value) { } export function truncateText(value, limit) { - const text = String(value ?? ''); - if (!Number.isFinite(limit) || limit <= 0) return ''; + const text = String(value ?? ""); + if (!Number.isFinite(limit) || limit <= 0) return ""; if (text.length <= limit) return text; - if (limit <= 1) return '…'.slice(0, limit); + if (limit <= 1) return "…".slice(0, limit); return `${text.slice(0, limit - 1)}…`; } @@ -50,10 +50,9 @@ export function summarizeRecordEvidence(body, label, options = {}) { const charLimit = options.charLimit ?? DEFAULT_RECORD_PREVIEW_CHAR_LIMIT; const footerReserve = options.footerReserve ?? DEFAULT_RECORD_PREVIEW_FOOTER_RESERVE; const minRecordChars = options.minRecordChars ?? DEFAULT_RECORD_PREVIEW_MIN_RECORD_CHARS; - const truncatedMarker = - options.truncatedMarker ?? DEFAULT_RECORD_PREVIEW_TRUNCATED_MARKER; + const truncatedMarker = options.truncatedMarker ?? DEFAULT_RECORD_PREVIEW_TRUNCATED_MARKER; const records = extractRecordRows(body); - const hasMore = envelopeField(body, 'has_more') === true ? ' has_more=true.' : ''; + const hasMore = envelopeField(body, "has_more") === true ? " has_more=true." : ""; const handles = formatEnvelopeHandles(body); if (records.length === 0) return `${label}: 0 record(s).${handles}`; @@ -86,7 +85,7 @@ export function summarizeRecordEvidence(body, label, options = {}) { ); } - return lines.join('\n'); + return lines.join("\n"); } export function summarizeFieldWindowEvidence(body) { @@ -101,33 +100,33 @@ export function summarizeFieldWindowEvidence(body) { const nextCursor = firstString(window.next_cursor); const previousCursor = firstString(window.previous_cursor); const range = - start !== null && end !== null ? `chars ${start}..${end}` : start !== null ? `chars ${start}..` : 'chars'; - const identity = [connectionId, stream, recordId].filter(Boolean).join('/'); + start !== null && end !== null ? `chars ${start}..${end}` : start !== null ? `chars ${start}..` : "chars"; + const identity = [connectionId, stream, recordId].filter(Boolean).join("/"); const cursorText = [ nextCursor ? `next_cursor=${formatScalar(nextCursor)}` : null, previousCursor ? `previous_cursor=${formatScalar(previousCursor)}` : null, ] .filter(Boolean) - .join(' '); + .join(" "); return [ - `record=${identity || 'unknown'} field=${fieldPath || 'unknown'} ${range} complete=${complete}`, + `record=${identity || "unknown"} field=${fieldPath || "unknown"} ${range} complete=${complete}`, cursorText, - String(window.text ?? ''), + String(window.text ?? ""), ] .filter(Boolean) - .join('\n'); + .join("\n"); } export function formatEnvelopeHandles(body) { const parts = []; - const nextCursor = envelopeField(body, 'next_cursor'); + const nextCursor = envelopeField(body, "next_cursor"); if (nextCursor) parts.push(`next_cursor=${formatScalar(nextCursor)}`); - const nextChangesSince = envelopeField(body, 'next_changes_since'); + const nextChangesSince = envelopeField(body, "next_changes_since"); if (nextChangesSince) parts.push(`next_changes_since=${formatScalar(nextChangesSince)}`); const count = envelopeCount(body); if (count) parts.push(`count=${count}`); - return parts.length > 0 ? ` ${parts.join(' ')}.` : ''; + return parts.length > 0 ? ` ${parts.join(" ")}.` : ""; } export function buildRecordContentLadder(record, options = {}) { @@ -153,8 +152,8 @@ export function buildRecordContentLadder(record, options = {}) { connection_id: identity.connectionId, stream: identity.stream, record_id: identity.recordId, - handle_semantics: 'live_lookup', - record_uri: encodeResourceUri('record', { + handle_semantics: "live_lookup", + record_uri: encodeResourceUri("record", { connection_id: identity.connectionId, stream: identity.stream, record_id: identity.recordId, @@ -172,8 +171,8 @@ export function buildRecordSetContentLadder(body, options = {}) { .slice(0, options.recordLimit ?? DEFAULT_RECORD_PREVIEW_LIMIT); if (records.length === 0) return null; return { - kind: 'record_set', - read_tool: options.readTool ?? 'read_record_field', + kind: "record_set", + read_tool: options.readTool ?? "read_record_field", records, }; } @@ -194,20 +193,20 @@ export function encodeContentHandle(kind, payload) { export function decodeContentHandle(handle, expectedKind) { const payload = JSON.parse(base64UrlDecode(String(handle))); - if (!payload || typeof payload !== 'object' || Array.isArray(payload)) { - throw new Error('Content handle is malformed.'); + if (!payload || typeof payload !== "object" || Array.isArray(payload)) { + throw new Error("Content handle is malformed."); } if (payload.v !== 1 || payload.kind !== expectedKind) { - throw new Error('Content handle has the wrong kind or version.'); + throw new Error("Content handle has the wrong kind or version."); } return payload; } export function sanitizeRecordForEvidence(record) { - if (!record || typeof record !== 'object' || Array.isArray(record)) return record; + if (!record || typeof record !== "object" || Array.isArray(record)) return record; const sanitized = {}; for (const [key, value] of Object.entries(record)) { - if (key === 'data' && value && typeof value === 'object' && !Array.isArray(value)) { + if (key === "data" && value && typeof value === "object" && !Array.isArray(value)) { sanitized[key] = sanitizePayloadObject(value); continue; } @@ -217,7 +216,7 @@ export function sanitizeRecordForEvidence(record) { } export function binaryFieldMetadata(fieldPath, value) { - if (typeof fieldPath !== 'string' || fieldPath.length === 0 || OMIT_FIELD_KEYS.has(fieldPath)) return null; + if (typeof fieldPath !== "string" || fieldPath.length === 0 || OMIT_FIELD_KEYS.has(fieldPath)) return null; const blob = blobRefMetadata(value); if (blob) { @@ -225,8 +224,8 @@ export function binaryFieldMetadata(fieldPath, value) { field_path: fieldPath, binary_field: true, text_like: false, - handle_semantics: 'live_lookup', - preview_status: 'binary-only', + handle_semantics: "live_lookup", + preview_status: "binary-only", ...blob, }; } @@ -236,9 +235,9 @@ export function binaryFieldMetadata(fieldPath, value) { field_path: fieldPath, binary_field: true, text_like: false, - handle_semantics: 'live_lookup', - preview_status: 'binary-only', - encoding: 'base64', + handle_semantics: "live_lookup", + preview_status: "binary-only", + encoding: "base64", size_chars: value.length, }; } @@ -250,13 +249,7 @@ function recordContentIdentity(record, fallback = {}) { const payload = objectValue(record?.data) || objectValue(record?.record) || objectValue(record); const directId = firstString(fallback?.id, record?.id, record?.result_id, record?.record_id, record?.recordId); const parsed = directId ? parseRecordResultId(directId) : null; - const stream = firstString( - fallback?.stream, - record?.stream, - record?.stream_name, - payload?.stream, - parsed?.stream - ); + const stream = firstString(fallback?.stream, record?.stream, record?.stream_name, payload?.stream, parsed?.stream); const recordId = firstString( fallback?.recordId, fallback?.record_id, @@ -288,11 +281,11 @@ function recordContentFields(record, identity, options) { .map(([fieldPath, value]) => ({ field_path: fieldPath, text_like: true, - handle_semantics: 'live_lookup', - preview_status: value.length > options.windowLimitChars ? 'truncated' : 'complete', + handle_semantics: "live_lookup", + preview_status: value.length > options.windowLimitChars ? "truncated" : "complete", size_chars: value.length, read: { - tool: 'read_record_field', + tool: "read_record_field", args: { id: identity.id, field_path: fieldPath, @@ -300,7 +293,7 @@ function recordContentFields(record, identity, options) { limit_chars: options.windowLimitChars, }, }, - resource_uri: options.encodeResourceUri('field-window', { + resource_uri: options.encodeResourceUri("field-window", { connection_id: identity.connectionId, stream: identity.stream, record_id: identity.recordId, @@ -330,12 +323,12 @@ function recordContentJsonFields(record, identity, options) { field_path: fieldPath, json_field: true, text_like: false, - handle_semantics: 'live_lookup', - preview_status: rendered.length > options.jsonPreviewChars ? 'truncated' : 'complete', + handle_semantics: "live_lookup", + preview_status: rendered.length > options.jsonPreviewChars ? "truncated" : "complete", size_chars: rendered.length, preview_text: truncateText(rendered, options.jsonPreviewChars), read: { - tool: 'fetch', + tool: "fetch", args: { id: identity.id, fields: [fieldPath], @@ -346,8 +339,8 @@ function recordContentJsonFields(record, identity, options) { } function isJsonEvidenceField(fieldPath, value) { - if (typeof fieldPath !== 'string' || fieldPath.length === 0 || OMIT_FIELD_KEYS.has(fieldPath)) return false; - if (!value || typeof value !== 'object') return false; + if (typeof fieldPath !== "string" || fieldPath.length === 0 || OMIT_FIELD_KEYS.has(fieldPath)) return false; + if (!value || typeof value !== "object") return false; if (blobRefMetadata(value)) return false; return true; } @@ -363,12 +356,12 @@ function sanitizePayloadObject(payload) { function sanitizeEvidenceValue(value) { if (Array.isArray(value)) return value.map((entry) => sanitizeEvidenceValue(entry)); - if (!value || typeof value !== 'object') return value; + if (!value || typeof value !== "object") return value; const blob = blobRefMetadata(value); if (blob) { return { text_like: false, - preview_status: 'binary-only', + preview_status: "binary-only", ...blob, }; } @@ -377,22 +370,22 @@ function sanitizeEvidenceValue(value) { function isContentStringField(fieldPath, value) { return ( - typeof fieldPath === 'string' && + typeof fieldPath === "string" && fieldPath.length > 0 && !OMIT_FIELD_KEYS.has(fieldPath) && - typeof value === 'string' && + typeof value === "string" && value.length > 0 && !isLargeBase64Field(fieldPath, value) && - !fieldPath.includes('/') && - !fieldPath.includes('\\') && - fieldPath !== '.' && - fieldPath !== '..' && - !fieldPath.includes('..') + !fieldPath.includes("/") && + !fieldPath.includes("\\") && + fieldPath !== "." && + fieldPath !== ".." && + !fieldPath.includes("..") ); } function isLargeBase64Field(fieldPath, value) { - if (typeof value !== 'string' || value.length < 256) return false; + if (typeof value !== "string" || value.length < 256) return false; if (value.length % 4 !== 0) return false; if (new Set(value).size < 4) return false; return /^[A-Za-z0-9+/]+={0,2}$/.test(value); @@ -420,8 +413,8 @@ function blobRefMetadata(value) { } function parseRecordResultId(id) { - const slash = id.indexOf('/'); - const colon = id.indexOf(':', slash + 1); + const slash = id.indexOf("/"); + const colon = id.indexOf(":", slash + 1); if (colon <= 0) return null; if (slash > 0) { return { @@ -440,15 +433,15 @@ function parseRecordResultId(id) { function envelopeField(body, key) { if (body && Object.hasOwn(body, key)) return body[key]; if (body?.meta && Object.hasOwn(body.meta, key)) return body.meta[key]; - if (body?.data && typeof body.data === 'object' && !Array.isArray(body.data) && Object.hasOwn(body.data, key)) { + if (body?.data && typeof body.data === "object" && !Array.isArray(body.data) && Object.hasOwn(body.data, key)) { return body.data[key]; } return undefined; } function envelopeCount(body) { - const count = envelopeField(body, 'count'); - if (!count || typeof count !== 'object') return null; + const count = envelopeField(body, "count"); + if (!count || typeof count !== "object") return null; const kind = firstString(count.kind); const value = numberValue(count.value); if (!kind || value === null) return null; @@ -456,20 +449,20 @@ function envelopeCount(body) { } function objectValue(value) { - return value && typeof value === 'object' && !Array.isArray(value) ? value : null; + return value && typeof value === "object" && !Array.isArray(value) ? value : null; } function firstString(...values) { for (const value of values) { - if (typeof value === 'string' && value.length > 0) return value; + if (typeof value === "string" && value.length > 0) return value; } return null; } function numberValue(...values) { for (const value of values) { - if (typeof value === 'number' && Number.isFinite(value)) return value; - if (typeof value === 'string' && value.trim() !== '') { + if (typeof value === "number" && Number.isFinite(value)) return value; + if (typeof value === "string" && value.trim() !== "") { const parsed = Number.parseInt(value, 10); if (Number.isFinite(parsed)) return parsed; } @@ -482,9 +475,9 @@ function formatScalar(value) { } function base64UrlEncode(value) { - return Buffer.from(value, 'utf8').toString('base64url'); + return Buffer.from(value, "utf8").toString("base64url"); } function base64UrlDecode(value) { - return Buffer.from(value, 'base64url').toString('utf8'); + return Buffer.from(value, "base64url").toString("utf8"); } diff --git a/reference-implementation/lib/spine.ts b/reference-implementation/lib/spine.ts index 95c2da835..f215ab84b 100644 --- a/reference-implementation/lib/spine.ts +++ b/reference-implementation/lib/spine.ts @@ -1158,9 +1158,7 @@ function summarizeEvents(events: readonly SpineEventRecord[]): SpineSummary | nu source_kind: source?.kind ?? null, source_id: source?.id ?? null, connector_id: findFirstConnectorId(events), - ...(connectionId - ? { connection_id: connectionId, connector_instance_id: connectionId } - : {}), + ...(connectionId ? { connection_id: connectionId, connector_instance_id: connectionId } : {}), actor_type: first.actor_type, actor_id: first.actor_id, ...pickBrowserSurfaceFields(browserSurface), diff --git a/reference-implementation/runtime/browser-surface/remote-surface-optional.ts b/reference-implementation/runtime/browser-surface/remote-surface-optional.ts index 72bc0a9c3..75951a3b0 100644 --- a/reference-implementation/runtime/browser-surface/remote-surface-optional.ts +++ b/reference-implementation/runtime/browser-surface/remote-surface-optional.ts @@ -30,9 +30,7 @@ let cached: RemoteSurfaceAvailability | null = null; export async function loadRemoteSurface(): Promise { if (cached) return cached; try { - const module = (await import( - "@opendatalabs/remote-surface/leases" - )) as RemoteSurfaceModule; + const module = (await import("@opendatalabs/remote-surface/leases")) as RemoteSurfaceModule; cached = { available: true, module }; } catch (error) { cached = { diff --git a/reference-implementation/server/connector-instance-write-coordinator.ts b/reference-implementation/server/connector-instance-write-coordinator.ts index 9f4cf3b4a..aaf7501b5 100644 --- a/reference-implementation/server/connector-instance-write-coordinator.ts +++ b/reference-implementation/server/connector-instance-write-coordinator.ts @@ -78,7 +78,10 @@ export function __setConnectorInstancePostgresLockPoolForTest( /** Narrow deterministic ordering seam; production never installs a hook. */ export function __setConnectorInstanceWritePhaseHookForTest( hook: - | ((stage: "before_key_acquire" | "after_acquire", context: { connectorInstanceId: string }) => Promise | void) + | (( + stage: "before_key_acquire" | "after_acquire", + context: { connectorInstanceId: string } + ) => Promise | void) | null ): void { writePhaseHookForTest = hook; diff --git a/reference-implementation/server/connector-summary-read-model.ts b/reference-implementation/server/connector-summary-read-model.ts index f39ef8bc3..30c42e31b 100644 --- a/reference-implementation/server/connector-summary-read-model.ts +++ b/reference-implementation/server/connector-summary-read-model.ts @@ -1582,18 +1582,40 @@ async function foldConnectorSummaryStreamFactsOnce( const foldStore = createStreamFactsFoldStore(); const rows = (await store.listEvidence(connectorInstanceIds === null ? {} : { connectorInstanceIds })) as Row[]; if (rows.length === 0) { - return { casRejectedInstanceIds: [], folded: 0, participants: 0, refused: 0, incomplete: false, resumeAfterSeq: null }; + return { + casRejectedInstanceIds: [], + folded: 0, + participants: 0, + refused: 0, + incomplete: false, + resumeAfterSeq: null, + }; } const maxSeq = await foldStore.readMaxTerminalEventSeq(connectorInstanceIds); const participants = rows.filter((row) => rowNeedsFoldParticipation(row, maxSeq)); if (participants.length === 0) { - return { casRejectedInstanceIds: [], folded: 0, participants: 0, refused: 0, incomplete: false, resumeAfterSeq: null }; + return { + casRejectedInstanceIds: [], + folded: 0, + participants: 0, + refused: 0, + incomplete: false, + resumeAfterSeq: null, + }; } if (maxSeq == null) { const casRejectedInstanceIds = await stampZeroCheckpointForBootstrap(foldStore, participants); - return { casRejectedInstanceIds, folded: 0, participants: participants.length, refused: 0, incomplete: false, resumeAfterSeq: null }; + return { + casRejectedInstanceIds, + folded: 0, + participants: participants.length, + refused: 0, + incomplete: false, + resumeAfterSeq: null, + }; } - const { factsByInstance, checkpointByInstance, casBaselineByInstance, generationByInstance, sinceSeq } = seedFoldState(participants); + const { factsByInstance, checkpointByInstance, casBaselineByInstance, generationByInstance, sinceSeq } = + seedFoldState(participants); // Test-only: see `testOnlyFoldPauseHook` — a no-op unless a test installs // a hook. Held here, immediately after the baseline (checkpointByInstance) // is captured and before this pass's own terminal-event read/CAS write — @@ -1658,8 +1680,12 @@ async function foldConnectorSummaryStreamFactsOnce( foldStore, instanceId, facts, - sourceGenerationCurrent ? writeSeq : checkpointByInstance.get(instanceId) ?? 0, - terminalFactsCurrent ? null : sourceGenerationCurrent ? REASON_CODES.TERMINAL_FOLD_INCOMPLETE : REASON_CODES.TERMINAL_FACTS_HISTORICAL, + sourceGenerationCurrent ? writeSeq : (checkpointByInstance.get(instanceId) ?? 0), + terminalFactsCurrent + ? null + : sourceGenerationCurrent + ? REASON_CODES.TERMINAL_FOLD_INCOMPLETE + : REASON_CODES.TERMINAL_FACTS_HISTORICAL, terminalFactsCurrent, checkpointByInstance, casBaselineByInstance diff --git a/reference-implementation/server/explore-timeline-substrate.ts b/reference-implementation/server/explore-timeline-substrate.ts index fea85f384..7f8297c68 100644 --- a/reference-implementation/server/explore-timeline-substrate.ts +++ b/reference-implementation/server/explore-timeline-substrate.ts @@ -293,10 +293,11 @@ function sqliteFetchSnapshotAnchor(): { snapshotSeq: number; snapshotAt: string type PartitionPageDirection = "asc" | "desc"; -const PARTITION_PAGE_DIRECTION_SQL: Record = { - asc: { order: "ASC", seek: ">" }, - desc: { order: "DESC", seek: "<" }, -}; +const PARTITION_PAGE_DIRECTION_SQL: Record = + { + asc: { order: "ASC", seek: ">" }, + desc: { order: "DESC", seek: "<" }, + }; function partitionPageDirection(input: PartitionPageInput): PartitionPageDirection { return input.direction === "asc" ? "asc" : "desc"; @@ -340,7 +341,10 @@ function appendSqlitePartitionPageCursor( binds.push(...cursorValues); } -function sqlitePartitionPageQuery(input: PartitionPageInput): { readonly sql: string; readonly binds: (string | number)[] } { +function sqlitePartitionPageQuery(input: PartitionPageInput): { + readonly sql: string; + readonly binds: (string | number)[]; +} { const { connectorId, stream, snapshotSeq, limit } = input; const direction = partitionPageDirection(input); const directionSql = PARTITION_PAGE_DIRECTION_SQL[direction]; @@ -910,7 +914,14 @@ async function postgresFetchUpcoming(input: UpcomingFetchInput): Promise { if (req.tokenInfo?.pdpp_token_kind === "mcp_package") { - return buildPackageMcpServerOptions( - req, - res, - resource, - internalBase - ); + return buildPackageMcpServerOptions(req, res, resource, internalBase); } return buildStandaloneMcpServerOptions(resource, internalBase, inboundToken); } diff --git a/reference-implementation/server/streaming/cdp-companion.ts b/reference-implementation/server/streaming/cdp-companion.ts index 4a73060af..93c1c6b24 100644 --- a/reference-implementation/server/streaming/cdp-companion.ts +++ b/reference-implementation/server/streaming/cdp-companion.ts @@ -81,8 +81,13 @@ function mouseCommand(type: string, x: number, y: number, button: string, clickC } const MOUSE_COMMANDS: Record = { - mousemove: ({ x, y }) => [{ method: "Input.dispatchMouseEvent", params: { type: "mouseMoved", x, y, button: "none" } }], - click: ({ x, y, button }) => [mouseCommand("mousePressed", x, y, button), mouseCommand("mouseReleased", x, y, button)], + mousemove: ({ x, y }) => [ + { method: "Input.dispatchMouseEvent", params: { type: "mouseMoved", x, y, button: "none" } }, + ], + click: ({ x, y, button }) => [ + mouseCommand("mousePressed", x, y, button), + mouseCommand("mouseReleased", x, y, button), + ], dblclick: ({ x, y }) => [ mouseCommand("mousePressed", x, y, "left"), mouseCommand("mouseReleased", x, y, "left"), @@ -144,7 +149,12 @@ function mapKeyboardEventToCdp(event: WireInput): CdpCommand[] { const isPrintable = event.key.length === 1; const type = keyboardType(event.action, isPrintable); const text = type === "keyDown" ? { text: event.key } : {}; - return [{ method: "Input.dispatchKeyEvent", params: { type, key: event.key, code, modifiers, ...text, ...virtualKeyCodeParams(vk) } }]; + return [ + { + method: "Input.dispatchKeyEvent", + params: { type, key: event.key, code, modifiers, ...text, ...virtualKeyCodeParams(vk) }, + }, + ]; } function mapTouchEventToCdp(event: WireInput): CdpCommand[] { diff --git a/scripts/check-console-health-surfaces.mjs b/scripts/check-console-health-surfaces.mjs index 9daecc616..682e83336 100644 --- a/scripts/check-console-health-surfaces.mjs +++ b/scripts/check-console-health-surfaces.mjs @@ -32,7 +32,8 @@ const RULES = [ }, { id: "legacy-next-action", - pattern: /\b(?:formatNextAction\(|summary\.next_action|connection_health\?\.next_action|connectionHealth\?\.next_action)/, + pattern: + /\b(?:formatNextAction\(|summary\.next_action|connection_health\?\.next_action|connectionHealth\?\.next_action)/, message: "owner surface must derive CTAs from rendered_verdict.required_actions", }, { diff --git a/scripts/check-dist-tag-posture.mjs b/scripts/check-dist-tag-posture.mjs index 7e65c67d9..06541bb8a 100644 --- a/scripts/check-dist-tag-posture.mjs +++ b/scripts/check-dist-tag-posture.mjs @@ -26,36 +26,36 @@ // The reason is printed and the check exits 0, but the // finding is still reported so the waiver stays honest and visible. -import { readdirSync, readFileSync } from 'node:fs'; -import path from 'node:path'; -import process from 'node:process'; -import { execFile } from 'node:child_process'; -import { fileURLToPath } from 'node:url'; -import { promisify } from 'node:util'; +import { readdirSync, readFileSync } from "node:fs"; +import path from "node:path"; +import process from "node:process"; +import { execFile } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import { promisify } from "node:util"; const execFileAsync = promisify(execFile); const repoRoot = process.cwd(); -export const placeholderVersion = '0.0.0'; +export const placeholderVersion = "0.0.0"; function readJson(filePath) { - return JSON.parse(readFileSync(filePath, 'utf8')); + return JSON.parse(readFileSync(filePath, "utf8")); } -function listPublishablePackageNames(rootDir = 'packages') { +function listPublishablePackageNames(rootDir = "packages") { const root = path.join(repoRoot, rootDir); const names = []; for (const entry of readdirSync(root, { withFileTypes: true })) { if (!entry.isDirectory()) { continue; } - const manifestPath = path.join(root, entry.name, 'package.json'); + const manifestPath = path.join(root, entry.name, "package.json"); let manifest; try { manifest = readJson(manifestPath); } catch { continue; } - if (manifest.name?.startsWith('@pdpp/') && manifest.private !== true) { + if (manifest.name?.startsWith("@pdpp/") && manifest.private !== true) { names.push(manifest.name); } } @@ -73,7 +73,7 @@ function listPublishablePackageNames(rootDir = 'packages') { export function classifyDistTagPosture(packageName, distTags) { if (!distTags) { return { - status: 'skip', + status: "skip", detail: `${packageName}: not published yet or registry unreachable; nothing to verify`, }; } @@ -82,33 +82,33 @@ export function classifyDistTagPosture(packageName, distTags) { if (!latest) { if (beta) { return { - status: 'hazard', + status: "hazard", detail: `${packageName}: no "latest" dist-tag while "beta" is ${beta}; a bare install has no stable target`, }; } return { - status: 'skip', + status: "skip", detail: `${packageName}: no dist-tags published yet; nothing to verify`, }; } if (latest === placeholderVersion) { return { - status: 'hazard', + status: "hazard", detail: `${packageName}: "latest" resolves to placeholder ${placeholderVersion}${ - beta ? ` while "beta" is ${beta}` : '' + beta ? ` while "beta" is ${beta}` : "" }; a bare \`npm install ${packageName}\` would install an empty package`, }; } return { - status: 'ok', - detail: `${packageName}: "latest" is ${latest}${beta ? ` ("beta" is ${beta})` : ''}`, + status: "ok", + detail: `${packageName}: "latest" is ${latest}${beta ? ` ("beta" is ${beta})` : ""}`, }; } async function fetchDistTags(packageName) { try { - const { stdout } = await execFileAsync('npm', ['view', packageName, 'dist-tags', '--json'], { - encoding: 'utf8', + const { stdout } = await execFileAsync("npm", ["view", packageName, "dist-tags", "--json"], { + encoding: "utf8", maxBuffer: 1024 * 1024, }); const trimmed = stdout.trim(); @@ -119,7 +119,7 @@ async function fetchDistTags(packageName) { } catch (error) { // `npm view` exits non-zero for E404 (package does not exist) and for // network failures. Both are non-fatal "skip" signals here. - const text = `${error.stdout ?? ''}${error.stderr ?? ''}${error.message ?? ''}`; + const text = `${error.stdout ?? ""}${error.stderr ?? ""}${error.message ?? ""}`; if (/E404|404 Not Found|ENOTFOUND|ETIMEDOUT|ECONNREFUSED|EAI_AGAIN|network/i.test(text)) { return null; } @@ -129,8 +129,8 @@ async function fetchDistTags(packageName) { async function main() { const args = new Set(process.argv.slice(2)); - const requireReachable = args.has('--require-reachable'); - const asJson = args.has('--json'); + const requireReachable = args.has("--require-reachable"); + const asJson = args.has("--json"); const waiver = process.env.PDPP_RELEASE_DIST_TAG_WAIVER?.trim(); const packageNames = listPublishablePackageNames(); @@ -140,21 +140,21 @@ async function main() { results.push({ packageName, distTags, ...classifyDistTagPosture(packageName, distTags) }); } - const hazards = results.filter((result) => result.status === 'hazard'); - const skips = results.filter((result) => result.status === 'skip'); + const hazards = results.filter((result) => result.status === "hazard"); + const skips = results.filter((result) => result.status === "skip"); if (asJson) { process.stdout.write(`${JSON.stringify({ results, waiver: waiver ?? null }, null, 2)}\n`); } else { for (const result of results) { - const marker = result.status === 'ok' ? 'OK ' : result.status === 'skip' ? 'SKIP' : 'FAIL'; + const marker = result.status === "ok" ? "OK " : result.status === "skip" ? "SKIP" : "FAIL"; process.stdout.write(`${marker} ${result.detail}\n`); } } if (requireReachable && skips.length > 0) { process.stderr.write( - `\nFAIL --require-reachable was set but ${skips.length} package(s) could not be verified against the registry.\n`, + `\nFAIL --require-reachable was set but ${skips.length} package(s) could not be verified against the registry.\n` ); process.exit(1); } @@ -167,19 +167,19 @@ async function main() { if (waiver) { process.stdout.write( `\nPDPP dist-tag posture WAIVED: ${hazards.length} known hazard(s) acknowledged.\nReason: ${waiver}\n` + - 'Clear the waiver and run the owner promotion step in docs/reference/package-release-policy.md to make this pass cleanly.\n', + "Clear the waiver and run the owner promotion step in docs/reference/package-release-policy.md to make this pass cleanly.\n" ); process.exit(0); } process.stderr.write( - '\nPDPP dist-tag posture check failed:\n' + - hazards.map((hazard) => `- ${hazard.detail}`).join('\n') + - '\n\nThe default install target is broken for operators. Either:\n' + - ' 1. promote the package to a real stable `latest` (owner release-readiness step in\n' + - ' docs/reference/package-release-policy.md), or\n' + + "\nPDPP dist-tag posture check failed:\n" + + hazards.map((hazard) => `- ${hazard.detail}`).join("\n") + + "\n\nThe default install target is broken for operators. Either:\n" + + " 1. promote the package to a real stable `latest` (owner release-readiness step in\n" + + " docs/reference/package-release-policy.md), or\n" + ' 2. set PDPP_RELEASE_DIST_TAG_WAIVER="" to acknowledge it explicitly.\n' + - 'Until then, a bare `npm install` of the package hands operators the empty placeholder.\n', + "Until then, a bare `npm install` of the package hands operators the empty placeholder.\n" ); process.exit(1); } diff --git a/scripts/check-flyio-deploy-env.mjs b/scripts/check-flyio-deploy-env.mjs index 398580e93..226d5dea3 100644 --- a/scripts/check-flyio-deploy-env.mjs +++ b/scripts/check-flyio-deploy-env.mjs @@ -19,19 +19,19 @@ // node scripts/check-flyio-deploy-env.mjs --core core.env // node scripts/check-flyio-deploy-env.mjs --core core.env --json -import { readFileSync } from 'node:fs'; -import process from 'node:process'; -import { fileURLToPath } from 'node:url'; +import { readFileSync } from "node:fs"; +import process from "node:process"; +import { fileURLToPath } from "node:url"; const PLACEHOLDER_RE = /<[^>]+>/; -const FORBIDDEN_CORE_KEYS = ['PORT', 'AS_PORT', 'RS_PORT', 'PDPP_AS_URL', 'PDPP_RS_URL']; +const FORBIDDEN_CORE_KEYS = ["PORT", "AS_PORT", "RS_PORT", "PDPP_AS_URL", "PDPP_RS_URL"]; export function isPlaceholder(value) { if (value === undefined || value === null) { return true; } const trimmed = String(value).trim(); - if (trimmed === '') { + if (trimmed === "") { return true; } return PLACEHOLDER_RE.test(trimmed); @@ -39,21 +39,18 @@ export function isPlaceholder(value) { export function parseEnv(text) { const env = {}; - for (const rawLine of text.split('\n')) { + for (const rawLine of text.split("\n")) { const line = rawLine.trim(); - if (line === '' || line.startsWith('#')) { + if (line === "" || line.startsWith("#")) { continue; } - const eq = line.indexOf('='); + const eq = line.indexOf("="); if (eq === -1) { continue; } const key = line.slice(0, eq).trim(); let value = line.slice(eq + 1).trim(); - if ( - (value.startsWith('"') && value.endsWith('"')) || - (value.startsWith("'") && value.endsWith("'")) - ) { + if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { value = value.slice(1, -1); } if (key) { @@ -69,34 +66,29 @@ export function evaluateFlyioCoreEnv(coreEnv) { const origin = coreEnv.PDPP_REFERENCE_ORIGIN; if (isPlaceholder(origin)) { violations.push( - 'PDPP_REFERENCE_ORIGIN is not set. Set it to the public Core HTTPS origin ' + - '(e.g. https://.fly.dev).', + "PDPP_REFERENCE_ORIGIN is not set. Set it to the public Core HTTPS origin " + "(e.g. https://.fly.dev)." ); } else if (!/^https:\/\//.test(String(origin).trim())) { - violations.push( - `PDPP_REFERENCE_ORIGIN must be an https:// origin for a public deploy; got "${origin}".`, - ); + violations.push(`PDPP_REFERENCE_ORIGIN must be an https:// origin for a public deploy; got "${origin}".`); } if (isPlaceholder(coreEnv.PDPP_OWNER_PASSWORD)) { violations.push( - 'PDPP_OWNER_PASSWORD is empty. A public origin with no owner password serves the ' + - 'dashboard and device-approval surfaces anonymously. Set a non-empty secret.', + "PDPP_OWNER_PASSWORD is empty. A public origin with no owner password serves the " + + "dashboard and device-approval surfaces anonymously. Set a non-empty secret." ); } if (isPlaceholder(coreEnv.PDPP_DATABASE_URL) && isPlaceholder(coreEnv.DATABASE_URL)) { violations.push( - 'No durable database URL is set. Use Fly Postgres (`fly launch --db`) or attach Postgres ' + - 'with PDPP_DATABASE_URL/DATABASE_URL so data survives restart.', + "No durable database URL is set. Use Fly Postgres (`fly launch --db`) or attach Postgres " + + "with PDPP_DATABASE_URL/DATABASE_URL so data survives restart." ); } for (const key of FORBIDDEN_CORE_KEYS) { if (!isPlaceholder(coreEnv[key])) { - violations.push( - `core ${key} must not be set as a Fly app variable; the platform-core image keeps it internal.`, - ); + violations.push(`core ${key} must not be set as a Fly app variable; the platform-core image keeps it internal.`); } } @@ -105,7 +97,7 @@ export function evaluateFlyioCoreEnv(coreEnv) { function loadEnvFile(path) { try { - return parseEnv(readFileSync(path, 'utf8')); + return parseEnv(readFileSync(path, "utf8")); } catch (err) { process.stderr.write(`Error reading env file "${path}": ${err.message}\n`); process.exit(1); @@ -118,32 +110,30 @@ function main() { let jsonOutput = false; for (let i = 0; i < args.length; i++) { - if (args[i] === '--core' && args[i + 1]) { + if (args[i] === "--core" && args[i + 1]) { corePath = args[++i]; - } else if (args[i] === '--json') { + } else if (args[i] === "--json") { jsonOutput = true; } } if (!corePath) { - process.stderr.write( - 'Usage: node scripts/check-flyio-deploy-env.mjs --core [--json]\n', - ); + process.stderr.write("Usage: node scripts/check-flyio-deploy-env.mjs --core [--json]\n"); process.exit(1); } const violations = evaluateFlyioCoreEnv(loadEnvFile(corePath)); if (jsonOutput) { - process.stdout.write(JSON.stringify({ ok: violations.length === 0, violations }, null, 2) + '\n'); + process.stdout.write(JSON.stringify({ ok: violations.length === 0, violations }, null, 2) + "\n"); } else if (violations.length === 0) { - process.stdout.write('Fly.io deploy env contract: OK\n'); + process.stdout.write("Fly.io deploy env contract: OK\n"); } else { process.stdout.write(`Fly.io deploy env contract: ${violations.length} violation(s)\n\n`); for (const violation of violations) { process.stdout.write(` • ${violation}\n`); } - process.stdout.write('\nSee deploy/flyio/README.md for the correct values.\n'); + process.stdout.write("\nSee deploy/flyio/README.md for the correct values.\n"); process.exit(1); } } diff --git a/scripts/check-package-release-policy.mjs b/scripts/check-package-release-policy.mjs index 8a46eddd1..9bb80a8a4 100644 --- a/scripts/check-package-release-policy.mjs +++ b/scripts/check-package-release-policy.mjs @@ -2,20 +2,20 @@ // Copyright The PDP-Connect Contributors // SPDX-License-Identifier: Apache-2.0 -import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'; -import path from 'node:path'; -import process from 'node:process'; -import { fileURLToPath } from 'node:url'; +import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; +import path from "node:path"; +import process from "node:process"; +import { fileURLToPath } from "node:url"; const repoRoot = process.cwd(); const expectedPublishConfig = { - access: 'public', + access: "public", provenance: false, - registry: 'https://registry.npmjs.org/', - tag: 'latest', + registry: "https://registry.npmjs.org/", + tag: "latest", }; -const expectedRepositoryUrl = 'git+https://github.com/PDP-Connect/pdpp.git'; -const expectedNodeEngine = '>=22.14.0'; +const expectedRepositoryUrl = "git+https://github.com/PDP-Connect/pdpp.git"; +const expectedNodeEngine = ">=22.14.0"; // The release train publishes a single channel: 0.x versions on npm's default // `latest` dist-tag, released from `main` (see docs/reference/package-release-policy.md). @@ -24,20 +24,20 @@ const expectedNodeEngine = '>=22.14.0'; // the retired `@beta` tag on every publishable-package install command in the // active docs below, so the docs cannot silently regress operators onto the // dead prerelease channel. -const retiredDistTag = 'beta'; +const retiredDistTag = "beta"; const installDocRoots = [ - 'README.md', - 'docs', - 'reference-implementation/README.md', - 'reference-implementation/docs', - 'apps/site/content/docs', - 'packages/cli/README.md', - 'packages/local-collector/README.md', - 'packages/mcp-server/README.md', + "README.md", + "docs", + "reference-implementation/README.md", + "reference-implementation/docs", + "apps/site/content/docs", + "packages/cli/README.md", + "packages/local-collector/README.md", + "packages/mcp-server/README.md", ]; function readJson(filePath) { - return JSON.parse(readFileSync(filePath, 'utf8')); + return JSON.parse(readFileSync(filePath, "utf8")); } function listMarkdownFiles(relativeRoot) { @@ -49,7 +49,7 @@ function listMarkdownFiles(relativeRoot) { function walk(target) { const stat = statSync(target); if (stat.isFile()) { - if (target.endsWith('.md')) { + if (target.endsWith(".md")) { out.push(path.relative(repoRoot, target)); } return; @@ -58,7 +58,7 @@ function listMarkdownFiles(relativeRoot) { return; } for (const entry of readdirSync(target, { withFileTypes: true })) { - if (entry.name === 'node_modules' || entry.name === '.next') { + if (entry.name === "node_modules" || entry.name === ".next") { continue; } walk(path.join(target, entry.name)); @@ -78,7 +78,7 @@ export function findRetiredTagInstallDocReferences({ packageNames, docFiles, rea // package name (specs, comments) are intentionally ignored. const installCommand = /\b(?:npm\s+(?:i|install|add)|pnpm\s+(?:add|install|dlx|exec)|npx)\b/; for (const docFile of docFiles) { - const lines = readFile(docFile).split('\n'); + const lines = readFile(docFile).split("\n"); for (const line of lines) { // Skip shell comments and Markdown headings (`# ...`): they describe an // install command in prose but cannot execute one, so a bare package @@ -95,7 +95,7 @@ export function findRetiredTagInstallDocReferences({ packageNames, docFiles, rea const retiredReference = new RegExp(`${escapeRegExp(packageName)}@${retiredDistTag}(?![\\w.-])`); if (retiredReference.test(line)) { problems.push( - `${docFile} installs ${packageName} with the retired @${retiredDistTag} dist-tag (the release train publishes only to latest): ${line.trim()}`, + `${docFile} installs ${packageName} with the retired @${retiredDistTag} dist-tag (the release train publishes only to latest): ${line.trim()}` ); } } @@ -105,13 +105,13 @@ export function findRetiredTagInstallDocReferences({ packageNames, docFiles, rea } function escapeRegExp(value) { - return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } -function listPackageJsonFiles(rootDir = '.') { +function listPackageJsonFiles(rootDir = ".") { const root = path.join(repoRoot, rootDir); const out = []; - const ignoredDirs = new Set(['.git', '.next', '.turbo', 'build', 'coverage', 'dist', 'node_modules']); + const ignoredDirs = new Set([".git", ".next", ".turbo", "build", "coverage", "dist", "node_modules"]); function walk(dir) { for (const entry of readdirSync(dir, { withFileTypes: true })) { const fullPath = path.join(dir, entry.name); @@ -121,7 +121,7 @@ function listPackageJsonFiles(rootDir = '.') { } continue; } - if (entry.name === 'package.json') { + if (entry.name === "package.json") { out.push(path.relative(repoRoot, fullPath)); } } @@ -146,13 +146,13 @@ export function findPublishableWorkspaceDependencyErrors(publishablePackages) { optionalDependencies: manifest.optionalDependencies, peerDependencies: manifest.peerDependencies, })) { - if (!dependencies || typeof dependencies !== 'object') { + if (!dependencies || typeof dependencies !== "object") { continue; } for (const [name, range] of Object.entries(dependencies)) { - if (typeof range === 'string' && range.startsWith('workspace:')) { + if (typeof range === "string" && range.startsWith("workspace:")) { problems.push( - `${file} ${section}.${name} uses ${range}; publishable packages must use registry-resolvable ranges`, + `${file} ${section}.${name} uses ${range}; publishable packages must use registry-resolvable ranges` ); } } @@ -164,9 +164,9 @@ export function findPublishableWorkspaceDependencyErrors(publishablePackages) { const isMainModule = process.argv[1] === fileURLToPath(import.meta.url); const errors = []; -const rootPackage = readJson(path.join(repoRoot, 'package.json')); +const rootPackage = readJson(path.join(repoRoot, "package.json")); if (rootPackage.private !== true) { - fail(errors, 'root package.json must remain private:true so the workspace root cannot publish'); + fail(errors, "root package.json must remain private:true so the workspace root cannot publish"); } const packageFiles = listPackageJsonFiles(); @@ -176,11 +176,14 @@ const packages = packageFiles.map((file) => ({ manifest: readJson(path.join(repoRoot, file)), })); const publishablePackages = packages - .filter(({ dir, manifest }) => dir.startsWith('packages/') && manifest.name?.startsWith('@pdpp/') && manifest.private !== true) + .filter( + ({ dir, manifest }) => + dir.startsWith("packages/") && manifest.name?.startsWith("@pdpp/") && manifest.private !== true + ) .sort((a, b) => a.dir.localeCompare(b.dir)); for (const { file, manifest } of packages) { - if (file === 'package.json') { + if (file === "package.json") { continue; } const hasPublishConfig = manifest.publishConfig !== undefined; @@ -197,7 +200,7 @@ for (const { file, dir, manifest } of publishablePackages) { if (!deepEqual(manifest.publishConfig, expectedPublishConfig)) { fail(errors, `${file} publishConfig must match ${JSON.stringify(expectedPublishConfig)}`); } - if (manifest.version !== '0.0.0') { + if (manifest.version !== "0.0.0") { fail(errors, `${file} version must stay at 0.0.0; semantic-release owns published versions`); } if (!manifest.description) { @@ -218,7 +221,7 @@ for (const { file, dir, manifest } of publishablePackages) { if (manifest.engines?.node !== expectedNodeEngine) { fail(errors, `${file} engines.node must be ${expectedNodeEngine}`); } - if (!existsSync(path.join(repoRoot, dir, 'README.md'))) { + if (!existsSync(path.join(repoRoot, dir, "README.md"))) { fail(errors, `${dir}/README.md must exist for the npm package`); } } @@ -227,39 +230,39 @@ for (const problem of findPublishableWorkspaceDependencyErrors(publishablePackag fail(errors, problem); } -const releaseConfig = readFileSync(path.join(repoRoot, '.releaserc.yaml'), 'utf8'); -const releaseWorkflow = readFileSync(path.join(repoRoot, '.github/workflows/semantic-release.yml'), 'utf8'); +const releaseConfig = readFileSync(path.join(repoRoot, ".releaserc.yaml"), "utf8"); +const releaseWorkflow = readFileSync(path.join(repoRoot, ".github/workflows/semantic-release.yml"), "utf8"); const configuredPackageRoots = [...releaseConfig.matchAll(/pkgRoot:\s*"([^"]+)"/g)].map((match) => match[1]).sort(); const publishableRoots = publishablePackages.map(({ dir }) => dir).sort(); if (!deepEqual(configuredPackageRoots, publishableRoots)) { fail( errors, - `.releaserc.yaml @semantic-release/npm pkgRoot list ${JSON.stringify(configuredPackageRoots)} must match publishable packages ${JSON.stringify(publishableRoots)}`, + `.releaserc.yaml @semantic-release/npm pkgRoot list ${JSON.stringify(configuredPackageRoots)} must match publishable packages ${JSON.stringify(publishableRoots)}` ); } if (!/id-token:\s*write/.test(releaseWorkflow)) { - fail(errors, '.github/workflows/semantic-release.yml must grant id-token: write for npm trusted publishing'); + fail(errors, ".github/workflows/semantic-release.yml must grant id-token: write for npm trusted publishing"); } if (/\b(NPM_TOKEN|NODE_AUTH_TOKEN)\b/.test(releaseWorkflow)) { - fail(errors, 'semantic-release workflow must not use NPM_TOKEN/NODE_AUTH_TOKEN in the normal release path'); + fail(errors, "semantic-release workflow must not use NPM_TOKEN/NODE_AUTH_TOKEN in the normal release path"); } if (!/pnpm release:policy-check/.test(releaseWorkflow)) { - fail(errors, 'semantic-release quality job must run pnpm release:policy-check'); + fail(errors, "semantic-release quality job must run pnpm release:policy-check"); } // Single release channel: releases are cut from `main` onto npm's default // `latest` dist-tag. A prerelease branch (the old `beta` lane) is a second // moving part that goes stale; the policy forbids reintroducing one. if (/^\s*prerelease\s*:/m.test(releaseConfig) || /^\s*-\s*name:\s*beta\s*$/m.test(releaseConfig)) { - fail(errors, '.releaserc.yaml must declare a single release channel from main (no prerelease/beta branch)'); + fail(errors, ".releaserc.yaml must declare a single release channel from main (no prerelease/beta branch)"); } if (!/branches:\s*\[main\]/.test(releaseWorkflow)) { - fail(errors, 'semantic-release workflow must trigger on push to main (single release channel)'); + fail(errors, "semantic-release workflow must trigger on push to main (single release channel)"); } -const packageScopes = publishablePackages.map(({ manifest }) => manifest.name.replace(/^@pdpp\//, '')); +const packageScopes = publishablePackages.map(({ manifest }) => manifest.name.replace(/^@pdpp\//, "")); for (const scope of packageScopes) { if (!new RegExp(`scope:\\s*${scope}\\b`).test(releaseConfig)) { fail(errors, `.releaserc.yaml release-notes grouping must include Conventional Commit scope "${scope}"`); @@ -278,7 +281,7 @@ const installDocFiles = installDocRoots.flatMap((root) => listMarkdownFiles(root const retiredTagInstallDocReferences = findRetiredTagInstallDocReferences({ packageNames: publishablePackageNames, docFiles: installDocFiles, - readFile: (relativePath) => readFileSync(path.join(repoRoot, relativePath), 'utf8'), + readFile: (relativePath) => readFileSync(path.join(repoRoot, relativePath), "utf8"), }); for (const problem of retiredTagInstallDocReferences) { fail(errors, problem); @@ -290,7 +293,7 @@ export const policyErrors = errors; if (isMainModule) { if (errors.length > 0) { - console.error('PDPP package release policy check failed:'); + console.error("PDPP package release policy check failed:"); for (const error of errors) { console.error(`- ${error}`); } @@ -298,6 +301,6 @@ if (isMainModule) { } console.log( - `PDPP package release policy OK: ${publishablePackages.map(({ manifest }) => manifest.name).join(', ') || 'no publishable packages'}`, + `PDPP package release policy OK: ${publishablePackages.map(({ manifest }) => manifest.name).join(", ") || "no publishable packages"}` ); } diff --git a/scripts/check-railway-deploy-env.mjs b/scripts/check-railway-deploy-env.mjs index b3e1d9555..d9b1fd5a7 100644 --- a/scripts/check-railway-deploy-env.mjs +++ b/scripts/check-railway-deploy-env.mjs @@ -35,9 +35,9 @@ // // Exit codes: 0 = contract satisfied; 1 = one or more violations. -import { readFileSync } from 'node:fs'; -import process from 'node:process'; -import { fileURLToPath } from 'node:url'; +import { readFileSync } from "node:fs"; +import process from "node:process"; +import { fileURLToPath } from "node:url"; // A placeholder is an unfilled template value, not a real configured value. // `deploy/railway/env.example` uses angle-bracket placeholders and Railway @@ -49,7 +49,7 @@ export function isPlaceholder(value) { return true; } const trimmed = String(value).trim(); - if (trimmed === '') { + if (trimmed === "") { return true; } return PLACEHOLDER_RE.test(trimmed); @@ -59,27 +59,24 @@ export function isPlaceholder(value) { // `http://${{reference.RAILWAY_PRIVATE_DOMAIN}}:${{reference.PORT}}` are Railway // reference variables: real, intentional bindings Railway resolves at deploy time. export function isRailwayReference(value) { - return typeof value === 'string' && /\$\{\{[^}]+\}\}/.test(value.trim()); + return typeof value === "string" && /\$\{\{[^}]+\}\}/.test(value.trim()); } export function parseEnv(text) { const env = {}; - for (const rawLine of text.split('\n')) { + for (const rawLine of text.split("\n")) { const line = rawLine.trim(); - if (line === '' || line.startsWith('#')) { + if (line === "" || line.startsWith("#")) { continue; } - const eq = line.indexOf('='); + const eq = line.indexOf("="); if (eq === -1) { continue; } const key = line.slice(0, eq).trim(); let value = line.slice(eq + 1).trim(); // Strip a single layer of matching quotes. - if ( - (value.startsWith('"') && value.endsWith('"')) || - (value.startsWith("'") && value.endsWith("'")) - ) { + if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { value = value.slice(1, -1); } if (key) { @@ -92,7 +89,7 @@ export function parseEnv(text) { // The default SQLite path is NOT under the documented Railway volume mount, so // leaving it as-is silently loses data on redeploy. Any path under a real mount // (anything that is not the known unmounted default) is accepted. -export const UNMOUNTED_SQLITE_DEFAULT = '/var/lib/pdpp/pdpp.sqlite'; +export const UNMOUNTED_SQLITE_DEFAULT = "/var/lib/pdpp/pdpp.sqlite"; function value(env, key) { return env[key]; @@ -113,7 +110,7 @@ function requireSame(violations, consoleEnv, referenceEnv, key, label) { String(consoleValue).trim() !== String(referenceValue).trim() ) { violations.push( - `${label}: console and reference ${key} must match; got "${consoleValue}" and "${referenceValue}".`, + `${label}: console and reference ${key} must match; got "${consoleValue}" and "${referenceValue}".` ); } } @@ -123,17 +120,11 @@ function isRailwayPrivateUrl(url, port) { return false; } if (isRailwayReference(url)) { - return new RegExp(`^http://\\$\\{\\{[^}]*RAILWAY_PRIVATE_DOMAIN[^}]*\\}\\}:${port}$`).test( - String(url).trim(), - ); + return new RegExp(`^http://\\$\\{\\{[^}]*RAILWAY_PRIVATE_DOMAIN[^}]*\\}\\}:${port}$`).test(String(url).trim()); } try { const parsed = new URL(String(url).trim()); - return ( - parsed.protocol === 'http:' && - parsed.hostname.endsWith('.railway.internal') && - parsed.port === String(port) - ); + return parsed.protocol === "http:" && parsed.hostname.endsWith(".railway.internal") && parsed.port === String(port); } catch { return false; } @@ -143,9 +134,9 @@ function isRailwayPrivateUrlWithPortReference(url, portReference) { if (isPlaceholder(url) || !isRailwayReference(url)) { return false; } - const escaped = String(portReference).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const escaped = String(portReference).replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); return new RegExp(`^http://\\$\\{\\{[^}]*RAILWAY_PRIVATE_DOMAIN[^}]*\\}\\}:\\$\\{\\{${escaped}\\}\\}$`).test( - String(url).trim(), + String(url).trim() ); } @@ -176,20 +167,16 @@ export function evaluateRailwayDeployEnv(env) { // 1. Single public HTTPS origin. const origin = env.PDPP_REFERENCE_ORIGIN; if (isPlaceholder(origin)) { - violations.push( - 'PDPP_REFERENCE_ORIGIN is not set. Set it to the public console HTTPS origin on both services.', - ); + violations.push("PDPP_REFERENCE_ORIGIN is not set. Set it to the public console HTTPS origin on both services."); } else if (!/^https:\/\//.test(origin.trim())) { - violations.push( - `PDPP_REFERENCE_ORIGIN must be an https:// origin for a public deploy; got "${origin}".`, - ); + violations.push(`PDPP_REFERENCE_ORIGIN must be an https:// origin for a public deploy; got "${origin}".`); } // 2. Owner gate required. if (isPlaceholder(env.PDPP_OWNER_PASSWORD)) { violations.push( - 'PDPP_OWNER_PASSWORD is empty. A public origin with no owner password serves the dashboard and ' + - 'device-approval surfaces anonymously. Set a non-empty secret.', + "PDPP_OWNER_PASSWORD is empty. A public origin with no owner password serves the dashboard and " + + "device-approval surfaces anonymously. Set a non-empty secret." ); } @@ -198,48 +185,46 @@ export function evaluateRailwayDeployEnv(env) { // style deployments may use PDPP_CREDENTIAL_ENCRYPTION_KEY_FILE. if (!hasCredentialKeyProvider(env)) { violations.push( - 'PDPP_CREDENTIAL_ENCRYPTION_KEY or PDPP_CREDENTIAL_ENCRYPTION_KEY_FILE is not set. ' + - 'Static-secret connector setup will be blocked until an instance-level credential key provider exists.', + "PDPP_CREDENTIAL_ENCRYPTION_KEY or PDPP_CREDENTIAL_ENCRYPTION_KEY_FILE is not set. " + + "Static-secret connector setup will be blocked until an instance-level credential key provider exists." ); } // 4. Console private AS/RS targets. if (isPlaceholder(env.PDPP_AS_URL)) { violations.push( - 'PDPP_AS_URL is not set. The console must reach the private reference Authorization Server ' + - '(e.g. http://${{reference.RAILWAY_PRIVATE_DOMAIN}}:${{reference.PORT}}).', + "PDPP_AS_URL is not set. The console must reach the private reference Authorization Server " + + "(e.g. http://${{reference.RAILWAY_PRIVATE_DOMAIN}}:${{reference.PORT}})." ); } if (isPlaceholder(env.PDPP_RS_URL)) { violations.push( - 'PDPP_RS_URL is not set. The console must reach the private reference Resource Server ' + - '(e.g. http://${{reference.RAILWAY_PRIVATE_DOMAIN}}:7663).', + "PDPP_RS_URL is not set. The console must reach the private reference Resource Server " + + "(e.g. http://${{reference.RAILWAY_PRIVATE_DOMAIN}}:7663)." ); } // 5. Storage chosen explicitly and durably. - const configuredBackend = (env.PDPP_STORAGE_BACKEND ?? '').trim().toLowerCase(); - const backend = configuredBackend || (hasDurableDatabaseUrl(env) ? 'postgres' : ''); - if (backend === 'postgres') { + const configuredBackend = (env.PDPP_STORAGE_BACKEND ?? "").trim().toLowerCase(); + const backend = configuredBackend || (hasDurableDatabaseUrl(env) ? "postgres" : ""); + if (backend === "postgres") { const url = env.PDPP_DATABASE_URL; if (isPlaceholder(url) && !isRailwayReference(url)) { - violations.push( - 'Postgres storage requires PDPP_DATABASE_URL (e.g. ${{Postgres.DATABASE_URL}}).', - ); + violations.push("Postgres storage requires PDPP_DATABASE_URL (e.g. ${{Postgres.DATABASE_URL}})."); } - } else if (backend === 'sqlite') { - const dbPath = (env.PDPP_DB_PATH ?? '').trim(); - if (dbPath === '' || dbPath === UNMOUNTED_SQLITE_DEFAULT) { + } else if (backend === "sqlite") { + const dbPath = (env.PDPP_DB_PATH ?? "").trim(); + if (dbPath === "" || dbPath === UNMOUNTED_SQLITE_DEFAULT) { violations.push( `PDPP_STORAGE_BACKEND=sqlite requires PDPP_DB_PATH on a mounted persistent volume. ` + - `"${dbPath || '(unset)'}" is the unmounted default and loses data on redeploy.`, + `"${dbPath || "(unset)"}" is the unmounted default and loses data on redeploy.` ); } } else { violations.push( `PDPP_STORAGE_BACKEND must be "postgres" or "sqlite", or PDPP_DATABASE_URL must be set so Postgres can be inferred; got "${ - env.PDPP_STORAGE_BACKEND ?? '(unset)' - }". The in-memory default is not durable.`, + env.PDPP_STORAGE_BACKEND ?? "(unset)" + }". The in-memory default is not durable.` ); } @@ -250,19 +235,19 @@ export function evaluateRailwayCoreServiceEnv(coreEnv) { const violations = []; const deployEnv = { ...coreEnv, - PDPP_AS_URL: coreEnv.PDPP_AS_URL ?? 'http://127.0.0.1:7662', - PDPP_RS_URL: coreEnv.PDPP_RS_URL ?? 'http://127.0.0.1:7663', + PDPP_AS_URL: coreEnv.PDPP_AS_URL ?? "http://127.0.0.1:7662", + PDPP_RS_URL: coreEnv.PDPP_RS_URL ?? "http://127.0.0.1:7663", }; violations.push( ...evaluateRailwayDeployEnv(deployEnv).filter((violation) => { - return !violation.startsWith('PDPP_AS_URL') && !violation.startsWith('PDPP_RS_URL'); - }), + return !violation.startsWith("PDPP_AS_URL") && !violation.startsWith("PDPP_RS_URL"); + }) ); - for (const key of ['PORT', 'AS_PORT', 'RS_PORT', 'PDPP_AS_URL', 'PDPP_RS_URL']) { + for (const key of ["PORT", "AS_PORT", "RS_PORT", "PDPP_AS_URL", "PDPP_RS_URL"]) { if (!isPlaceholder(coreEnv[key])) { violations.push( - `core ${key} must not be set as a Railway service variable; the railway-core image keeps it internal.`, + `core ${key} must not be set as a Railway service variable; the railway-core image keeps it internal.` ); } } @@ -273,94 +258,100 @@ export function evaluateRailwayCoreServiceEnv(coreEnv) { export function evaluateRailwayServiceEnvs({ consoleEnv, referenceEnv }) { const violations = []; - requireSame(violations, consoleEnv, referenceEnv, 'PDPP_REFERENCE_ORIGIN', 'public origin'); + requireSame(violations, consoleEnv, referenceEnv, "PDPP_REFERENCE_ORIGIN", "public origin"); const origin = consoleEnv.PDPP_REFERENCE_ORIGIN; if (!isPlaceholder(origin) && !/^https:\/\//.test(origin.trim())) { - violations.push( - `public origin: PDPP_REFERENCE_ORIGIN must be an https:// origin; got "${origin}".`, - ); + violations.push(`public origin: PDPP_REFERENCE_ORIGIN must be an https:// origin; got "${origin}".`); } requireExpectedIfSet( violations, consoleEnv, - 'PDPP_REFERENCE_MODE', - 'composed', - 'composed mode: console PDPP_REFERENCE_MODE must be "composed" when set.', + "PDPP_REFERENCE_MODE", + "composed", + 'composed mode: console PDPP_REFERENCE_MODE must be "composed" when set.' ); requireExpectedIfSet( violations, referenceEnv, - 'PDPP_REFERENCE_MODE', - 'composed', - 'composed mode: reference PDPP_REFERENCE_MODE must be "composed" when set.', + "PDPP_REFERENCE_MODE", + "composed", + 'composed mode: reference PDPP_REFERENCE_MODE must be "composed" when set.' ); - requireSame(violations, consoleEnv, referenceEnv, 'PDPP_OWNER_PASSWORD', 'owner gate'); + requireSame(violations, consoleEnv, referenceEnv, "PDPP_OWNER_PASSWORD", "owner gate"); const consoleAsUrl = consoleEnv.PDPP_AS_URL; - if (!isRailwayPrivateUrlWithPortReference(consoleAsUrl, 'reference.PORT')) { + if (!isRailwayPrivateUrlWithPortReference(consoleAsUrl, "reference.PORT")) { violations.push( - 'console PDPP_AS_URL must point at the private Railway reference AS ' + - '(expected http://.railway.internal:${{reference.PORT}}).', + "console PDPP_AS_URL must point at the private Railway reference AS " + + "(expected http://.railway.internal:${{reference.PORT}})." ); } const consoleRsUrl = consoleEnv.PDPP_RS_URL; if (!isRailwayPrivateUrl(consoleRsUrl, 7663)) { violations.push( - 'console PDPP_RS_URL must point at the private Railway reference RS ' + - '(expected http://.railway.internal:7663).', + "console PDPP_RS_URL must point at the private Railway reference RS " + + "(expected http://.railway.internal:7663)." ); } requireExpectedIfSet( violations, consoleEnv, - 'PDPP_ENABLE_DASHBOARD', - '1', - 'console PDPP_ENABLE_DASHBOARD must be "1" when set.', + "PDPP_ENABLE_DASHBOARD", + "1", + 'console PDPP_ENABLE_DASHBOARD must be "1" when set.' ); if (!isPlaceholder(consoleEnv.PORT)) { - violations.push('console PORT must not be set; Railway injects the public service port.'); + violations.push("console PORT must not be set; Railway injects the public service port."); } - requireExpectedIfSet(violations, referenceEnv, 'NODE_ENV', 'production', 'reference NODE_ENV must be "production".'); - requireExpectedIfSet(violations, referenceEnv, 'AS_PORT', '7662', 'reference AS_PORT must be "7662" when set outside Railway.'); - requireExpectedIfSet(violations, referenceEnv, 'RS_PORT', '7663', 'reference RS_PORT must be "7663".'); + requireExpectedIfSet(violations, referenceEnv, "NODE_ENV", "production", 'reference NODE_ENV must be "production".'); + requireExpectedIfSet( + violations, + referenceEnv, + "AS_PORT", + "7662", + 'reference AS_PORT must be "7662" when set outside Railway.' + ); + requireExpectedIfSet(violations, referenceEnv, "RS_PORT", "7663", 'reference RS_PORT must be "7663".'); if (!isPlaceholder(referenceEnv.PORT)) { - violations.push('reference PORT must not be set; Railway injects it and the image maps it to AS_PORT.'); + violations.push("reference PORT must not be set; Railway injects it and the image maps it to AS_PORT."); } requireExpectedIfSet( violations, referenceEnv, - 'PDPP_REFERENCE_OPERATIONAL_DEFAULTS', - '1', - 'reference PDPP_REFERENCE_OPERATIONAL_DEFAULTS must be "1".', + "PDPP_REFERENCE_OPERATIONAL_DEFAULTS", + "1", + 'reference PDPP_REFERENCE_OPERATIONAL_DEFAULTS must be "1".' ); requireExpectedIfSet( violations, referenceEnv, - 'PDPP_RS_URL', - 'http://127.0.0.1:7663', - 'reference PDPP_RS_URL must be "http://127.0.0.1:7663" for hosted-MCP self-calls.', + "PDPP_RS_URL", + "http://127.0.0.1:7663", + 'reference PDPP_RS_URL must be "http://127.0.0.1:7663" for hosted-MCP self-calls.' ); requireExpectedIfSet( violations, referenceEnv, - 'PDPP_EMBEDDING_DOWNLOAD_ALLOWED', - '0', - 'reference PDPP_EMBEDDING_DOWNLOAD_ALLOWED must be "0" for the Core test.', + "PDPP_EMBEDDING_DOWNLOAD_ALLOWED", + "0", + 'reference PDPP_EMBEDDING_DOWNLOAD_ALLOWED must be "0" for the Core test.' ); - violations.push(...evaluateRailwayDeployEnv(referenceEnv).filter((violation) => { - return ( - !violation.startsWith('PDPP_REFERENCE_ORIGIN') && - !violation.startsWith('PDPP_OWNER_PASSWORD') && - !violation.startsWith('PDPP_AS_URL') && - !violation.startsWith('PDPP_RS_URL') - ); - })); + violations.push( + ...evaluateRailwayDeployEnv(referenceEnv).filter((violation) => { + return ( + !violation.startsWith("PDPP_REFERENCE_ORIGIN") && + !violation.startsWith("PDPP_OWNER_PASSWORD") && + !violation.startsWith("PDPP_AS_URL") && + !violation.startsWith("PDPP_RS_URL") + ); + }) + ); return violations; } @@ -368,7 +359,7 @@ export function evaluateRailwayServiceEnvs({ consoleEnv, referenceEnv }) { function readEnvFile(filePath) { let text; try { - text = readFileSync(filePath, 'utf8'); + text = readFileSync(filePath, "utf8"); } catch (error) { throw new Error(`Cannot read env file "${filePath}": ${error?.message ?? error}`); } @@ -377,15 +368,15 @@ function readEnvFile(filePath) { function main(argv) { const args = argv.slice(2); - const json = args.includes('--json'); - const coreFlag = args.indexOf('--core'); - const consoleFlag = args.indexOf('--console'); - const referenceFlag = args.indexOf('--reference'); + const json = args.includes("--json"); + const coreFlag = args.indexOf("--core"); + const consoleFlag = args.indexOf("--console"); + const referenceFlag = args.indexOf("--reference"); const coreFile = coreFlag === -1 ? null : args[coreFlag + 1]; const consoleFile = consoleFlag === -1 ? null : args[consoleFlag + 1]; const referenceFile = referenceFlag === -1 ? null : args[referenceFlag + 1]; - if (coreFile && !coreFile.startsWith('--')) { + if (coreFile && !coreFile.startsWith("--")) { let coreEnv; try { coreEnv = readEnvFile(coreFile); @@ -401,18 +392,16 @@ function main(argv) { process.stdout.write(`Railway core env contract satisfied: core=${coreFile}\n`); } else { process.stderr.write( - `Railway core env contract violations:\n` + - violations.map((v) => `- ${v}`).join('\n') + - '\n', + `Railway core env contract violations:\n` + violations.map((v) => `- ${v}`).join("\n") + "\n" ); } process.exit(ok ? 0 : 1); } - if (!consoleFile || !referenceFile || consoleFile.startsWith('--') || referenceFile.startsWith('--')) { + if (!consoleFile || !referenceFile || consoleFile.startsWith("--") || referenceFile.startsWith("--")) { process.stderr.write( - 'Usage: node scripts/check-railway-deploy-env.mjs --core [--json]\n' + - ' or: node scripts/check-railway-deploy-env.mjs --console --reference [--json]\n', + "Usage: node scripts/check-railway-deploy-env.mjs --core [--json]\n" + + " or: node scripts/check-railway-deploy-env.mjs --console --reference [--json]\n" ); process.exit(1); } @@ -432,17 +421,13 @@ function main(argv) { if (json) { process.stdout.write( - `${JSON.stringify({ ok, console: consoleFile, reference: referenceFile, violations }, null, 2)}\n`, + `${JSON.stringify({ ok, console: consoleFile, reference: referenceFile, violations }, null, 2)}\n` ); } else if (ok) { - process.stdout.write( - `Railway deploy env contract satisfied: console=${consoleFile} reference=${referenceFile}\n`, - ); + process.stdout.write(`Railway deploy env contract satisfied: console=${consoleFile} reference=${referenceFile}\n`); } else { process.stderr.write( - `Railway deploy env contract violations:\n` + - violations.map((v) => `- ${v}`).join('\n') + - '\n', + `Railway deploy env contract violations:\n` + violations.map((v) => `- ${v}`).join("\n") + "\n" ); } diff --git a/scripts/check-railway-ghcr-public.mjs b/scripts/check-railway-ghcr-public.mjs index 8e03eb696..518f8ede7 100644 --- a/scripts/check-railway-ghcr-public.mjs +++ b/scripts/check-railway-ghcr-public.mjs @@ -42,14 +42,12 @@ // Exit codes: 0 = template image PUBLIC (gate clear); 1 = image not pullable // (gate blocked); 2 = bad usage. -import process from 'node:process'; -import { fileURLToPath } from 'node:url'; +import process from "node:process"; +import { fileURLToPath } from "node:url"; // The app-service image, mapped to its Railway service and Dockerfile stage. // Repository path only — no registry host, no tag. -export const TEMPLATE_IMAGES = [ - { image: 'pdp-connect/pdpp/railway-core', service: 'core', stage: 'railway-core' }, -]; +export const TEMPLATE_IMAGES = [{ image: "pdp-connect/pdpp/railway-core", service: "core", stage: "railway-core" }]; // Map an anonymous GHCR pull-token HTTP status onto a package-visibility verdict. // 200 grants a token (package is anonymously readable); 401 means auth required @@ -57,15 +55,15 @@ export const TEMPLATE_IMAGES = [ // unclassified transport result we refuse to treat as "public". export function classifyTokenStatus(status) { if (status === 200) { - return { visibility: 'public', tokenGranted: true }; + return { visibility: "public", tokenGranted: true }; } if (status === 401) { - return { visibility: 'private', tokenGranted: false }; + return { visibility: "private", tokenGranted: false }; } if (status === 403) { - return { visibility: 'absent', tokenGranted: false }; + return { visibility: "absent", tokenGranted: false }; } - return { visibility: 'unknown', tokenGranted: false }; + return { visibility: "unknown", tokenGranted: false }; } // Collapse a token verdict (+ optional tags/list outcome, a required tag pin, @@ -95,11 +93,11 @@ export function classifyProbeResult({ if (!tokenGranted) { ok = false; reason = - visibility === 'private' - ? 'private — anonymous pull token refused (401); owner must flip package visibility to Public' - : visibility === 'absent' - ? 'absent — no such GHCR package path (403); check the image name' - : `unexpected GHCR token status ${tokenStatus}`; + visibility === "private" + ? "private — anonymous pull token refused (401); owner must flip package visibility to Public" + : visibility === "absent" + ? "absent — no such GHCR package path (403); check the image name" + : `unexpected GHCR token status ${tokenStatus}`; } else if (requiredTag && (tagPresent || manifestReadable)) { ok = true; reason = manifestReadable @@ -112,11 +110,11 @@ export function classifyProbeResult({ ok = false; reason = manifestStatus === undefined - ? `public, but required tag "${requiredTag}" is not published (have: ${tagList.join(', ') || 'none'})` - : `public, but required tag "${requiredTag}" is not anonymously readable (manifest status ${manifestStatus}; tags/list has: ${tagList.join(', ') || 'none'})`; + ? `public, but required tag "${requiredTag}" is not published (have: ${tagList.join(", ") || "none"})` + : `public, but required tag "${requiredTag}" is not anonymously readable (manifest status ${manifestStatus}; tags/list has: ${tagList.join(", ") || "none"})`; } else { ok = true; - reason = 'public (anonymously pullable)'; + reason = "public (anonymously pullable)"; } return { image, service, stage, visibility, ok, reason, tags: tagList, manifestStatus }; @@ -133,7 +131,7 @@ export function summarizePublishReadiness(results) { blocked, ownerAction: ready ? null - : 'Flip each blocked package to Public: GitHub -> org pdp-connect -> Packages -> the package -> Change visibility -> Public, then re-run this probe.', + : "Flip each blocked package to Public: GitHub -> org pdp-connect -> Packages -> the package -> Change visibility -> Public, then re-run this probe.", }; } @@ -142,11 +140,11 @@ export function parseArgs(argv) { const rest = argv.slice(2); for (let i = 0; i < rest.length; i += 1) { const arg = rest[i]; - if (arg === '--json') { + if (arg === "--json") { args.json = true; - } else if (arg === '--help' || arg === '-h') { + } else if (arg === "--help" || arg === "-h") { args.help = true; - } else if (arg === '--tag') { + } else if (arg === "--tag") { args.tag = rest[i + 1]; i += 1; } else { @@ -159,7 +157,7 @@ export function parseArgs(argv) { const USAGE = `Usage: node scripts/check-railway-ghcr-public.mjs [--json] [--tag ] Probes the Railway template image for anonymous (public) GHCR pullability: - ${TEMPLATE_IMAGES.map((i) => `ghcr.io/${i.image} (${i.service})`).join('\n ')} + ${TEMPLATE_IMAGES.map((i) => `ghcr.io/${i.image} (${i.service})`).join("\n ")} Exit codes: 0 = public (publish gate clear); 1 = blocked; 2 = bad usage.`; @@ -181,7 +179,7 @@ async function probeImage({ image, service, stage }, requiredTag) { const body = await tokenResult.response.json(); const headers = { Authorization: `Bearer ${body.token}`, - Accept: 'application/json', + Accept: "application/json", }; const tagsResult = await ghcrGet(`https://ghcr.io/v2/${image}/tags/list`, { ...headers, @@ -195,11 +193,11 @@ async function probeImage({ image, service, stage }, requiredTag) { const manifestResult = await ghcrGet(`https://ghcr.io/v2/${image}/manifests/${requiredTag}`, { ...headers, Accept: [ - 'application/vnd.oci.image.index.v1+json', - 'application/vnd.docker.distribution.manifest.list.v2+json', - 'application/vnd.oci.image.manifest.v1+json', - 'application/vnd.docker.distribution.manifest.v2+json', - ].join(','), + "application/vnd.oci.image.index.v1+json", + "application/vnd.docker.distribution.manifest.list.v2+json", + "application/vnd.oci.image.manifest.v1+json", + "application/vnd.docker.distribution.manifest.v2+json", + ].join(","), }); manifestStatus = manifestResult.status; } @@ -209,7 +207,7 @@ async function probeImage({ image, service, stage }, requiredTag) { image, service, stage, - visibility: 'unknown', + visibility: "unknown", ok: false, reason: `probe failed: ${error.message}`, tags: [], @@ -250,11 +248,11 @@ async function main() { } for (const result of results) { - const mark = result.ok ? 'OK ' : 'XX '; + const mark = result.ok ? "OK " : "XX "; process.stdout.write(`${mark}ghcr.io/${result.image} (${result.service}): ${result.reason}\n`); } if (summary.ready) { - process.stdout.write('\nPublish gate CLEAR: the template image is anonymously pullable.\n'); + process.stdout.write("\nPublish gate CLEAR: the template image is anonymously pullable.\n"); } else { process.stdout.write(`\nPublish gate BLOCKED.\n${summary.ownerAction}\n`); } @@ -269,6 +267,6 @@ if (process.argv[1] === fileURLToPath(import.meta.url)) { (error) => { process.stderr.write(`${error?.stack ?? error}\n`); process.exitCode = 1; - }, + } ); } diff --git a/scripts/ci-mode.mjs b/scripts/ci-mode.mjs index 915fece10..9b908e9fc 100644 --- a/scripts/ci-mode.mjs +++ b/scripts/ci-mode.mjs @@ -2,21 +2,21 @@ // Copyright The PDP-Connect Contributors // SPDX-License-Identifier: Apache-2.0 -import { execFileSync } from 'node:child_process'; -import process from 'node:process'; -import { fileURLToPath } from 'node:url'; +import { execFileSync } from "node:child_process"; +import process from "node:process"; +import { fileURLToPath } from "node:url"; -export const DEFAULT_RULESET_NAME = 'main: require PR + reference-implementation check'; -export const HOSTED_CONTEXT = 'typecheck + full test suite'; -export const LOCAL_CONTEXT = 'signoff/reference-implementation'; +export const DEFAULT_RULESET_NAME = "main: require PR + reference-implementation check"; +export const HOSTED_CONTEXT = "typecheck + full test suite"; +export const LOCAL_CONTEXT = "signoff/reference-implementation"; export const MANAGED_WORKFLOW_PATHS = [ - '.github/workflows/reference-implementation.yml', - '.github/workflows/react-doctor.yml', - '.github/workflows/docker-images.yml', - '.github/workflows/spec-check.yml', - '.github/workflows/polyfill-connectors.yml', - '.github/workflows/remote-surface.yml', - '.github/workflows/semantic-release.yml', + ".github/workflows/reference-implementation.yml", + ".github/workflows/react-doctor.yml", + ".github/workflows/docker-images.yml", + ".github/workflows/spec-check.yml", + ".github/workflows/polyfill-connectors.yml", + ".github/workflows/remote-surface.yml", + ".github/workflows/semantic-release.yml", ]; // Path prefixes whose change makes the connector-conformance gate (below) @@ -32,25 +32,22 @@ export const MANAGED_WORKFLOW_PATHS = [ // the gate's scripts are wired) — a change to the gate itself must exercise // the gate, so it cannot be weakened without proving the weakened version // still passes ci:mode:test and the connector-conformance run. -export const CONNECTOR_SURFACE_PATH_PREFIXES = [ - 'packages/polyfill-connectors/', - 'reference-implementation/manifests/', -]; +export const CONNECTOR_SURFACE_PATH_PREFIXES = ["packages/polyfill-connectors/", "reference-implementation/manifests/"]; // The specific, fast, deterministic tests that catch a scaffolded/dishonest // connector manifest — not the full polyfill-connectors package suite (which // also runs slow browser/integration connector tests unrelated to this gate). // Paths are relative to packages/polyfill-connectors. export const CONNECTOR_CONFORMANCE_TEST_FILES = [ - 'src/stream-evidence-strategy-manifest.test.ts', - 'src/coverage-policy-manifest-honesty.test.ts', - 'src/connector-conformance.test.ts', + "src/stream-evidence-strategy-manifest.test.ts", + "src/coverage-policy-manifest-honesty.test.ts", + "src/connector-conformance.test.ts", ]; export const CI_GATE_SELF_PATHS = [ - 'scripts/ci-mode.mjs', - 'scripts/ci-mode.test.mjs', - 'package.json', + "scripts/ci-mode.mjs", + "scripts/ci-mode.test.mjs", + "package.json", ...CONNECTOR_CONFORMANCE_TEST_FILES.map((path) => `packages/polyfill-connectors/${path}`), ]; @@ -59,8 +56,8 @@ export const CI_GATE_SELF_PATHS = [ // current; changing the generator or generated artifact must prove the same // fact, otherwise a local signoff could bless an intentionally stale render. export const STREAM_EVIDENCE_INVENTORY_PATHS = [ - 'scripts/stream-evidence-inventory.mjs', - 'docs/reference/stream-evidence-inventory.md', + "scripts/stream-evidence-inventory.mjs", + "docs/reference/stream-evidence-inventory.md", ]; export function changeTouchesConnectorSurface(changedFiles) { @@ -104,11 +101,11 @@ must equal HEAD — signoff refuses to post a status for a commit whose code it did not just test. It diffs HEAD against --base (default origin/main); it gets changed paths with --no-renames and NUL delimiters; if that diff touches either shipped manifest root -(${CONNECTOR_SURFACE_PATH_PREFIXES.join(', ')}), it runs the connector-conformance -suite (${CONNECTOR_CONFORMANCE_TEST_FILES.join(', ')}) and the source-derived +(${CONNECTOR_SURFACE_PATH_PREFIXES.join(", ")}), it runs the connector-conformance +suite (${CONNECTOR_CONFORMANCE_TEST_FILES.join(", ")}) and the source-derived stream-evidence inventory check. Changes to the inventory producer/artifact -(${STREAM_EVIDENCE_INVENTORY_PATHS.join(', ')}) also run that inventory check. -For a gate-self change (${CI_GATE_SELF_PATHS.join(', ')}), signoff also runs +(${STREAM_EVIDENCE_INVENTORY_PATHS.join(", ")}) also run that inventory check. +For a gate-self change (${CI_GATE_SELF_PATHS.join(", ")}), signoff also runs ci:mode:test — failing closed if any required check does not pass. There is no opt-out: if the diff cannot be computed (missing base ref, shallow clone), signoff fails outright rather than silently skipping the gate.`; @@ -116,8 +113,8 @@ signoff fails outright rather than silently skipping the gate.`; function run(command, args, options = {}) { return execFileSync(command, args, { - encoding: 'utf8', - stdio: options.input === undefined ? ['ignore', 'pipe', 'pipe'] : ['pipe', 'pipe', 'pipe'], + encoding: "utf8", + stdio: options.input === undefined ? ["ignore", "pipe", "pipe"] : ["pipe", "pipe", "pipe"], input: options.input, }).trim(); } @@ -128,26 +125,26 @@ function runJson(command, args, options = {}) { } function gh(args, options = {}) { - return run('gh', args, options); + return run("gh", args, options); } function ghJson(args, options = {}) { - return runJson('gh', args, options); + return runJson("gh", args, options); } function git(args) { - return run('git', args); + return run("git", args); } function gitBuffer(args) { - return execFileSync('git', args, { - encoding: 'buffer', - stdio: ['ignore', 'pipe', 'pipe'], + return execFileSync("git", args, { + encoding: "buffer", + stdio: ["ignore", "pipe", "pipe"], }); } export function getRequiredStatusContexts(ruleset) { - const rule = ruleset.rules?.find((candidate) => candidate.type === 'required_status_checks'); + const rule = ruleset.rules?.find((candidate) => candidate.type === "required_status_checks"); return rule?.parameters?.required_status_checks?.map((check) => check.context) ?? []; } @@ -158,13 +155,13 @@ export function detectCiMode(contexts) { return mode; } } - return 'custom'; + return "custom"; } export function rulesetWithRequiredStatusContexts(ruleset, contexts) { let replaced = false; const nextRules = (ruleset.rules ?? []).map((rule) => { - if (rule.type !== 'required_status_checks') { + if (rule.type !== "required_status_checks") { return rule; } replaced = true; @@ -178,7 +175,7 @@ export function rulesetWithRequiredStatusContexts(ruleset, contexts) { }); if (!replaced) { nextRules.push({ - type: 'required_status_checks', + type: "required_status_checks", parameters: { do_not_enforce_on_create: false, required_status_checks: contexts.map((context) => ({ context })), @@ -197,25 +194,25 @@ export function rulesetWithRequiredStatusContexts(ruleset, contexts) { } export function workflowUpdatesForMode(workflows, mode, managedPaths = MANAGED_WORKFLOW_PATHS) { - if (mode !== 'hosted' && mode !== 'local') { + if (mode !== "hosted" && mode !== "local") { throw new Error(`unknown mode: ${mode}`); } const workflowsByPath = new Map(workflows.map((workflow) => [workflow.path, workflow])); return managedPaths.map((path) => { const workflow = workflowsByPath.get(path) ?? null; - const action = mode === 'hosted' ? 'enable' : 'disable'; + const action = mode === "hosted" ? "enable" : "disable"; return { action, missing: workflow === null, - needsChange: workflow ? (mode === 'hosted' ? workflow.state !== 'active' : workflow.state === 'active') : false, + needsChange: workflow ? (mode === "hosted" ? workflow.state !== "active" : workflow.state === "active") : false, path, - state: workflow?.state ?? 'missing', + state: workflow?.state ?? "missing", workflow, }; }); } -function repoApiPath(suffix = '') { +function repoApiPath(suffix = "") { return `repos/:owner/:repo${suffix}`; } @@ -224,10 +221,10 @@ function getManagedWorkflowPaths(mode, workflows) { let paths; if (configured) { paths = configured - .split(',') + .split(",") .map((value) => value.trim()) .filter(Boolean); - } else if (mode === 'local') { + } else if (mode === "local") { paths = workflows.map((workflow) => workflow.path); } else { paths = MANAGED_WORKFLOW_PATHS; @@ -238,38 +235,38 @@ function getManagedWorkflowPaths(mode, workflows) { function loadRuleset() { const configuredId = process.env.PDPP_CI_RULESET_ID; if (configuredId) { - return ghJson(['api', repoApiPath(`/rulesets/${configuredId}`)]); + return ghJson(["api", repoApiPath(`/rulesets/${configuredId}`)]); } const name = process.env.PDPP_CI_RULESET_NAME || DEFAULT_RULESET_NAME; - const rulesets = ghJson(['api', repoApiPath('/rulesets')]) ?? []; + const rulesets = ghJson(["api", repoApiPath("/rulesets")]) ?? []; const summary = rulesets.find((ruleset) => ruleset.name === name); if (!summary) { throw new Error(`ruleset not found: ${name}`); } - return ghJson(['api', repoApiPath(`/rulesets/${summary.id}`)]); + return ghJson(["api", repoApiPath(`/rulesets/${summary.id}`)]); } function writeRuleset(ruleset, contexts) { const body = JSON.stringify(rulesetWithRequiredStatusContexts(ruleset, contexts), null, 2); return ghJson( [ - 'api', - '--method', - 'PUT', - '-H', - 'Accept: application/vnd.github+json', - '-H', - 'X-GitHub-Api-Version: 2022-11-28', + "api", + "--method", + "PUT", + "-H", + "Accept: application/vnd.github+json", + "-H", + "X-GitHub-Api-Version: 2022-11-28", repoApiPath(`/rulesets/${ruleset.id}`), - '--input', - '-', + "--input", + "-", ], { input: body } ); } function loadWorkflows() { - const response = ghJson(['api', repoApiPath('/actions/workflows?per_page=100')]); + const response = ghJson(["api", repoApiPath("/actions/workflows?per_page=100")]); return response?.workflows ?? []; } @@ -278,20 +275,15 @@ function applyManagedWorkflowMode(mode) { const updates = workflowUpdatesForMode(workflows, mode, getManagedWorkflowPaths(mode, workflows)); const missing = updates.filter((update) => update.missing); if (missing.length > 0) { - throw new Error(`managed workflow not found: ${missing.map((update) => update.path).join(', ')}`); + throw new Error(`managed workflow not found: ${missing.map((update) => update.path).join(", ")}`); } const changed = updates.filter((update) => update.needsChange); for (const update of changed) { - gh([ - 'api', - '--method', - 'PUT', - repoApiPath(`/actions/workflows/${update.workflow.id}/${update.action}`), - ]); + gh(["api", "--method", "PUT", repoApiPath(`/actions/workflows/${update.workflow.id}/${update.action}`)]); } - console.log(`managed workflows: ${mode === 'hosted' ? 'enabled' : 'disabled'} (${changed.length} changed)`); + console.log(`managed workflows: ${mode === "hosted" ? "enabled" : "disabled"} (${changed.length} changed)`); for (const update of updates) { - const marker = update.needsChange ? update.action : 'unchanged'; + const marker = update.needsChange ? update.action : "unchanged"; console.log(`- ${update.path}: ${update.state} -> ${marker}`); } } @@ -302,13 +294,13 @@ function printStatus() { const mode = detectCiMode(contexts); console.log(`ruleset: ${ruleset.name} (#${ruleset.id})`); console.log(`mode: ${mode}`); - console.log('required status checks:'); + console.log("required status checks:"); for (const context of contexts) { console.log(`- ${context}`); } - console.log('managed workflows:'); + console.log("managed workflows:"); const workflows = loadWorkflows(); - const statusMode = mode === 'custom' ? 'hosted' : mode; + const statusMode = mode === "custom" ? "hosted" : mode; for (const update of workflowUpdatesForMode(workflows, statusMode, getManagedWorkflowPaths(statusMode, workflows))) { console.log(`- ${update.path}: ${update.state}`); } @@ -319,28 +311,28 @@ function setMode(mode) { if (!contexts) { throw new Error(`unknown mode: ${mode}`); } - if (mode === 'hosted') { + if (mode === "hosted") { applyManagedWorkflowMode(mode); } const before = loadRuleset(); const previous = getRequiredStatusContexts(before); const after = writeRuleset(before, contexts); const current = getRequiredStatusContexts(after); - if (mode === 'local') { + if (mode === "local") { applyManagedWorkflowMode(mode); } console.log(`mode: ${mode}`); console.log(`ruleset: ${after.name} (#${after.id})`); - console.log(`previous required status checks: ${previous.join(', ') || '(none)'}`); - console.log(`current required status checks: ${current.join(', ') || '(none)'}`); + console.log(`previous required status checks: ${previous.join(", ") || "(none)"}`); + console.log(`current required status checks: ${current.join(", ") || "(none)"}`); } function isCleanAndPushed() { - if (git(['status', '--porcelain'])) { + if (git(["status", "--porcelain"])) { return false; } - git(['rev-parse', '--abbrev-ref', '@{push}']); - return git(['log', '@{push}..']) === ''; + git(["rev-parse", "--abbrev-ref", "@{push}"]); + return git(["log", "@{push}.."]) === ""; } /** @@ -354,45 +346,45 @@ function isCleanAndPushed() { * "nothing changed." */ function changedFilesAgainstBase(base) { - const mergeBase = git(['merge-base', base, 'HEAD']); - const diff = gitBuffer(['diff', '--no-renames', '--name-only', '-z', `${mergeBase}..HEAD`]); + const mergeBase = git(["merge-base", base, "HEAD"]); + const diff = gitBuffer(["diff", "--no-renames", "--name-only", "-z", `${mergeBase}..HEAD`]); if (diff.length === 0) { return []; } - const paths = diff.toString('utf8').split('\0'); + const paths = diff.toString("utf8").split("\0"); paths.pop(); return paths; } function parseSignoffArgs(args) { const out = { - base: 'origin/main', + base: "origin/main", context: LOCAL_CONTEXT, - description: 'Local reference-implementation gate signed off', + description: "Local reference-implementation gate signed off", sha: null, targetUrl: null, }; for (let i = 0; i < args.length; i += 1) { const arg = args[i]; - if (arg === '--') { + if (arg === "--") { continue; } - if (arg === '--base') { + if (arg === "--base") { out.base = args[++i]; - } else if (arg === '--context') { + } else if (arg === "--context") { out.context = args[++i]; - } else if (arg === '--description') { + } else if (arg === "--description") { out.description = args[++i]; - } else if (arg === '--sha') { + } else if (arg === "--sha") { out.sha = args[++i]; - } else if (arg === '--target-url') { + } else if (arg === "--target-url") { out.targetUrl = args[++i]; } else { throw new Error(`unknown signoff option: ${arg}`); } } if (!out.context) { - throw new Error('signoff context cannot be empty'); + throw new Error("signoff context cannot be empty"); } return out; } @@ -420,32 +412,31 @@ export function ciModeSelfTestRequired(changedFiles) { } function runConnectorConformanceGate() { - console.log('a shipped manifest root or this gate changed — running the connector-conformance gate...'); - execFileSync( - 'node', - ['--test', '--test-timeout=30000', '--import', 'tsx', ...CONNECTOR_CONFORMANCE_TEST_FILES], - { cwd: 'packages/polyfill-connectors', stdio: 'inherit' } - ); + console.log("a shipped manifest root or this gate changed — running the connector-conformance gate..."); + execFileSync("node", ["--test", "--test-timeout=30000", "--import", "tsx", ...CONNECTOR_CONFORMANCE_TEST_FILES], { + cwd: "packages/polyfill-connectors", + stdio: "inherit", + }); } function runStreamEvidenceInventoryGate() { - console.log('a shipped manifest root or stream-evidence inventory input changed — running the inventory check...'); - execFileSync('node', ['scripts/stream-evidence-inventory.mjs', '--check'], { stdio: 'inherit' }); + console.log("a shipped manifest root or stream-evidence inventory input changed — running the inventory check..."); + execFileSync("node", ["scripts/stream-evidence-inventory.mjs", "--check"], { stdio: "inherit" }); } function runCiModeSelfTest() { - console.log('this gate changed — running ci:mode:test...'); - execFileSync('node', ['--test', 'scripts/ci-mode.test.mjs'], { stdio: 'inherit' }); + console.log("this gate changed — running ci:mode:test..."); + execFileSync("node", ["--test", "scripts/ci-mode.test.mjs"], { stdio: "inherit" }); } function signoff(args) { const options = parseSignoffArgs(args); if (!isCleanAndPushed()) { throw new Error( - 'repository has uncommitted or unpushed changes; signoff always tests and signs the exact pushed HEAD, so there is no dirty-tree override' + "repository has uncommitted or unpushed changes; signoff always tests and signs the exact pushed HEAD, so there is no dirty-tree override" ); } - const headSha = git(['rev-parse', 'HEAD']); + const headSha = git(["rev-parse", "HEAD"]); if (options.sha && options.sha !== headSha) { throw new Error( `--sha ${options.sha} does not match HEAD ${headSha}; signoff can only post a status for the commit it just tested` @@ -465,12 +456,12 @@ function signoff(args) { const body = { context: options.context, description: options.description, - state: 'success', + state: "success", }; if (options.targetUrl) { body.target_url = options.targetUrl; } - ghJson(['api', '--method', 'POST', repoApiPath(`/statuses/${sha}`), '--input', '-'], { + ghJson(["api", "--method", "POST", repoApiPath(`/statuses/${sha}`), "--input", "-"], { input: JSON.stringify(body), }); console.log(`signed off ${sha} with ${options.context}`); @@ -478,19 +469,19 @@ function signoff(args) { async function main() { const [command, ...args] = process.argv.slice(2); - if (!command || command === '--help' || command === '-h') { + if (!command || command === "--help" || command === "-h") { console.log(usage()); return; } - if (command === 'status') { + if (command === "status") { printStatus(); return; } - if (command === 'hosted' || command === 'local') { + if (command === "hosted" || command === "local") { setMode(command); return; } - if (command === 'signoff') { + if (command === "signoff") { signoff(args); return; } diff --git a/scripts/cli-acceptance-smoke.mjs b/scripts/cli-acceptance-smoke.mjs index 2f1be1916..e23c95ca1 100644 --- a/scripts/cli-acceptance-smoke.mjs +++ b/scripts/cli-acceptance-smoke.mjs @@ -1,25 +1,25 @@ // Copyright The PDP-Connect Contributors // SPDX-License-Identifier: Apache-2.0 -import { spawn, spawnSync } from 'node:child_process'; -import { existsSync } from 'node:fs'; -import { mkdtemp, readFile, rm } from 'node:fs/promises'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; +import { spawn, spawnSync } from "node:child_process"; +import { existsSync } from "node:fs"; +import { mkdtemp, readFile, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; const [, , mode] = process.argv; -const repoRoot = new URL('..', import.meta.url).pathname; -const cliBin = join(repoRoot, 'packages/cli/bin/pdpp.js'); +const repoRoot = new URL("..", import.meta.url).pathname; +const cliBin = join(repoRoot, "packages/cli/bin/pdpp.js"); -if (mode === 'published-help') { - const result = run('npx', ['-y', '@pdpp/cli@latest', '--help'], { cwd: repoRoot }); - requireSuccess(result, 'published @pdpp/cli --help'); - requireMatch(result.stdout, /PDPP CLI/, 'published help output should identify the PDPP CLI'); - process.stdout.write('PASS published @pdpp/cli --help\n'); +if (mode === "published-help") { + const result = run("npx", ["-y", "@pdpp/cli@latest", "--help"], { cwd: repoRoot }); + requireSuccess(result, "published @pdpp/cli --help"); + requireMatch(result.stdout, /PDPP CLI/, "published help output should identify the PDPP CLI"); + process.stdout.write("PASS published @pdpp/cli --help\n"); process.exit(0); } -if (mode === 'local-connect') { +if (mode === "local-connect") { try { await runLocalConnectSmoke(); process.exit(0); @@ -31,20 +31,20 @@ if (mode === 'local-connect') { process.stderr.write( [ - 'Usage: node scripts/cli-acceptance-smoke.mjs ', - '', - 'Modes:', - ' published-help Run npx -y @pdpp/cli@latest --help.', - ' local-connect Run local pdpp connect smoke.', - '', - ].join('\n') + "Usage: node scripts/cli-acceptance-smoke.mjs ", + "", + "Modes:", + " published-help Run npx -y @pdpp/cli@latest --help.", + " local-connect Run local pdpp connect smoke.", + "", + ].join("\n") ); process.exit(64); function run(command, args, options) { return spawnSync(command, args, { ...options, - encoding: 'utf8', + encoding: "utf8", maxBuffer: 1024 * 1024, }); } @@ -87,19 +87,19 @@ async function runLocalConnectSmoke() { return runExternalLocalConnectSmoke(process.env.PDPP_CONNECT_SMOKE_PROVIDER_URL); } - const tmpRoot = await mkdtemp(join(tmpdir(), 'pdpp-cli-connect-smoke-')); + const tmpRoot = await mkdtemp(join(tmpdir(), "pdpp-cli-connect-smoke-")); let server = null; try { const [{ startServer }, nativeManifest] = await Promise.all([ - import('../reference-implementation/server/index.js'), - readFile(join(repoRoot, 'reference-implementation/manifests/northstar-hr.json'), 'utf8').then(JSON.parse), + import("../reference-implementation/server/index.js"), + readFile(join(repoRoot, "reference-implementation/manifests/northstar-hr.json"), "utf8").then(JSON.parse), ]); server = await startServer({ asPort: 0, rsPort: 0, - bindHost: '127.0.0.1', + bindHost: "127.0.0.1", quiet: true, - dbPath: join(tmpRoot, 'reference.sqlite'), + dbPath: join(tmpRoot, "reference.sqlite"), nativeManifest, semanticRetrievalSupported: false, lexicalRetrievalSupported: false, @@ -124,7 +124,7 @@ async function runLocalConnectSmoke() { } function runExternalLocalConnectSmoke(providerUrl) { - const result = run(process.execPath, [cliBin, 'connect', providerUrl], { cwd: repoRoot }); + const result = run(process.execPath, [cliBin, "connect", providerUrl], { cwd: repoRoot }); if (result.status === 0) { process.stdout.write(`PASS local pdpp connect ${providerUrl}\n`); @@ -133,14 +133,14 @@ function runExternalLocalConnectSmoke(providerUrl) { const blockedReason = getConnectBlockedReason(result.stderr); const gated = result.status === 69 && blockedReason; - if (gated && process.env.PDPP_CONNECT_SMOKE_REQUIRE_ENABLED !== '1') { + if (gated && process.env.PDPP_CONNECT_SMOKE_REQUIRE_ENABLED !== "1") { process.stdout.write( [ `SKIP local pdpp connect ${providerUrl}`, `Reason: ${blockedReason}`, - 'Set PDPP_CONNECT_SMOKE_REQUIRE_ENABLED=1 to require the external provider to be connect-ready.', - '', - ].join('\n') + "Set PDPP_CONNECT_SMOKE_REQUIRE_ENABLED=1 to require the external provider to be connect-ready.", + "", + ].join("\n") ); return; } @@ -150,40 +150,44 @@ function runExternalLocalConnectSmoke(providerUrl) { function runConnectAndApprove({ providerUrl, cwd, timeoutMs }) { return new Promise((resolve, reject) => { - const child = spawn(process.execPath, [cliBin, 'connect', providerUrl], { + const child = spawn(process.execPath, [cliBin, "connect", providerUrl], { cwd, - env: { ...process.env, NO_COLOR: '1' }, - stdio: ['ignore', 'pipe', 'pipe'], + env: { ...process.env, NO_COLOR: "1" }, + stdio: ["ignore", "pipe", "pipe"], }); - let stdout = ''; - let stderr = ''; + let stdout = ""; + let stderr = ""; let approvalPosted = false; const timeout = setTimeout(() => { - child.kill('SIGTERM'); - reject(new Error(`FAIL local pdpp connect ${providerUrl}\nTimed out waiting for approval flow.\nstdout:\n${stdout}\nstderr:\n${stderr}`)); + child.kill("SIGTERM"); + reject( + new Error( + `FAIL local pdpp connect ${providerUrl}\nTimed out waiting for approval flow.\nstdout:\n${stdout}\nstderr:\n${stderr}` + ) + ); }, timeoutMs); - child.stdout.on('data', (chunk) => { + child.stdout.on("data", (chunk) => { stdout += chunk.toString(); if (!approvalPosted) { const approvalUrl = extractApprovalUrl(stdout); if (approvalUrl) { approvalPosted = true; approveAccess(approvalUrl).catch((error) => { - child.kill('SIGTERM'); + child.kill("SIGTERM"); reject(error); }); } } }); - child.stderr.on('data', (chunk) => { + child.stderr.on("data", (chunk) => { stderr += chunk.toString(); }); - child.on('error', (error) => { + child.on("error", (error) => { clearTimeout(timeout); reject(error); }); - child.on('close', (status) => { + child.on("close", (status) => { clearTimeout(timeout); resolve({ status, stdout, stderr }); }); @@ -197,15 +201,15 @@ function extractApprovalUrl(stdout) { async function approveAccess(approvalUrl) { const url = new URL(approvalUrl); - const requestUri = url.searchParams.get('request_uri'); + const requestUri = url.searchParams.get("request_uri"); if (!requestUri) { throw new Error(`FAIL approval URL missing request_uri: ${approvalUrl}`); } - const response = await fetch(new URL('/consent/approve', url), { - method: 'POST', - headers: { Accept: 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' }, - body: new URLSearchParams({ request_uri: requestUri, subject_id: 'owner_local' }).toString(), - redirect: 'manual', + const response = await fetch(new URL("/consent/approve", url), { + method: "POST", + headers: { Accept: "application/json", "Content-Type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ request_uri: requestUri, subject_id: "owner_local" }).toString(), + redirect: "manual", }); if (!response.ok && response.status !== 302 && response.status !== 303) { throw new Error(`FAIL test approval failed: HTTP ${response.status}`); @@ -216,11 +220,8 @@ async function closeReferenceServer(server) { if (!server) { return; } - server.abortStartupBackfill?.('cli-connect-smoke complete'); - await Promise.all([ - closeNodeServer(server.asServer), - closeNodeServer(server.rsServer), - ]); + server.abortStartupBackfill?.("cli-connect-smoke complete"); + await Promise.all([closeNodeServer(server.asServer), closeNodeServer(server.rsServer)]); } function closeNodeServer(server) { @@ -244,20 +245,20 @@ function formatConnectFailure(providerUrl, result) { if (result.stderr) { lines.push(`stderr:\n${result.stderr}`); } - return new Error(lines.join('\n')); + return new Error(lines.join("\n")); } function getConnectBlockedReason(stderr) { if (/not enabled yet|no-owner-token scoped grant completion/i.test(stderr)) { - return 'CLI reported that no-owner-token scoped grant completion is not enabled.'; + return "CLI reported that no-owner-token scoped grant completion is not enabled."; } - if (stderr.includes('/.well-known/oauth-protected-resource') && /HTTP 404/i.test(stderr)) { - return 'local reference server did not expose protected-resource metadata for connect smoke.'; + if (stderr.includes("/.well-known/oauth-protected-resource") && /HTTP 404/i.test(stderr)) { + return "local reference server did not expose protected-resource metadata for connect smoke."; } if (/fetch failed|ECONNREFUSED|ENOTFOUND/i.test(stderr)) { - return 'local reference provider was unavailable for connect smoke.'; + return "local reference provider was unavailable for connect smoke."; } return null; diff --git a/scripts/dev.mjs b/scripts/dev.mjs index 2f922f2ef..7cb90b13e 100644 --- a/scripts/dev.mjs +++ b/scripts/dev.mjs @@ -78,7 +78,7 @@ const child = spawn( { env, stdio: "inherit", - }, + } ); child.once("error", (err) => { diff --git a/scripts/event-subscription-test-receiver.mjs b/scripts/event-subscription-test-receiver.mjs index b909b2f9d..ff47a2c6f 100755 --- a/scripts/event-subscription-test-receiver.mjs +++ b/scripts/event-subscription-test-receiver.mjs @@ -105,7 +105,7 @@ function printUsage() { " is returned after the receiver has already started.", " --insecure Skip signature verification. For one-off envelope inspection only.", "", - ].join("\n"), + ].join("\n") ); } @@ -144,10 +144,7 @@ function verify(secret, eventId, timestampHeader, body, signatureHeader) { if (idx < 0) continue; if (token.slice(0, idx) !== "v1") continue; const candidateBuf = Buffer.from(token.slice(idx + 1)); - if ( - candidateBuf.length === expectedBuf.length && - timingSafeEqual(candidateBuf, expectedBuf) - ) { + if (candidateBuf.length === expectedBuf.length && timingSafeEqual(candidateBuf, expectedBuf)) { return { ok: true }; } } @@ -205,7 +202,7 @@ async function main() { " Set WEBHOOK_SECRET, pass --secret , or write the value into --secret-file before delivery.", " Until then, the receiver will reject every signed delivery as `no_secret_configured`.", "", - ].join("\n"), + ].join("\n") ); } @@ -300,7 +297,7 @@ async function main() { args.port + "/webhook from your client to drive the verification handshake.", "", - ].join("\n"), + ].join("\n") ); }); diff --git a/scripts/lib/owner-session.mjs b/scripts/lib/owner-session.mjs index 53851e0f7..70de2ae16 100644 --- a/scripts/lib/owner-session.mjs +++ b/scripts/lib/owner-session.mjs @@ -95,7 +95,11 @@ export async function loginWithOwnerPassword({ base, password, fetchImpl }) { }); const sessionCookie = findSetCookiePair(getSetCookieList(resp), "pdpp_owner_session"); if (!sessionCookie) { - return { header: {}, mode: "password-session", error: `owner login did not issue a session cookie (status ${resp.status})` }; + return { + header: {}, + mode: "password-session", + error: `owner login did not issue a session cookie (status ${resp.status})`, + }; } return { header: { cookie: sessionCookie }, mode: "password-session", error: null }; } diff --git a/scripts/manual-action-stream-smoke.mjs b/scripts/manual-action-stream-smoke.mjs index 205286186..d854d46f9 100755 --- a/scripts/manual-action-stream-smoke.mjs +++ b/scripts/manual-action-stream-smoke.mjs @@ -327,7 +327,10 @@ function normalizeClipToViewport(clip, viewportSize) { async function captureStreamRemoteRect(page, remoteRect, remoteViewport) { const contentRect = await streamContentRect(page, remoteViewport); - const clip = normalizeClipToViewport(mapRemoteRectToLocalClip(contentRect, remoteRect, remoteViewport), page.viewportSize()); + const clip = normalizeClipToViewport( + mapRemoteRectToLocalClip(contentRect, remoteRect, remoteViewport), + page.viewportSize() + ); return { clip, png: await page.screenshot({ clip }), @@ -518,9 +521,13 @@ async function streamFailureSignature(page) { const media = node.querySelector("video, canvas"); const video = media instanceof HTMLVideoElement ? media : null; const inlineError = [...node.querySelectorAll("[data-pdpp-stream-ui]")].some((element) => - /n\.eko WebRTC stream did not attach|secure browser viewport could not be applied/i.test(element.textContent || "") + /n\.eko WebRTC stream did not attach|secure browser viewport could not be applied/i.test( + element.textContent || "" + ) + ); + const retryAffordance = [...node.querySelectorAll("button")].some((button) => + /retry secure browser/i.test(button.textContent || "") ); - const retryAffordance = [...node.querySelectorAll("button")].some((button) => /retry secure browser/i.test(button.textContent || "")); const bodyText = document.body?.innerText || ""; const reachFailure = /couldn['’]t reach the browser stream|browser stream failed to start|browser stream isn['’]t available|n\.eko browser window did not settle/i.test( @@ -532,7 +539,11 @@ async function streamFailureSignature(page) { // the distinguishing signal that keeps dark content from looking like a // black-frame failure. hasFirstFrameSignal: Boolean( - video && video.videoWidth > 0 && video.videoHeight > 0 && video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA && video.currentTime > 0 + video && + video.videoWidth > 0 && + video.videoHeight > 0 && + video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA && + video.currentTime > 0 ), hasRasterMedia: Boolean(media), }; @@ -750,7 +761,9 @@ async function assertCornerControlsDisclosure(page) { await new Promise((resolve) => setTimeout(resolve, 200)); const afterFirstEscape = await cornerControlsSnapshot(page); if (!afterFirstEscape.dialogPresent) { - fail("stream dialog closed/tore down on the FIRST Escape while actions were expanded — the disclosure must consume that Escape, not the dialog"); + fail( + "stream dialog closed/tore down on the FIRST Escape while actions were expanded — the disclosure must consume that Escape, not the dialog" + ); } if (afterFirstEscape.toggleExpanded !== "false") { fail(`first Escape did not collapse the disclosure: ${JSON.stringify(afterFirstEscape)}`); @@ -813,7 +826,9 @@ async function captureFailureEvidence(page, debugEvents, requestEvidence, messag screenshotPath, streamFrame: frame, streamFramePixels: framePixels, - remotePlaygroundReady: Boolean(latestEvent(debugEvents, (_event, _payload, type) => type === "playground.ready")), + remotePlaygroundReady: Boolean( + latestEvent(debugEvents, (_event, _payload, type) => type === "playground.ready") + ), remoteLayoutSource: eventType(latestRemoteLayoutEvent(debugEvents)), requestEvidence, debugEventCount: debugEvents.length, @@ -980,7 +995,9 @@ async function proveStrictVisualTyping(page, debugEvents, { mobile = false } = { async function run() { const origin = publicUrlFromEnv(); if (!origin) { - skip("set PDPP_STREAM_SMOKE_URL, PDPP_PUBLIC_URL, or PDPP_REFERENCE_ORIGIN to the running Docker/public web origin."); + skip( + "set PDPP_STREAM_SMOKE_URL, PDPP_PUBLIC_URL, or PDPP_REFERENCE_ORIGIN to the running Docker/public web origin." + ); return; } if (!(await deploymentReachable(origin))) { @@ -990,7 +1007,13 @@ async function run() { const { chromium } = await importPatchright(); const debugEvents = []; - const requestEvidence = { consoleErrors: [], eventStreamErrors: [], failedRequests: [], httpFailures: [], nekoHttpFailures: [] }; + const requestEvidence = { + consoleErrors: [], + eventStreamErrors: [], + failedRequests: [], + httpFailures: [], + nekoHttpFailures: [], + }; let debugEventSequence = 0; const mobile = mobileSmokeEnabled(); const executablePath = browserExecutablePath(); @@ -1043,7 +1066,12 @@ async function run() { const url = new URL(request.url()); const status = response.status(); if (status >= 400) { - const failure = { method: request.method(), resourceType: request.resourceType(), status, url: redactRequestUrl(url) }; + const failure = { + method: request.method(), + resourceType: request.resourceType(), + status, + url: redactRequestUrl(url), + }; appendEvidence(requestEvidence.httpFailures, failure); if (url.pathname === "/neko" || url.pathname.startsWith("/neko/")) { appendEvidence(requestEvidence.nekoHttpFailures, failure); @@ -1104,8 +1132,9 @@ async function run() { await waitFor( async () => - !(await streamFrame.evaluate((node) => Boolean(node.getAttribute("data-pdpp-stream-loading"))).catch(() => true)) || - hasEvent(debugEvents, (_event, _payload, type) => type === "neko.media.displayable"), + !(await streamFrame + .evaluate((node) => Boolean(node.getAttribute("data-pdpp-stream-loading"))) + .catch(() => true)) || hasEvent(debugEvents, (_event, _payload, type) => type === "neko.media.displayable"), "stream never became displayable" ); @@ -1143,7 +1172,8 @@ async function run() { "strict-mode smoke did not observe healthy n.eko pointer mapping" ); await waitFor( - () => hasEvent(debugEvents, (_event, _payload, type) => type === "adapter_mounted" || type === "neko.client.start"), + () => + hasEvent(debugEvents, (_event, _payload, type) => type === "adapter_mounted" || type === "neko.client.start"), "strict-mode smoke did not observe n.eko adapter/client startup" ); if (mobile) { @@ -1251,8 +1281,14 @@ async function run() { debugEvents, (_event, _payload, type) => type.startsWith("surface.neko.") || type.startsWith("neko.touch") ) && - hasEvent(debugEvents, (_event, payload, type) => type === "playground.click" || type === "playground.input") && - hasEvent(debugEvents, (_event, payload, type) => type === "stream.input.post.result" || type === "neko.client.start"), + hasEvent( + debugEvents, + (_event, payload, type) => type === "playground.click" || type === "playground.input" + ) && + hasEvent( + debugEvents, + (_event, payload, type) => type === "stream.input.post.result" || type === "neko.client.start" + ), "telemetry did not capture both local input path and remote playground events" ); } @@ -1279,7 +1315,9 @@ async function run() { process.stdout.write( `PASS manual-action stream smoke ${JSON.stringify({ mobile, - mobileBoundary: mobile ? "Chromium touch emulation and proxy focus/typing; OS keyboard visibility not proven" : null, + mobileBoundary: mobile + ? "Chromium touch emulation and proxy focus/typing; OS keyboard visibility not proven" + : null, url, })}\n` ); diff --git a/scripts/openspec-archive-check.mjs b/scripts/openspec-archive-check.mjs index 1c2da153a..486f7921a 100644 --- a/scripts/openspec-archive-check.mjs +++ b/scripts/openspec-archive-check.mjs @@ -237,8 +237,7 @@ export function classifyChange(changeName, { existsRepoRel, readFile = readFileS } const referenced = extractReferencedPaths(changeDir, readFile); - const codeLanded = - typeof existsRepoRel === "function" ? resolveReferencedPaths(referenced, existsRepoRel) : []; + const codeLanded = typeof existsRepoRel === "function" ? resolveReferencedPaths(referenced, existsRepoRel) : []; // Signal 1: every implementation checkbox is done (and there is at least one). const implComplete = tasks.impl.total > 0 && tasks.impl.done === tasks.impl.total; diff --git a/scripts/openspec-status.mjs b/scripts/openspec-status.mjs index fe67ff5b7..09bb57fae 100755 --- a/scripts/openspec-status.mjs +++ b/scripts/openspec-status.mjs @@ -112,10 +112,7 @@ console.log(`Active changes: ${records.length} (archive bucket not scanned)`); formatBucket("In-flight (some tasks done, some open)", byStatus["in-flight"]); formatBucket("Zero-progress (proposed, no tasks done yet)", byStatus["zero-progress"]); -formatBucket( - "Complete (all tasks done — awaiting owner archive sweep)", - byStatus.complete -); +formatBucket("Complete (all tasks done — awaiting owner archive sweep)", byStatus.complete); if (byStatus["no-tasks"].length > 0 || byStatus["no-tasks-file"].length > 0) { formatBucket("Without task tracking", [...byStatus["no-tasks"], ...byStatus["no-tasks-file"]], { diff --git a/scripts/owner-journey-acceptance/harness.mjs b/scripts/owner-journey-acceptance/harness.mjs index ca32904d2..ae812f08d 100644 --- a/scripts/owner-journey-acceptance/harness.mjs +++ b/scripts/owner-journey-acceptance/harness.mjs @@ -68,9 +68,7 @@ async function walkRepoFiles(repoRelativeDir) { async function readRouteSources(repoRelativeRoot) { const files = await walkRepoFiles(repoRelativeRoot); - const routeFiles = files - .filter((file) => /\/(?:page|loading)\.tsx$/.test(file)) - .sort(); + const routeFiles = files.filter((file) => /\/(?:page|loading)\.tsx$/.test(file)).sort(); const sources = []; for (const file of routeFiles) { sources.push({ path: file, src: await readRepoFile(file) }); @@ -143,9 +141,7 @@ export async function runLocalAcceptance(opts = {}) { const scanRenderedTier = async (file, tier) => { const src = await readRepoFile(file); findings.push(...scanForbiddenStrings({ path: file, src, tier, rules: FORBIDDEN_STRING_RULES })); - findings.push( - ...scanRenderedHelperReachability({ path: file, src, forbiddenHelpers: FORBIDDEN_RENDERED_HELPERS }) - ); + findings.push(...scanRenderedHelperReachability({ path: file, src, forbiddenHelpers: FORBIDDEN_RENDERED_HELPERS })); const cmds = extractRenderedCommands(src).map((c) => ({ ...c, path: file })); const fresh = checkCommandFreshness({ commands: cmds, surfaceByPackage, publishedPackages: PUBLISHED_PACKAGES }); findings.push(...fresh.findings); @@ -203,9 +199,7 @@ export async function runLocalAcceptance(opts = {}) { ); } - const publishedSurface = Object.fromEntries( - Object.entries(surfaceByPackage).map(([k, v]) => [k, [...v].sort()]) - ); + const publishedSurface = Object.fromEntries(Object.entries(surfaceByPackage).map(([k, v]) => [k, [...v].sort()])); return { findings, diff --git a/scripts/owner-journey-acceptance/live.mjs b/scripts/owner-journey-acceptance/live.mjs index aeb44c223..e651a9741 100644 --- a/scripts/owner-journey-acceptance/live.mjs +++ b/scripts/owner-journey-acceptance/live.mjs @@ -94,9 +94,7 @@ function htmlToText(html) { } function htmlToProseText(html) { - return htmlToText( - String(html).replace(/<(pre|code|kbd|samp)\b[^>]*>[\s\S]*?<\/\1>/gi, " ") - ); + return htmlToText(String(html).replace(/<(pre|code|kbd|samp)\b[^>]*>[\s\S]*?<\/\1>/gi, " ")); } function visibleMonogramInitials(html) { @@ -195,7 +193,8 @@ function connectorLabel(connector) { function sourceCountPhrase(connector) { const records = Number(connector?.total_records); - const rawStreamCount = connector?.stream_count ?? (Array.isArray(connector?.streams) ? connector.streams.length : null); + const rawStreamCount = + connector?.stream_count ?? (Array.isArray(connector?.streams) ? connector.streams.length : null); const streams = Number(rawStreamCount); if (!Number.isFinite(records) || !Number.isFinite(streams)) { return null; @@ -440,7 +439,8 @@ async function runLiveSemanticChecks({ base, header, fetchImpl, htmlByPath }) { } if (sourceIssues.length > 0 || rawSourceIssues.length > 0) { - const allClearRe = /No source issues to review here|Nothing needs you\.[^.]*sources are syncing\.|everything'?s syncing/i; + const allClearRe = + /No source issues to review here|Nothing needs you\.[^.]*sources are syncing\.|everything'?s syncing/i; if (allClearRe.test(dashboardText)) { findings.push({ ruleId: "dashboard-source-issue-all-clear", @@ -455,9 +455,12 @@ async function runLiveSemanticChecks({ base, header, fetchImpl, htmlByPath }) { } const overstatedHealthyAdvisories = healthyRefreshAdvisories.filter((issue) => { - const renderedAsBroken = dashboardText.includes(`${issue.label} is degraded`) || dashboardText.includes(`${issue.label} can't collect`); + const renderedAsBroken = + dashboardText.includes(`${issue.label} is degraded`) || dashboardText.includes(`${issue.label} can't collect`); const renderedWithRefreshStatement = - issue.forwardStatement.length > 0 && dashboardText.includes(issue.label) && dashboardText.includes(issue.forwardStatement); + issue.forwardStatement.length > 0 && + dashboardText.includes(issue.label) && + dashboardText.includes(issue.forwardStatement); return renderedAsBroken || renderedWithRefreshStatement; }); if (overstatedHealthyAdvisories.length > 0) { @@ -466,7 +469,10 @@ async function runLiveSemanticChecks({ base, header, fetchImpl, htmlByPath }) { class: "dashboard-trust-claim", path: "live:/", line: 0, - excerpt: overstatedHealthyAdvisories.map((issue) => issue.label).slice(0, 5).join(", "), + excerpt: overstatedHealthyAdvisories + .map((issue) => issue.label) + .slice(0, 5) + .join(", "), rationale: "A healthy source with a refresh-available advisory must not appear in the dashboard issue list as degraded or broken. The source detail may offer Refresh now, but the dashboard must not manufacture urgency.", }); @@ -480,7 +486,10 @@ async function runLiveSemanticChecks({ base, header, fetchImpl, htmlByPath }) { class: "dashboard-trust-claim", path: "live:/", line: 0, - excerpt: sourceIssues.map((issue) => issue.label).slice(0, 5).join(", "), + excerpt: sourceIssues + .map((issue) => issue.label) + .slice(0, 5) + .join(", "), rationale: "The dashboard reference data contains material source issues, but none of their source labels appear on the rendered dashboard. The owner needs a visible issue row, not a silent calm state.", }); @@ -570,7 +579,11 @@ async function runLiveSemanticChecks({ base, header, fetchImpl, htmlByPath }) { title: "Explore", required: [ { label: "Explore title", pattern: /\bExplore\b/i }, - { label: "record query controls", pattern: /Search names, fields, and values|Search records|text across every searchable stream|\boperators\b.*\bcon:/i }, + { + label: "record query controls", + pattern: + /Search names, fields, and values|Search records|text across every searchable stream|\boperators\b.*\bcon:/i, + }, { label: "record filters", pattern: /\bFilters\b/i }, { label: "record sort controls", pattern: /\bnewest\b.*\boldest\b/i }, ], @@ -586,16 +599,16 @@ async function runLiveSemanticChecks({ base, header, fetchImpl, htmlByPath }) { path: `live:${expectation.path}`, line: 0, excerpt: missing.map((item) => item.label).join(", "), - rationale: - `${expectation.title} must render its core owner controls on the live surface. A shell-only, login, or error-boundary page cannot prove the owner can use this journey.`, + rationale: `${expectation.title} must render its core owner controls on the live surface. A shell-only, login, or error-boundary page cannot prove the owner can use this journey.`, }); } checks.push({ id: expectation.id, status: missing.length > 0 ? "fail" : "pass", - detail: missing.length > 0 - ? `missing ${missing.map((item) => item.label).join(", ")}` - : `${expectation.title} rendered core owner controls`, + detail: + missing.length > 0 + ? `missing ${missing.map((item) => item.label).join(", ")}` + : `${expectation.title} rendered core owner controls`, }); } diff --git a/scripts/owner-journey-acceptance/report.mjs b/scripts/owner-journey-acceptance/report.mjs index 346284255..d6abe8b01 100644 --- a/scripts/owner-journey-acceptance/report.mjs +++ b/scripts/owner-journey-acceptance/report.mjs @@ -162,5 +162,7 @@ function dedupeCommands(commands) { } function escapeCell(text) { - return String(text ?? "").replace(/\|/g, "\\|").replace(/\n/g, " "); + return String(text ?? "") + .replace(/\|/g, "\\|") + .replace(/\n/g, " "); } diff --git a/scripts/owner-journey-acceptance/scan.mjs b/scripts/owner-journey-acceptance/scan.mjs index 3a7da4fc4..80d1bfac0 100644 --- a/scripts/owner-journey-acceptance/scan.mjs +++ b/scripts/owner-journey-acceptance/scan.mjs @@ -149,8 +149,7 @@ export function scanForbiddenStrings({ path, src, tier, rules }) { * @returns {string} */ export function blankModuleSpecifiers(src) { - const blankRange = (text, re) => - text.replace(re, (full) => full.replace(/[^\n]/g, " ")); + const blankRange = (text, re) => text.replace(re, (full) => full.replace(/[^\n]/g, " ")); let out = src; // import ... from "x" / export ... from "x" (single or double quoted) out = blankRange(out, /\b(?:import|export)\b[^\n;]*?\bfrom\s*["'][^"'\n]*["']/g); @@ -306,7 +305,10 @@ export function deriveSpecifierVars(src) { for (const mm of cleaned.matchAll(/\b(?:const|let|var)\s+([\w]+)\s*=\s*`\$\{\s*([\w]+)\s*\}@([\w.-]+)`/g)) { const base = vars[mm[2]]; if (base) { - vars[mm[1]] = `${base.split("@").slice(0, base.startsWith("@") ? 2 : 1).join("@")}@${mm[3]}`; + vars[mm[1]] = `${base + .split("@") + .slice(0, base.startsWith("@") ? 2 : 1) + .join("@")}@${mm[3]}`; } } return vars; @@ -560,8 +562,7 @@ export function checkSharedShellNavContract({ path, src, requiredItems }) { path, line: 0, excerpt: "aria-current active page marker", - rationale: - "The shared shell must mark the current route with aria-current so the owner can tell where they are.", + rationale: "The shared shell must mark the current route with aria-current so the owner can tell where they are.", }); } @@ -670,8 +671,5 @@ export function lineOf(text, index) { export function excerptAround(text, index, radius = 50) { const start = Math.max(0, index - radius); const end = Math.min(text.length, index + radius); - return text - .slice(start, end) - .replace(/\s+/g, " ") - .trim(); + return text.slice(start, end).replace(/\s+/g, " ").trim(); } diff --git a/scripts/owner-journey-acceptance/surface-manifest.mjs b/scripts/owner-journey-acceptance/surface-manifest.mjs index dfb564e19..939167b77 100644 --- a/scripts/owner-journey-acceptance/surface-manifest.mjs +++ b/scripts/owner-journey-acceptance/surface-manifest.mjs @@ -77,9 +77,7 @@ export const NORMAL_OWNER_ROUTE_SCAN_ROOTS = [ * may legitimately show `connector_instance_id`, `source_instance_id`, and a * `docs/operator/...` runbook pointer behind reference-experimental framing). */ -export const ADVANCED_OWNER_UI_FILES = [ - "apps/console/src/app/(console)/device-exporters/page.tsx", -]; +export const ADVANCED_OWNER_UI_FILES = ["apps/console/src/app/(console)/device-exporters/page.tsx"]; /** * Command-source libraries: not rendered themselves, but they build the command @@ -139,11 +137,7 @@ export const FORBIDDEN_RENDERED_HELPERS = [ id: "browser-collector-monorepo-helper", class: "developer-only-path", // Helper symbols that emit `pnpm --dir packages/polyfill-connectors ...`. - symbols: [ - "pdppBrowserCollectorEnrollCommand", - "pdppBrowserCollectorRunCommand", - "pdppCliMonorepoCommand", - ], + symbols: ["pdppBrowserCollectorEnrollCommand", "pdppBrowserCollectorRunCommand", "pdppCliMonorepoCommand"], rationale: "These helpers emit monorepo-only `pnpm --dir packages/...` commands. No rendered owner page may wire them into displayed setup copy.", }, @@ -177,8 +171,7 @@ export const FORBIDDEN_STRING_RULES = [ class: "developer-only-path", tiers: ["normal", "advanced"], pattern: /pnpm\s+--dir\b/, - rationale: - "`pnpm --dir` is a monorepo-checkout command. It cannot run from an owner's shipped install.", + rationale: "`pnpm --dir` is a monorepo-checkout command. It cannot run from an owner's shipped install.", }, { id: "monorepo-checkout", diff --git a/scripts/perf/bench.mjs b/scripts/perf/bench.mjs index 596797e67..198e803d5 100644 --- a/scripts/perf/bench.mjs +++ b/scripts/perf/bench.mjs @@ -66,8 +66,18 @@ function apiTargets() { return [ { group: "api", name: "schema", url: `${BASE}/v1/schema`, headers: h }, { group: "api", name: "search.lexical", url: `${BASE}/v1/search${q({ q: "error", limit: "25" })}`, headers: h }, - { group: "api", name: "search.semantic", url: `${BASE}/v1/search/semantic${q({ q: "deployment failure", limit: "25" })}`, headers: h }, - { group: "api", name: "search.hybrid", url: `${BASE}/v1/search/hybrid${q({ q: "deployment failure", limit: "25" })}`, headers: h }, + { + group: "api", + name: "search.semantic", + url: `${BASE}/v1/search/semantic${q({ q: "deployment failure", limit: "25" })}`, + headers: h, + }, + { + group: "api", + name: "search.hybrid", + url: `${BASE}/v1/search/hybrid${q({ q: "deployment failure", limit: "25" })}`, + headers: h, + }, // messages is shared across connectors → must disambiguate with connection_id. // PDPP_BENCH_CONNECTION_ID lets a run pin a real connection; default below is a // live Slack connection (override per-instance). A 400 here in results means the @@ -114,16 +124,7 @@ async function fetchOwnerCookie() { function pageTargets(cookieOverride) { const cookie = cookieOverride || process.env.PDPP_BENCH_COOKIE || ""; const headers = cookie ? { Cookie: cookie } : {}; - const routes = [ - "/", - "/sources", - "/sources/add", - "/explore", - "/syncs", - "/grants", - "/connect", - "/search", - ]; + const routes = ["/", "/sources", "/sources/add", "/explore", "/syncs", "/grants", "/connect", "/search"]; return routes.map((r) => ({ group: "page", name: r, url: `${BASE}${r}`, headers })); } @@ -197,10 +198,7 @@ async function main() { if (args.has("--login") && !cookie) { console.error("# --login: no cookie (set PDPP_OWNER_PASSWORD) — pages measured unauthenticated"); } - const targets = [ - ...(PAGES_ONLY ? [] : apiTargets()), - ...(API_ONLY ? [] : pageTargets(cookie)), - ]; + const targets = [...(PAGES_ONLY ? [] : apiTargets()), ...(API_ONLY ? [] : pageTargets(cookie))]; if (targets.length === 0) { console.error("No targets. (RS API targets need PDPP_OWNER_TOKEN; pages need none.)"); process.exit(2); @@ -257,7 +255,9 @@ function compare(current, priorPath) { if (!p || p.p50 == null || r.p50 == null) continue; const deltaPct = ((r.p50 - p.p50) / p.p50) * 100; const flag = deltaPct > 15 ? " ⚠ SLOWER" : deltaPct < -15 ? " ✓ faster" : ""; - console.error(` ${pad(r.name, 26)} ${pad(p.p50, 7)} → ${pad(r.p50, 7)}ms (${deltaPct >= 0 ? "+" : ""}${deltaPct.toFixed(0)}%)${flag}`); + console.error( + ` ${pad(r.name, 26)} ${pad(p.p50, 7)} → ${pad(r.p50, 7)}ms (${deltaPct >= 0 ? "+" : ""}${deltaPct.toFixed(0)}%)${flag}` + ); } } diff --git a/scripts/railway-mcp-query-smoke.mjs b/scripts/railway-mcp-query-smoke.mjs index 6e5a7c49d..22dac0cb0 100644 --- a/scripts/railway-mcp-query-smoke.mjs +++ b/scripts/railway-mcp-query-smoke.mjs @@ -52,16 +52,16 @@ // Exit codes: 0 = the seed + MCP query gate passed; 1 = a check failed or the // origin was unreachable; 2 = usage error. -import crypto from 'node:crypto'; -import process from 'node:process'; -import { fileURLToPath } from 'node:url'; +import crypto from "node:crypto"; +import process from "node:process"; +import { fileURLToPath } from "node:url"; import { establishOwnerSessionCookie, extractCsrfFieldValue, findSetCookiePair, getSetCookieList, -} from './lib/owner-session.mjs'; +} from "./lib/owner-session.mjs"; // Re-exported for this module's own offline unit tests // (railway-mcp-query-smoke.test.mjs), which pin the cookie/CSRF parsing @@ -79,43 +79,43 @@ export { extractCsrfFieldValue, findSetCookiePair }; // rather than inventing a connector. The records are hand-built here, NOT // produced by running the seed connector — task 3.2 is explicit that the seed is // a hand-imported fixture with no connector run. -export const SEED_CONNECTOR_ID = 'https://registry.pdpp.org/connectors/spotify'; -export const SEED_STREAM = 'top_artists'; +export const SEED_CONNECTOR_ID = "https://registry.pdpp.org/connectors/spotify"; +export const SEED_STREAM = "top_artists"; // Stable keys + matching data.id (ingestRecord rejects a key that disagrees with // data.id). Deterministic timestamps keep re-runs byte-identical; the live // smoke deliberately avoids unadvertised sort fields and only asserts presence. export const SEED_RECORDS = [ { - key: 'railway-seed-artist-1', + key: "railway-seed-artist-1", data: { - id: 'railway-seed-artist-1', - name: 'Deploy Test Quartet', - genres: ['test-fixture'], + id: "railway-seed-artist-1", + name: "Deploy Test Quartet", + genres: ["test-fixture"], popularity: 41, followers: 10, - source_updated_at: '2026-01-01T00:00:01.000Z', + source_updated_at: "2026-01-01T00:00:01.000Z", }, - emitted_at: '2026-01-01T00:00:01.000Z', + emitted_at: "2026-01-01T00:00:01.000Z", }, { - key: 'railway-seed-artist-2', + key: "railway-seed-artist-2", data: { - id: 'railway-seed-artist-2', - name: 'Restart Survival Band', - genres: ['test-fixture'], + id: "railway-seed-artist-2", + name: "Restart Survival Band", + genres: ["test-fixture"], popularity: 42, followers: 20, - source_updated_at: '2026-01-01T00:00:02.000Z', + source_updated_at: "2026-01-01T00:00:02.000Z", }, - emitted_at: '2026-01-01T00:00:02.000Z', + emitted_at: "2026-01-01T00:00:02.000Z", }, ]; // Build the NDJSON body POST /v1/ingest/:stream expects: one JSON record per // line. The operation splits on \n and JSON.parses each non-empty line. export function buildSeedNdjson(records = SEED_RECORDS) { - return records.map((record) => JSON.stringify(record)).join('\n'); + return records.map((record) => JSON.stringify(record)).join("\n"); } // --------------------------------------------------------------------------- @@ -124,27 +124,27 @@ export function buildSeedNdjson(records = SEED_RECORDS) { export function mcpInitializeMessage(id = 1) { return { - jsonrpc: '2.0', + jsonrpc: "2.0", id, - method: 'initialize', + method: "initialize", params: { - protocolVersion: '2024-11-05', + protocolVersion: "2024-11-05", capabilities: {}, - clientInfo: { name: 'railway-mcp-query-smoke', version: '1' }, + clientInfo: { name: "railway-mcp-query-smoke", version: "1" }, }, }; } export function mcpToolsListMessage(id = 2) { - return { jsonrpc: '2.0', id, method: 'tools/list', params: {} }; + return { jsonrpc: "2.0", id, method: "tools/list", params: {} }; } export function mcpQueryRecordsMessage(stream, args = {}, id = 3) { return { - jsonrpc: '2.0', + jsonrpc: "2.0", id, - method: 'tools/call', - params: { name: 'query_records', arguments: { stream, ...args } }, + method: "tools/call", + params: { name: "query_records", arguments: { stream, ...args } }, }; } @@ -152,17 +152,17 @@ export function mcpQueryRecordsMessage(stream, args = {}, id = 3) { // SSE-framed text/event-stream. Normalize both to the parsed JSON-RPC object so // the assertions do not care which transport the deploy negotiated. export function parseMcpResponseText(contentType, text) { - const ct = String(contentType || '').toLowerCase(); - if (ct.includes('text/event-stream')) { + const ct = String(contentType || "").toLowerCase(); + if (ct.includes("text/event-stream")) { // Concatenate the data: lines of the last event and JSON.parse them. const dataLines = []; - for (const rawLine of String(text).split('\n')) { - const line = rawLine.replace(/\r$/, ''); - if (line.startsWith('data:')) { - dataLines.push(line.slice('data:'.length).trim()); + for (const rawLine of String(text).split("\n")) { + const line = rawLine.replace(/\r$/, ""); + if (line.startsWith("data:")) { + dataLines.push(line.slice("data:".length).trim()); } } - const payload = dataLines.join(''); + const payload = dataLines.join(""); return payload ? JSON.parse(payload) : null; } return text ? JSON.parse(text) : null; @@ -189,14 +189,14 @@ export function assertSeedRecordsPresent(rpc, expectedRecords = SEED_RECORDS) { } const returned = extractRecordsFromQueryResult(rpc); const returnedKeys = new Set( - returned.map((entry) => entry?.key ?? entry?.id ?? entry?.data?.id).filter((k) => k != null), + returned.map((entry) => entry?.key ?? entry?.id ?? entry?.data?.id).filter((k) => k != null) ); const expectedKeys = expectedRecords.map((record) => record.key); const missing = expectedKeys.filter((key) => !returnedKeys.has(key)); if (missing.length > 0) { return { ok: false, - reason: `seeded record(s) missing from query result: ${missing.join(', ')}`, + reason: `seeded record(s) missing from query result: ${missing.join(", ")}`, returnedKeys: [...returnedKeys], }; } @@ -208,16 +208,16 @@ export function assertSeedRecordsPresent(rpc, expectedRecords = SEED_RECORDS) { // as a refusal (token-kind guard) but the gate asserts 401 specifically per the // runbook. Any 2xx is a hard failure (anonymous data access). export function classifyAnonymousMcpStatus(status) { - if (status === 401) return { refused: true, code: 'unauthorized' }; - if (status === 403) return { refused: true, code: 'forbidden' }; - if (status >= 200 && status < 300) return { refused: false, code: 'allowed' }; + if (status === 401) return { refused: true, code: "unauthorized" }; + if (status === 403) return { refused: true, code: "forbidden" }; + if (status >= 200 && status < 300) return { refused: false, code: "allowed" }; // Any other non-2xx still means anonymous access did not succeed. return { refused: true, code: `http_${status}` }; } // PKCE S256 challenge for the client authorization-code flow. export function pkceChallenge(verifier) { - return crypto.createHash('sha256').update(verifier).digest('base64url'); + return crypto.createHash("sha256").update(verifier).digest("base64url"); } // --------------------------------------------------------------------------- @@ -245,8 +245,8 @@ async function readBody(resp) { // token directly. async function establishOwnerSession(origin, ownerPassword, log) { if (!ownerPassword) { - log('owner-login: no --owner-password given; assuming open local-dev owner auth'); - return ''; + log("owner-login: no --owner-password given; assuming open local-dev owner auth"); + return ""; } let sessionCookie; try { @@ -254,20 +254,20 @@ async function establishOwnerSession(origin, ownerPassword, log) { } catch (err) { throw new SmokeError( `owner-login: ${err instanceof Error ? err.message : String(err)}. ` + - 'Is owner auth enabled (PDPP_OWNER_PASSWORD set) and the origin correct?', + "Is owner auth enabled (PDPP_OWNER_PASSWORD set) and the origin correct?" ); } - log('owner-login: owner session established'); + log("owner-login: owner session established"); return sessionCookie; } // Mint an owner access token via the device flow. /device/approve is owner- // session gated when a password is set, so we carry the session cookie. async function mintOwnerToken(origin, sessionCookie, subjectId, log) { - const clientId = 'pdpp-polyfill-owner-bootstrap'; + const clientId = "pdpp-polyfill-owner-bootstrap"; const deviceResp = await fetch(`${origin}/oauth/device_authorization`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, + method: "POST", + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ client_id: clientId }), }); if (deviceResp.status !== 200) { @@ -277,9 +277,9 @@ async function mintOwnerToken(origin, sessionCookie, subjectId, log) { const device = (await readBody(deviceResp)).json; const approveResp = await fetch(`${origin}/device/approve`, { - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", ...(sessionCookie ? { Cookie: sessionCookie } : {}), }, body: JSON.stringify({ user_code: device.user_code, subject_id: subjectId }), @@ -288,15 +288,15 @@ async function mintOwnerToken(origin, sessionCookie, subjectId, log) { const { text } = await readBody(approveResp); throw new SmokeError( `device/approve failed ${approveResp.status}: ${text}. ` + - 'If owner auth is enabled, pass --owner-password so the session can approve.', + "If owner auth is enabled, pass --owner-password so the session can approve." ); } const tokenResp = await fetch(`${origin}/oauth/token`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, + method: "POST", + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ - grant_type: 'urn:ietf:params:oauth:grant-type:device_code', + grant_type: "urn:ietf:params:oauth:grant-type:device_code", device_code: device.device_code, client_id: clientId, }), @@ -305,7 +305,7 @@ async function mintOwnerToken(origin, sessionCookie, subjectId, log) { const { text } = await readBody(tokenResp); throw new SmokeError(`/oauth/token (device_code) failed ${tokenResp.status}: ${text}`); } - log('owner-token: minted owner access token'); + log("owner-token: minted owner access token"); return (await readBody(tokenResp)).json.access_token; } @@ -315,35 +315,35 @@ async function registerSeedManifest(origin, log) { // declares. Re-register is idempotent (409 on unchanged version is fine). const manifest = { connector_id: SEED_CONNECTOR_ID, - name: 'Spotify (Railway seed fixture)', - version: '1.0.0', + name: "Spotify (Railway seed fixture)", + version: "1.0.0", streams: [ { name: SEED_STREAM, - description: 'Railway Core smoke fixture artists', - semantics: 'mutable_state', + description: "Railway Core smoke fixture artists", + semantics: "mutable_state", schema: { - type: 'object', + type: "object", properties: { - id: { type: 'string' }, - name: { type: 'string' }, - genres: { type: 'array', items: { type: 'string' } }, - popularity: { type: 'integer' }, - followers: { type: 'integer' }, - source_updated_at: { type: 'string', format: 'date-time' }, + id: { type: "string" }, + name: { type: "string" }, + genres: { type: "array", items: { type: "string" } }, + popularity: { type: "integer" }, + followers: { type: "integer" }, + source_updated_at: { type: "string", format: "date-time" }, }, - required: ['id', 'name'], + required: ["id", "name"], }, - primary_key: ['id'], - cursor_field: 'source_updated_at', - consent_time_field: 'source_updated_at', + primary_key: ["id"], + cursor_field: "source_updated_at", + consent_time_field: "source_updated_at", selection: { fields: true, resources: true }, }, ], }; const resp = await fetch(`${origin}/connectors`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, + method: "POST", + headers: { "Content-Type": "application/json" }, body: JSON.stringify(manifest), }); if (![200, 201, 409].includes(resp.status)) { @@ -357,10 +357,10 @@ async function registerSeedManifest(origin, log) { async function seedRecords(origin, ownerToken, log) { const url = `${origin}/v1/ingest/${encodeURIComponent(SEED_STREAM)}?connector_id=${encodeURIComponent(SEED_CONNECTOR_ID)}`; const resp = await fetch(url, { - method: 'POST', + method: "POST", headers: { Authorization: `Bearer ${ownerToken}`, - 'Content-Type': 'application/x-ndjson', + "Content-Type": "application/x-ndjson", }, body: buildSeedNdjson(), }); @@ -370,7 +370,7 @@ async function seedRecords(origin, ownerToken, log) { } if (!json || json.records_accepted !== SEED_RECORDS.length) { throw new SmokeError( - `ingest accepted ${json?.records_accepted} of ${SEED_RECORDS.length} records; rejected ${json?.records_rejected}. errors=${JSON.stringify(json?.errors)}`, + `ingest accepted ${json?.records_accepted} of ${SEED_RECORDS.length} records; rejected ${json?.records_rejected}. errors=${JSON.stringify(json?.errors)}` ); } log(`seed: ingested ${json.records_accepted} record(s) into ${SEED_STREAM}`); @@ -378,20 +378,20 @@ async function seedRecords(origin, ownerToken, log) { async function assertAnonymousMcpRefused(origin, log) { const resp = await fetch(`${origin}/mcp`, { - method: 'POST', - headers: { Accept: 'application/json, text/event-stream', 'Content-Type': 'application/json' }, + method: "POST", + headers: { Accept: "application/json, text/event-stream", "Content-Type": "application/json" }, body: JSON.stringify(mcpInitializeMessage()), }); const verdict = classifyAnonymousMcpStatus(resp.status); if (!verdict.refused) { throw new SmokeError( - `anonymous /mcp was NOT refused (status ${resp.status}). A public origin must not serve MCP anonymously.`, + `anonymous /mcp was NOT refused (status ${resp.status}). A public origin must not serve MCP anonymously.` ); } if (resp.status !== 401) { log(`anonymous /mcp refused with ${resp.status} (${verdict.code}); runbook expects 401`); } else { - log('anonymous /mcp refused with 401'); + log("anonymous /mcp refused with 401"); } return resp.status; } @@ -400,15 +400,15 @@ async function assertAnonymousMcpRefused(origin, log) { // authorization-code flow with consent approval under the owner session. async function mintClientToken(origin, sessionCookie, log) { const registerResp = await fetch(`${origin}/oauth/register`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, + method: "POST", + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ - client_name: 'Railway MCP query smoke client', - redirect_uris: ['https://client.example/callback'], - grant_types: ['authorization_code', 'refresh_token'], - response_types: ['code'], - application_type: 'web', - token_endpoint_auth_method: 'none', + client_name: "Railway MCP query smoke client", + redirect_uris: ["https://client.example/callback"], + grant_types: ["authorization_code", "refresh_token"], + response_types: ["code"], + application_type: "web", + token_endpoint_auth_method: "none", }), }); if (registerResp.status !== 201) { @@ -417,58 +417,58 @@ async function mintClientToken(origin, sessionCookie, log) { } const client = (await readBody(registerResp)).json; - const verifier = crypto.randomBytes(32).toString('base64url'); + const verifier = crypto.randomBytes(32).toString("base64url"); const authorizationDetails = [ { - type: 'https://pdpp.org/data-access', - source: { kind: 'connector', id: SEED_CONNECTOR_ID }, - purpose_code: 'https://pdpp.org/purpose/personal_ai_assistant', - purpose_description: 'Railway MCP query smoke', - access_mode: 'continuous', - streams: [{ name: '*' }], + type: "https://pdpp.org/data-access", + source: { kind: "connector", id: SEED_CONNECTOR_ID }, + purpose_code: "https://pdpp.org/purpose/personal_ai_assistant", + purpose_description: "Railway MCP query smoke", + access_mode: "continuous", + streams: [{ name: "*" }], }, ]; const authorizeUrl = new URL(`${origin}/oauth/authorize`); - authorizeUrl.searchParams.set('client_id', client.client_id); - authorizeUrl.searchParams.set('redirect_uri', 'https://client.example/callback'); - authorizeUrl.searchParams.set('response_type', 'code'); - authorizeUrl.searchParams.set('state', 'railway-smoke'); - authorizeUrl.searchParams.set('code_challenge', pkceChallenge(verifier)); - authorizeUrl.searchParams.set('code_challenge_method', 'S256'); - authorizeUrl.searchParams.set('authorization_details', JSON.stringify(authorizationDetails)); + authorizeUrl.searchParams.set("client_id", client.client_id); + authorizeUrl.searchParams.set("redirect_uri", "https://client.example/callback"); + authorizeUrl.searchParams.set("response_type", "code"); + authorizeUrl.searchParams.set("state", "railway-smoke"); + authorizeUrl.searchParams.set("code_challenge", pkceChallenge(verifier)); + authorizeUrl.searchParams.set("code_challenge_method", "S256"); + authorizeUrl.searchParams.set("authorization_details", JSON.stringify(authorizationDetails)); const authorizeResp = await fetch(authorizeUrl, { - redirect: 'manual', + redirect: "manual", headers: sessionCookie ? { Cookie: sessionCookie } : {}, }); if (authorizeResp.status !== 302) { const { text } = await readBody(authorizeResp); throw new SmokeError(`oauth/authorize expected 302, got ${authorizeResp.status}: ${text}`); } - const consentUrl = new URL(authorizeResp.headers.get('location'), origin); - const requestUri = consentUrl.searchParams.get('request_uri'); + const consentUrl = new URL(authorizeResp.headers.get("location"), origin); + const requestUri = consentUrl.searchParams.get("request_uri"); if (!requestUri) { - throw new SmokeError('oauth/authorize did not return a consent request_uri'); + throw new SmokeError("oauth/authorize did not return a consent request_uri"); } // Consent approval is owner-session + CSRF gated. Read the consent CSRF from // the consent page rendered for this request_uri, then approve. const consentPageResp = await fetch(consentUrl, { - headers: { Accept: 'text/html', ...(sessionCookie ? { Cookie: sessionCookie } : {}) }, - redirect: 'manual', + headers: { Accept: "text/html", ...(sessionCookie ? { Cookie: sessionCookie } : {}) }, + redirect: "manual", }); - const consentCsrfCookie = findSetCookiePair(getSetCookieList(consentPageResp), 'pdpp_owner_csrf'); + const consentCsrfCookie = findSetCookiePair(getSetCookieList(consentPageResp), "pdpp_owner_csrf"); const consentCsrfField = extractCsrfFieldValue(await consentPageResp.text()); - const approveHeaders = { 'Content-Type': 'application/x-www-form-urlencoded' }; + const approveHeaders = { "Content-Type": "application/x-www-form-urlencoded" }; const cookieParts = [sessionCookie, consentCsrfCookie].filter(Boolean); - if (cookieParts.length > 0) approveHeaders.Cookie = cookieParts.join('; '); - const approveBody = { request_uri: requestUri, subject_id: 'owner_railway_smoke' }; + if (cookieParts.length > 0) approveHeaders.Cookie = cookieParts.join("; "); + const approveBody = { request_uri: requestUri, subject_id: "owner_railway_smoke" }; if (consentCsrfField) approveBody._csrf = consentCsrfField; const approveResp = await fetch(`${origin}/consent/approve`, { - method: 'POST', - redirect: 'manual', + method: "POST", + redirect: "manual", headers: approveHeaders, body: new URLSearchParams(approveBody).toString(), }); @@ -476,20 +476,20 @@ async function mintClientToken(origin, sessionCookie, log) { const { text } = await readBody(approveResp); throw new SmokeError(`consent/approve expected 302, got ${approveResp.status}: ${text}`); } - const callback = new URL(approveResp.headers.get('location')); - const code = callback.searchParams.get('code'); + const callback = new URL(approveResp.headers.get("location")); + const code = callback.searchParams.get("code"); if (!code) { - throw new SmokeError('consent/approve did not return an authorization code'); + throw new SmokeError("consent/approve did not return an authorization code"); } const tokenResp = await fetch(`${origin}/oauth/token`, { - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ - grant_type: 'authorization_code', + grant_type: "authorization_code", code, client_id: client.client_id, - redirect_uri: 'https://client.example/callback', + redirect_uri: "https://client.example/callback", code_verifier: verifier, }).toString(), }); @@ -497,22 +497,22 @@ async function mintClientToken(origin, sessionCookie, log) { const { text } = await readBody(tokenResp); throw new SmokeError(`oauth/token (authorization_code) failed ${tokenResp.status}: ${text}`); } - log('client-token: minted scoped client access token'); + log("client-token: minted scoped client access token"); return (await readBody(tokenResp)).json.access_token; } async function mcpPost(origin, token, message) { const resp = await fetch(`${origin}/mcp`, { - method: 'POST', + method: "POST", headers: { - Accept: 'application/json, text/event-stream', + Accept: "application/json, text/event-stream", Authorization: `Bearer ${token}`, - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, body: JSON.stringify(message), }); const text = await resp.text(); - const rpc = parseMcpResponseText(resp.headers.get('content-type'), text); + const rpc = parseMcpResponseText(resp.headers.get("content-type"), text); return { status: resp.status, rpc, text }; } @@ -527,19 +527,15 @@ async function runScopedMcpQuery(origin, clientToken, log) { throw new SmokeError(`MCP tools/list failed ${list.status}: ${list.text}`); } const tools = list.rpc?.result?.tools ?? []; - const hasQuery = tools.some((t) => t?.name === 'query_records'); + const hasQuery = tools.some((t) => t?.name === "query_records"); if (!hasQuery) { throw new SmokeError( - `tools/list did not advertise query_records (got ${tools.map((t) => t?.name).join(', ') || 'none'})`, + `tools/list did not advertise query_records (got ${tools.map((t) => t?.name).join(", ") || "none"})` ); } log(`tools/list: ${tools.length} tool(s); query_records present`); - const query = await mcpPost( - origin, - clientToken, - mcpQueryRecordsMessage(SEED_STREAM, { limit: 10 }), - ); + const query = await mcpPost(origin, clientToken, mcpQueryRecordsMessage(SEED_STREAM, { limit: 10 })); if (query.status !== 200) { throw new SmokeError(`query_records failed ${query.status}: ${query.text}`); } @@ -547,7 +543,7 @@ async function runScopedMcpQuery(origin, clientToken, log) { if (!verdict.ok) { throw new SmokeError(`query_records: ${verdict.reason}`); } - log(`query_records: seeded record(s) returned (${verdict.foundKeys.join(', ')})`); + log(`query_records: seeded record(s) returned (${verdict.foundKeys.join(", ")})`); } export async function runLiveSmoke(options) { @@ -563,7 +559,7 @@ export async function runLiveSmoke(options) { await registerSeedManifest(origin, log); await seedRecords(origin, ownerToken, log); } else { - log('seed: skipped (--no-seed); querying existing records only'); + log("seed: skipped (--no-seed); querying existing records only"); } await assertAnonymousMcpRefused(origin, log); const clientToken = await mintClientToken(origin, sessionCookie, log); @@ -579,12 +575,12 @@ export function parseArgs(argv) { const out = { json: false }; for (let i = 0; i < args.length; i += 1) { const arg = args[i]; - if (arg === '--json') out.json = true; - else if (arg === '--no-seed') out.noSeed = true; - else if (arg === '--origin') out.origin = args[++i]; - else if (arg === '--owner-password') out.ownerPassword = args[++i]; - else if (arg === '--subject') out.subjectId = args[++i]; - else if (arg === '--help' || arg === '-h') out.help = true; + if (arg === "--json") out.json = true; + else if (arg === "--no-seed") out.noSeed = true; + else if (arg === "--origin") out.origin = args[++i]; + else if (arg === "--owner-password") out.ownerPassword = args[++i]; + else if (arg === "--subject") out.subjectId = args[++i]; + else if (arg === "--help" || arg === "-h") out.help = true; } return out; } @@ -615,9 +611,9 @@ async function main(argv) { process.stderr.write(`--origin is required.\n\n${USAGE}\n`); process.exit(2); } - const origin = opts.origin.replace(/\/$/, ''); - const ownerPassword = opts.ownerPassword ?? process.env.PDPP_OWNER_PASSWORD ?? ''; - const subjectId = opts.subjectId ?? process.env.PDPP_OWNER_SUBJECT_ID ?? 'owner_local'; + const origin = opts.origin.replace(/\/$/, ""); + const ownerPassword = opts.ownerPassword ?? process.env.PDPP_OWNER_PASSWORD ?? ""; + const subjectId = opts.subjectId ?? process.env.PDPP_OWNER_SUBJECT_ID ?? "owner_local"; const steps = []; const log = (message) => { diff --git a/scripts/read-surface-smoke.mjs b/scripts/read-surface-smoke.mjs index e339a2ce4..91de716e0 100644 --- a/scripts/read-surface-smoke.mjs +++ b/scripts/read-surface-smoke.mjs @@ -13,38 +13,38 @@ // PDPP_READ_SURFACE_TOKEN=... node scripts/read-surface-smoke.mjs \ // --origin https://pdpp.example --connection-id cin_... --stream messages -import crypto from 'node:crypto'; -import { fileURLToPath } from 'node:url'; -import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; +import crypto from "node:crypto"; +import { fileURLToPath } from "node:url"; +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; import { establishOwnerSessionCookie, extractCsrfFieldValue, findSetCookiePair, getSetCookieList, -} from './lib/owner-session.mjs'; +} from "./lib/owner-session.mjs"; -const CORE_MCP_TOOLS = ['schema', 'query_records', 'fetch', 'search', 'aggregate']; +const CORE_MCP_TOOLS = ["schema", "query_records", "fetch", "search", "aggregate"]; const FORBIDDEN_NORMAL_MCP_TOOLS = [ - 'list_streams', - 'discover_event_subscription_capabilities', - 'list_event_subscriptions', - 'create_event_subscription', - 'get_event_subscription', - 'send_test_event', - 'update_event_subscription', - 'delete_event_subscription', + "list_streams", + "discover_event_subscription_capabilities", + "list_event_subscriptions", + "create_event_subscription", + "get_event_subscription", + "send_test_event", + "update_event_subscription", + "delete_event_subscription", ]; -const DEFAULT_STREAM = 'messages'; -const DEFAULT_SEARCH_QUERY = 'test'; -const DEFAULT_DATE_FIELD = 'sent_at'; -const DEFAULT_SINCE = '1970-01-01T00:00:00.000Z'; +const DEFAULT_STREAM = "messages"; +const DEFAULT_SEARCH_QUERY = "test"; +const DEFAULT_DATE_FIELD = "sent_at"; +const DEFAULT_SINCE = "1970-01-01T00:00:00.000Z"; const DEFAULT_TIMEOUT_MS = 30_000; const SCOPED_FULL_SCHEMA_BYTE_BUDGET = 200_000; -const NON_GRANT_BEARER = 'pdpp-read-surface-smoke-non-grant-bearer'; +const NON_GRANT_BEARER = "pdpp-read-surface-smoke-non-grant-bearer"; export function parseArgs(argv) { const args = argv.slice(2); @@ -61,37 +61,37 @@ export function parseArgs(argv) { }; for (let i = 0; i < args.length; i += 1) { const arg = args[i]; - if (arg === '--json') out.json = true; - else if (arg === '--skip-cli') out.skipCli = true; - else if (arg === '--skip-mcp') out.skipMcp = true; - else if (arg === '--skip-rest') out.skipRest = true; - else if (arg === '--origin') out.origin = args[++i]; - else if (arg === '--token') out.token = args[++i]; - else if (arg === '--owner-password') out.ownerPassword = args[++i]; - else if (arg === '--owner-subject') out.ownerSubject = args[++i]; - else if (arg === '--connector-id') out.connectorId = args[++i]; - else if (arg === '--connection-id') out.connectionId = args[++i]; - else if (arg === '--stream') out.stream = args[++i]; - else if (arg === '--search-query') out.searchQuery = args[++i]; - else if (arg === '--date-field') out.dateField = args[++i]; - else if (arg === '--since') out.since = args[++i]; - else if (arg === '--timeout-ms') out.timeoutMs = Number(args[++i]); - else if (arg === '--help' || arg === '-h') out.help = true; + if (arg === "--json") out.json = true; + else if (arg === "--skip-cli") out.skipCli = true; + else if (arg === "--skip-mcp") out.skipMcp = true; + else if (arg === "--skip-rest") out.skipRest = true; + else if (arg === "--origin") out.origin = args[++i]; + else if (arg === "--token") out.token = args[++i]; + else if (arg === "--owner-password") out.ownerPassword = args[++i]; + else if (arg === "--owner-subject") out.ownerSubject = args[++i]; + else if (arg === "--connector-id") out.connectorId = args[++i]; + else if (arg === "--connection-id") out.connectionId = args[++i]; + else if (arg === "--stream") out.stream = args[++i]; + else if (arg === "--search-query") out.searchQuery = args[++i]; + else if (arg === "--date-field") out.dateField = args[++i]; + else if (arg === "--since") out.since = args[++i]; + else if (arg === "--timeout-ms") out.timeoutMs = Number(args[++i]); + else if (arg === "--help" || arg === "-h") out.help = true; } return out; } export function normalizeOrigin(origin) { - return String(origin || '').replace(/\/+$/, ''); + return String(origin || "").replace(/\/+$/, ""); } export function buildUrl(origin, path, params = {}) { const url = new URL(path, normalizeOrigin(origin)); for (const [key, value] of Object.entries(params)) { - if (value === undefined || value === null || value === '') continue; + if (value === undefined || value === null || value === "") continue; if (Array.isArray(value)) { for (const item of value) { - if (item !== undefined && item !== null && item !== '') url.searchParams.append(key, String(item)); + if (item !== undefined && item !== null && item !== "") url.searchParams.append(key, String(item)); } continue; } @@ -102,34 +102,34 @@ export function buildUrl(origin, path, params = {}) { export function mcpInitializeMessage(id = 1) { return { - jsonrpc: '2.0', + jsonrpc: "2.0", id, - method: 'initialize', + method: "initialize", params: { - protocolVersion: '2024-11-05', + protocolVersion: "2024-11-05", capabilities: {}, - clientInfo: { name: 'pdpp-read-surface-smoke', version: '1' }, + clientInfo: { name: "pdpp-read-surface-smoke", version: "1" }, }, }; } export function mcpToolsListMessage(id = 2) { - return { jsonrpc: '2.0', id, method: 'tools/list', params: {} }; + return { jsonrpc: "2.0", id, method: "tools/list", params: {} }; } export function mcpToolCallMessage(name, args = {}, id = 3) { - return { jsonrpc: '2.0', id, method: 'tools/call', params: { name, arguments: args } }; + return { jsonrpc: "2.0", id, method: "tools/call", params: { name, arguments: args } }; } export function parseMcpResponseText(contentType, text) { if (!text) return null; - if (String(contentType || '').includes('text/event-stream')) { + if (String(contentType || "").includes("text/event-stream")) { const dataLines = String(text) - .split('\n') - .filter((line) => line.startsWith('data:')) + .split("\n") + .filter((line) => line.startsWith("data:")) .map((line) => line.slice(5).trim()) .filter(Boolean); - const payload = dataLines.find((line) => line !== '[DONE]'); + const payload = dataLines.find((line) => line !== "[DONE]"); return payload ? JSON.parse(payload) : null; } return JSON.parse(text); @@ -145,38 +145,38 @@ export function extractListData(body) { export function extractRecordId(record) { const candidate = record?.id ?? record?.key ?? record?.record_id ?? record?.data?.id; - return typeof candidate === 'string' && candidate.length > 0 ? candidate : null; + return typeof candidate === "string" && candidate.length > 0 ? candidate : null; } function recordPayload(record) { - if (record?.data && typeof record.data === 'object' && !Array.isArray(record.data)) return record.data; - if (record && typeof record === 'object' && !Array.isArray(record)) return record; + if (record?.data && typeof record.data === "object" && !Array.isArray(record.data)) return record.data; + if (record && typeof record === "object" && !Array.isArray(record)) return record; return {}; } function objectKeys(value) { - return value && typeof value === 'object' && !Array.isArray(value) ? Object.keys(value).sort() : []; + return value && typeof value === "object" && !Array.isArray(value) ? Object.keys(value).sort() : []; } export function classifyStrictProjection(record, expectedFields) { const payload = recordPayload(record); const actual = objectKeys(payload); const expected = [...expectedFields].sort(); - if (actual.length === 0) return { status: 'skip', detail: 'no projected record was available' }; + if (actual.length === 0) return { status: "skip", detail: "no projected record was available" }; const unexpected = actual.filter((key) => !expected.includes(key)); const missing = expected.filter((key) => !actual.includes(key)); if (unexpected.length > 0 || missing.length > 0) { return { - status: 'fail', - detail: `projected payload keys were ${actual.join(',') || ''}; expected exactly ${expected.join(',')}`, + status: "fail", + detail: `projected payload keys were ${actual.join(",") || ""}; expected exactly ${expected.join(",")}`, extra: { payload }, }; } - return { status: 'pass', detail: `projection returned only ${expected.join(',')}` }; + return { status: "pass", detail: `projection returned only ${expected.join(",")}` }; } function extractSearchResults(body) { - if (!body || typeof body !== 'object') return []; + if (!body || typeof body !== "object") return []; if (Array.isArray(body.results)) return body.results; if (Array.isArray(body.hits)) return body.hits; if (Array.isArray(body.data?.results)) return body.data.results; @@ -187,48 +187,55 @@ function extractSearchResults(body) { } function sourceIdForHit(hit) { - const source = hit?.source && typeof hit.source === 'object' ? hit.source : {}; - return hit?.connection_id ?? hit?.connector_instance_id ?? source.connection_id ?? source.connector_instance_id ?? null; + const source = hit?.source && typeof hit.source === "object" ? hit.source : {}; + return ( + hit?.connection_id ?? hit?.connector_instance_id ?? source.connection_id ?? source.connector_instance_id ?? null + ); } export function classifySearchLimitAndSource(body, limit) { const hits = extractSearchResults(body); if (hits.length > limit) { - return { status: 'fail', detail: `returned ${hits.length} hits for limit ${limit}`, extra: { hits } }; + return { status: "fail", detail: `returned ${hits.length} hits for limit ${limit}`, extra: { hits } }; } - if (hits.length === 0) return { status: 'warn', detail: 'search returned no hits; limit held but source identity is unproven' }; + if (hits.length === 0) + return { status: "warn", detail: "search returned no hits; limit held but source identity is unproven" }; const missingSource = hits.filter((hit) => !sourceIdForHit(hit)); if (missingSource.length > 0) { - return { status: 'fail', detail: `${missingSource.length} hit(s) lacked connection_id/source identity`, extra: { hits } }; + return { + status: "fail", + detail: `${missingSource.length} hit(s) lacked connection_id/source identity`, + extra: { hits }, + }; } const sourceMix = body?.meta?.package?.source_mix ?? body?.data?.meta?.package?.source_mix; return { - status: 'pass', + status: "pass", detail: `returned ${hits.length} hit(s) within limit ${limit} with source identity`, ...(Array.isArray(sourceMix) ? { extra: { sourceMix } } : {}), }; } export function classifyPageHandles(body) { - const root = body?.data && typeof body.data === 'object' && !Array.isArray(body.data) ? body.data : body; + const root = body?.data && typeof body.data === "object" && !Array.isArray(body.data) ? body.data : body; const hasMore = root?.has_more ?? body?.has_more; const nextCursor = root?.next_cursor ?? body?.next_cursor ?? body?.links?.next ?? null; const count = root?.meta?.count ?? body?.meta?.count ?? null; if (hasMore === true && !nextCursor) { - return { status: 'fail', detail: 'has_more=true but no next cursor/link was visible', extra: { body } }; + return { status: "fail", detail: "has_more=true but no next cursor/link was visible", extra: { body } }; } if (!count) { - return { status: 'warn', detail: 'page returned but count handle was not visible' }; + return { status: "warn", detail: "page returned but count handle was not visible" }; } - return { status: 'pass', detail: `page handles visible${nextCursor ? ' with cursor' : ''}` }; + return { status: "pass", detail: `page handles visible${nextCursor ? " with cursor" : ""}` }; } export function extractMcpToolData(rpc) { const structured = rpc?.result?.structuredContent; - if (structured && typeof structured === 'object' && 'error' in structured) return structured.error; - if (structured && typeof structured === 'object' && 'data' in structured) return structured.data; + if (structured && typeof structured === "object" && "error" in structured) return structured.error; + if (structured && typeof structured === "object" && "data" in structured) return structured.data; if (structured !== undefined) return structured; - const text = rpc?.result?.content?.find?.((entry) => entry?.type === 'text')?.text; + const text = rpc?.result?.content?.find?.((entry) => entry?.type === "text")?.text; if (!text) return null; try { return JSON.parse(text); @@ -239,25 +246,25 @@ export function extractMcpToolData(rpc) { export function extractMcpToolStructuredContent(rpc) { const structured = rpc?.result?.structuredContent; - return structured && typeof structured === 'object' ? structured : extractMcpToolData(rpc); + return structured && typeof structured === "object" ? structured : extractMcpToolData(rpc); } export function extractMcpToolError(rpc) { if (rpc?.error) { return { - code: rpc.error.code ? String(rpc.error.code) : 'json_rpc_error', - message: String(rpc.error.message ?? 'JSON-RPC error'), + code: rpc.error.code ? String(rpc.error.code) : "json_rpc_error", + message: String(rpc.error.message ?? "JSON-RPC error"), }; } if (!rpc?.result?.isError) return null; const data = extractMcpToolData(rpc); - if (data && typeof data === 'object') { + if (data && typeof data === "object") { return { - code: String(data.code ?? data.type ?? 'tool_error'), + code: String(data.code ?? data.type ?? "tool_error"), message: String(data.message ?? JSON.stringify(data)), }; } - return { code: 'tool_error', message: String(data ?? 'MCP tool error') }; + return { code: "tool_error", message: String(data ?? "MCP tool error") }; } export function bodyErrorCode(body) { @@ -266,52 +273,52 @@ export function bodyErrorCode(body) { export function classifyAmbiguousConnection(status, body) { if (status >= 200 && status < 300) { - return { ok: true, status: 'pass', detail: 'request succeeded without connection_id; grant may be single-source' }; + return { ok: true, status: "pass", detail: "request succeeded without connection_id; grant may be single-source" }; } const code = bodyErrorCode(body); - if (status === 409 && code === 'ambiguous_connection') { - return { ok: true, status: 'pass', detail: 'returned typed ambiguous_connection' }; + if (status === 409 && code === "ambiguous_connection") { + return { ok: true, status: "pass", detail: "returned typed ambiguous_connection" }; } return { ok: false, - status: 'fail', - detail: `expected 2xx or typed ambiguous_connection; got HTTP ${status}${code ? ` ${code}` : ''}`, + status: "fail", + detail: `expected 2xx or typed ambiguous_connection; got HTTP ${status}${code ? ` ${code}` : ""}`, }; } const BOUNDED_AUTH_ERROR_CODES = new Set([ - 'invalid_token', - 'insufficient_scope', - 'unauthorized', - 'invalid_request', - 'invalid_client', - 'authentication_error', + "invalid_token", + "insufficient_scope", + "unauthorized", + "invalid_request", + "invalid_client", + "authentication_error", ]); export function classifyExcludedBearer(status, body) { if (status >= 200 && status < 300) { return { - status: 'fail', - detail: 'ordinary read served a non-grant bearer; scoped-grant requirement not enforced', + status: "fail", + detail: "ordinary read served a non-grant bearer; scoped-grant requirement not enforced", extra: { body }, }; } if (status !== 401 && status !== 403) { - return { status: 'warn', detail: `non-grant bearer rejected with HTTP ${status} (expected bounded 401/403)` }; + return { status: "warn", detail: `non-grant bearer rejected with HTTP ${status} (expected bounded 401/403)` }; } const code = bodyErrorCode(body); if (code && !BOUNDED_AUTH_ERROR_CODES.has(String(code))) { - return { status: 'warn', detail: `non-grant bearer rejected HTTP ${status} with unexpected code ${code}` }; + return { status: "warn", detail: `non-grant bearer rejected HTTP ${status} with unexpected code ${code}` }; } return { - status: 'pass', - detail: `non-grant bearer rejected (HTTP ${status}${code ? ` ${code}` : ''}); reads require the scoped grant`, + status: "pass", + detail: `non-grant bearer rejected (HTTP ${status}${code ? ` ${code}` : ""}); reads require the scoped grant`, }; } function extractSchemaRoot(body) { - if (body?.object === 'schema') return body; - if (body?.data?.object === 'schema') return body.data; + if (body?.object === "schema") return body; + if (body?.data?.object === "schema") return body.data; if (Array.isArray(body?.connectors)) return body; if (Array.isArray(body?.data?.connectors)) return body.data; return null; @@ -319,9 +326,7 @@ function extractSchemaRoot(body) { function schemaStreams(root) { return (root?.connectors ?? []).flatMap((connector) => - Array.isArray(connector?.streams) - ? connector.streams.map((stream) => ({ connector, stream })) - : [], + Array.isArray(connector?.streams) ? connector.streams.map((stream) => ({ connector, stream })) : [] ); } @@ -343,38 +348,38 @@ function schemaConnectionIds(root) { } function addConnectionIds(ids, value) { - for (const key of ['connection_id', 'connector_instance_id']) { + for (const key of ["connection_id", "connector_instance_id"]) { const id = value?.[key]; - if (typeof id === 'string' && id.length > 0) ids.add(id); + if (typeof id === "string" && id.length > 0) ids.add(id); } } export function classifyScopedSchema(body, streamName, connectionId) { const root = extractSchemaRoot(body); if (!root) { - return { status: 'fail', detail: 'schema body did not contain a schema document', extra: { body } }; + return { status: "fail", detail: "schema body did not contain a schema document", extra: { body } }; } const rows = schemaStreams(root); if (rows.length === 0) { - return { status: 'fail', detail: 'scoped schema returned no streams', extra: { body } }; + return { status: "fail", detail: "scoped schema returned no streams", extra: { body } }; } const wrongStream = rows.find(({ stream }) => stream?.name !== streamName); if (wrongStream) { return { - status: 'fail', - detail: `scoped schema included unexpected stream ${wrongStream.stream?.name ?? ''}`, + status: "fail", + detail: `scoped schema included unexpected stream ${wrongStream.stream?.name ?? ""}`, extra: { body }, }; } const ids = schemaConnectionIds(root); if (ids.length !== 1 || ids[0] !== connectionId) { return { - status: 'fail', - detail: `scoped schema connection ids were ${ids.join(',') || ''}, expected ${connectionId}`, + status: "fail", + detail: `scoped schema connection ids were ${ids.join(",") || ""}, expected ${connectionId}`, extra: { body }, }; } - return { status: 'pass', detail: `schema narrowed to ${streamName} / ${connectionId}` }; + return { status: "pass", detail: `schema narrowed to ${streamName} / ${connectionId}` }; } function schemaConnectorKeys(root) { @@ -388,37 +393,37 @@ function schemaConnectorKeys(root) { function addConnectorKey(keys, value) { const key = value?.connector_key; - if (typeof key === 'string' && key.length > 0) keys.add(key); + if (typeof key === "string" && key.length > 0) keys.add(key); } export function classifySourceIdentity(body, connectionId) { const root = extractSchemaRoot(body); if (!root) { - return { status: 'fail', detail: 'schema body did not contain a schema document', extra: { body } }; + return { status: "fail", detail: "schema body did not contain a schema document", extra: { body } }; } const ids = schemaConnectionIds(root); if (ids.length !== 1 || ids[0] !== connectionId) { return { - status: 'fail', - detail: `expected canonical connection_id ${connectionId}; saw ${ids.join(',') || ''}`, + status: "fail", + detail: `expected canonical connection_id ${connectionId}; saw ${ids.join(",") || ""}`, extra: { body }, }; } const connectorKeys = schemaConnectorKeys(root); if (connectorKeys.length === 0) { return { - status: 'warn', + status: "warn", detail: `connection_id ${connectionId} present; connector_key not surfaced by this transport/view`, }; } if (connectorKeys.length > 1) { return { - status: 'fail', - detail: `scoped schema mixed connector_keys ${connectorKeys.join(',')}`, + status: "fail", + detail: `scoped schema mixed connector_keys ${connectorKeys.join(",")}`, extra: { body }, }; } - return { status: 'pass', detail: `canonical source identity ${connectionId} / ${connectorKeys[0]}` }; + return { status: "pass", detail: `canonical source identity ${connectionId} / ${connectorKeys[0]}` }; } export function classifyToolNames(toolNames) { @@ -440,34 +445,34 @@ export function summarizeResults(results) { return { ok: counts.fail === 0, counts }; } -const PARITY_TRANSPORTS = ['REST', 'MCP', 'CLI']; +const PARITY_TRANSPORTS = ["REST", "MCP", "CLI"]; const PARITY_ROW_BY_CHECK = { - 'schema.compact': 'compact_schema', - 'schema.scoped': 'source_scoping', - 'schema.scoped_full': 'source_scoping', - 'schema.source_identity': 'source_identity', - 'query_records.projection': 'projection', - 'fetch.projection': 'projection', - 'record_detail.projection': 'projection', - 'search.fan_in_limit_source_identity': 'search_limit_source', - 'query_records.count': 'count_handle', - 'query_records.sort_count': 'count_handle', - 'aggregate.count': 'aggregate_count', - 'query_records.omit_connection_id': 'typed_ambiguity', - excluded_bearer: 'grant_bearer_only', + "schema.compact": "compact_schema", + "schema.scoped": "source_scoping", + "schema.scoped_full": "source_scoping", + "schema.source_identity": "source_identity", + "query_records.projection": "projection", + "fetch.projection": "projection", + "record_detail.projection": "projection", + "search.fan_in_limit_source_identity": "search_limit_source", + "query_records.count": "count_handle", + "query_records.sort_count": "count_handle", + "aggregate.count": "aggregate_count", + "query_records.omit_connection_id": "typed_ambiguity", + excluded_bearer: "grant_bearer_only", }; const PARITY_ROW_ORDER = [ - 'compact_schema', - 'source_scoping', - 'source_identity', - 'projection', - 'search_limit_source', - 'count_handle', - 'aggregate_count', - 'typed_ambiguity', - 'grant_bearer_only', + "compact_schema", + "source_scoping", + "source_identity", + "projection", + "search_limit_source", + "count_handle", + "aggregate_count", + "typed_ambiguity", + "grant_bearer_only", ]; export function buildParityMatrix(results) { @@ -478,7 +483,7 @@ export function buildParityMatrix(results) { if (!row) continue; const key = `${row}\0${entry.surface}`; const prior = cells.get(key); - if (prior === 'fail') continue; + if (prior === "fail") continue; cells.set(key, entry.status); } @@ -489,10 +494,10 @@ export function buildParityMatrix(results) { let anyPass = false; let anyFail = false; for (const surface of PARITY_TRANSPORTS) { - const status = cells.get(`${row}\0${surface}`) ?? 'absent'; + const status = cells.get(`${row}\0${surface}`) ?? "absent"; transports[surface] = status; - if (status === 'pass') anyPass = true; - if (status === 'fail') anyFail = true; + if (status === "pass") anyPass = true; + if (status === "fail") anyFail = true; } const rowDiverged = anyPass && anyFail; if (rowDiverged) diverged = true; @@ -502,8 +507,8 @@ export function buildParityMatrix(results) { } export function cliCredentialCacheFile(cacheRoot, origin) { - const host = new URL(normalizeOrigin(origin)).host.replace(/[^a-zA-Z0-9.-]/g, '_'); - return join(cacheRoot, 'clients', `${host}.json`); + const host = new URL(normalizeOrigin(origin)).host.replace(/[^a-zA-Z0-9.-]/g, "_"); + return join(cacheRoot, "clients", `${host}.json`); } async function readBody(resp) { @@ -518,7 +523,7 @@ async function readBody(resp) { } function pkceChallenge(verifier) { - return crypto.createHash('sha256').update(verifier).digest('base64url'); + return crypto.createHash("sha256").update(verifier).digest("base64url"); } // Delegates to the shared owner-session helper (scripts/lib/owner-session.mjs @@ -528,21 +533,21 @@ async function establishOwnerSession(origin, ownerPassword) { } async function mintScopedClientToken({ origin, ownerPassword, ownerSubject, connectorId, connectionId, stream }) { - if (!ownerPassword) throw new Error('--owner-password or PDPP_OWNER_PASSWORD is required when --token is omitted'); - if (!connectorId) throw new Error('--connector-id is required when --token is omitted'); + if (!ownerPassword) throw new Error("--owner-password or PDPP_OWNER_PASSWORD is required when --token is omitted"); + if (!connectorId) throw new Error("--connector-id is required when --token is omitted"); const sessionCookie = await establishOwnerSession(origin, ownerPassword); - const redirectUri = 'https://client.example/callback'; + const redirectUri = "https://client.example/callback"; const registerResp = await fetch(`${origin}/oauth/register`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, + method: "POST", + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ - client_name: 'PDPP read-surface smoke client', + client_name: "PDPP read-surface smoke client", redirect_uris: [redirectUri], - grant_types: ['authorization_code', 'refresh_token'], - response_types: ['code'], - application_type: 'web', - token_endpoint_auth_method: 'none', + grant_types: ["authorization_code", "refresh_token"], + response_types: ["code"], + application_type: "web", + token_endpoint_auth_method: "none", }), }); if (registerResp.status !== 201) { @@ -550,57 +555,57 @@ async function mintScopedClientToken({ origin, ownerPassword, ownerSubject, conn throw new Error(`oauth/register failed ${registerResp.status}: ${text}`); } const client = (await readBody(registerResp)).json; - const verifier = crypto.randomBytes(32).toString('base64url'); - const streamGrant = { name: stream || '*' }; + const verifier = crypto.randomBytes(32).toString("base64url"); + const streamGrant = { name: stream || "*" }; if (connectionId) streamGrant.connection_id = connectionId; const authorizationDetails = [ { - type: 'https://pdpp.org/data-access', - source: { kind: 'connector', id: connectorId }, - purpose_code: 'https://pdpp.org/purpose/personal_ai_assistant', - purpose_description: 'PDPP read-surface smoke', - access_mode: 'continuous', + type: "https://pdpp.org/data-access", + source: { kind: "connector", id: connectorId }, + purpose_code: "https://pdpp.org/purpose/personal_ai_assistant", + purpose_description: "PDPP read-surface smoke", + access_mode: "continuous", streams: [streamGrant], }, ]; const authorizeUrl = new URL(`${origin}/oauth/authorize`); - authorizeUrl.searchParams.set('client_id', client.client_id); - authorizeUrl.searchParams.set('redirect_uri', redirectUri); - authorizeUrl.searchParams.set('response_type', 'code'); - authorizeUrl.searchParams.set('state', 'read-surface-smoke'); - authorizeUrl.searchParams.set('code_challenge', pkceChallenge(verifier)); - authorizeUrl.searchParams.set('code_challenge_method', 'S256'); - authorizeUrl.searchParams.set('authorization_details', JSON.stringify(authorizationDetails)); + authorizeUrl.searchParams.set("client_id", client.client_id); + authorizeUrl.searchParams.set("redirect_uri", redirectUri); + authorizeUrl.searchParams.set("response_type", "code"); + authorizeUrl.searchParams.set("state", "read-surface-smoke"); + authorizeUrl.searchParams.set("code_challenge", pkceChallenge(verifier)); + authorizeUrl.searchParams.set("code_challenge_method", "S256"); + authorizeUrl.searchParams.set("authorization_details", JSON.stringify(authorizationDetails)); const authorizeResp = await fetch(authorizeUrl, { - redirect: 'manual', + redirect: "manual", headers: { Cookie: sessionCookie }, }); if (authorizeResp.status !== 302) { const { text } = await readBody(authorizeResp); throw new Error(`oauth/authorize failed ${authorizeResp.status}: ${text}`); } - const consentUrl = new URL(authorizeResp.headers.get('location'), origin); - const requestUri = consentUrl.searchParams.get('request_uri'); - if (!requestUri) throw new Error('oauth/authorize did not return a consent request_uri'); + const consentUrl = new URL(authorizeResp.headers.get("location"), origin); + const requestUri = consentUrl.searchParams.get("request_uri"); + if (!requestUri) throw new Error("oauth/authorize did not return a consent request_uri"); const consentPageResp = await fetch(consentUrl, { - headers: { Accept: 'text/html', Cookie: sessionCookie }, - redirect: 'manual', + headers: { Accept: "text/html", Cookie: sessionCookie }, + redirect: "manual", }); - const consentCsrfCookie = findSetCookiePair(getSetCookieList(consentPageResp), 'pdpp_owner_csrf'); + const consentCsrfCookie = findSetCookiePair(getSetCookieList(consentPageResp), "pdpp_owner_csrf"); const consentCsrfField = extractCsrfFieldValue(await consentPageResp.text()); - const approveHeaders = { 'Content-Type': 'application/x-www-form-urlencoded' }; + const approveHeaders = { "Content-Type": "application/x-www-form-urlencoded" }; const cookieParts = [sessionCookie, consentCsrfCookie].filter(Boolean); - if (cookieParts.length > 0) approveHeaders.Cookie = cookieParts.join('; '); - const approveBody = { request_uri: requestUri, subject_id: ownerSubject || 'owner_local' }; + if (cookieParts.length > 0) approveHeaders.Cookie = cookieParts.join("; "); + const approveBody = { request_uri: requestUri, subject_id: ownerSubject || "owner_local" }; if (consentCsrfField) approveBody._csrf = consentCsrfField; const approveResp = await fetch(`${origin}/consent/approve`, { - method: 'POST', - redirect: 'manual', + method: "POST", + redirect: "manual", headers: approveHeaders, body: new URLSearchParams(approveBody).toString(), }); @@ -608,15 +613,15 @@ async function mintScopedClientToken({ origin, ownerPassword, ownerSubject, conn const { text } = await readBody(approveResp); throw new Error(`consent/approve failed ${approveResp.status}: ${text}`); } - const callback = new URL(approveResp.headers.get('location')); - const code = callback.searchParams.get('code'); - if (!code) throw new Error('consent/approve did not return an authorization code'); + const callback = new URL(approveResp.headers.get("location")); + const code = callback.searchParams.get("code"); + if (!code) throw new Error("consent/approve did not return an authorization code"); const tokenResp = await fetch(`${origin}/oauth/token`, { - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ - grant_type: 'authorization_code', + grant_type: "authorization_code", code, client_id: client.client_id, redirect_uri: redirectUri, @@ -631,14 +636,11 @@ async function mintScopedClientToken({ origin, ownerPassword, ownerSubject, conn } export function classifyCliHelp(stdout) { - const text = String(stdout || ''); + const text = String(stdout || ""); const hasCliHelp = /PDPP CLI/.test(text); - const advertisedReadCommands = [ - /\bquery[_-]?records\b/i, - /\bsearch\b/i, - /\baggregate\b/i, - /\bfetch\b/i, - ].filter((pattern) => pattern.test(text)); + const advertisedReadCommands = [/\bquery[_-]?records\b/i, /\bsearch\b/i, /\baggregate\b/i, /\bfetch\b/i].filter( + (pattern) => pattern.test(text) + ); return { hasCliHelp, hasGrantScopedReadCommands: advertisedReadCommands.length > 0, @@ -650,28 +652,31 @@ function result(status, surface, name, detail, extra = {}) { } function ok(surface, name, detail, extra) { - return result('pass', surface, name, detail, extra); + return result("pass", surface, name, detail, extra); } function warn(surface, name, detail, extra) { - return result('warn', surface, name, detail, extra); + return result("warn", surface, name, detail, extra); } function fail(surface, name, detail, extra) { - return result('fail', surface, name, detail, extra); + return result("fail", surface, name, detail, extra); } function skip(surface, name, detail, extra) { - return result('skip', surface, name, detail, extra); + return result("skip", surface, name, detail, extra); } -async function fetchText(url, { token, method = 'GET', body, timeoutMs = DEFAULT_TIMEOUT_MS, accept = 'application/json' } = {}) { +async function fetchText( + url, + { token, method = "GET", body, timeoutMs = DEFAULT_TIMEOUT_MS, accept = "application/json" } = {} +) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); try { const headers = { Accept: accept }; if (token) headers.Authorization = `Bearer ${token}`; - if (body !== undefined) headers['Content-Type'] = 'application/json'; + if (body !== undefined) headers["Content-Type"] = "application/json"; const resp = await fetch(url, { method, headers, @@ -685,7 +690,7 @@ async function fetchText(url, { token, method = 'GET', body, timeoutMs = DEFAULT } catch { json = null; } - return { status: resp.status, contentType: resp.headers.get('content-type'), text, json }; + return { status: resp.status, contentType: resp.headers.get("content-type"), text, json }; } finally { clearTimeout(timer); } @@ -698,10 +703,10 @@ async function getJson(origin, path, params, opts) { async function mcpPost(origin, token, message, timeoutMs) { const resp = await fetchText(`${normalizeOrigin(origin)}/mcp`, { token, - method: 'POST', + method: "POST", body: message, timeoutMs, - accept: 'application/json, text/event-stream', + accept: "application/json, text/event-stream", }); const rpc = resp.text ? parseMcpResponseText(resp.contentType, resp.text) : null; return { ...resp, rpc }; @@ -718,15 +723,15 @@ async function pushChecked(results, surface, name, fn) { function require2xx(resp, surface, name) { if (resp.status >= 200 && resp.status < 300) return null; const code = bodyErrorCode(resp.json); - return fail(surface, name, `HTTP ${resp.status}${code ? ` ${code}` : ''}`, { body: resp.json ?? resp.text }); + return fail(surface, name, `HTTP ${resp.status}${code ? ` ${code}` : ""}`, { body: resp.json ?? resp.text }); } function requireMcpOk(resp, name) { if (resp.status < 200 || resp.status >= 300) { - return fail('MCP', name, `HTTP ${resp.status}`, { body: resp.json ?? resp.text }); + return fail("MCP", name, `HTTP ${resp.status}`, { body: resp.json ?? resp.text }); } const toolError = extractMcpToolError(resp.rpc); - if (toolError) return fail('MCP', name, `${toolError.code}: ${toolError.message}`); + if (toolError) return fail("MCP", name, `${toolError.code}: ${toolError.message}`); return null; } @@ -734,226 +739,231 @@ async function runRestChecks({ origin, token, connectionId, stream, searchQuery, const results = []; let firstRecordId = null; - await pushChecked(results, 'REST', 'schema', async () => { - const resp = await getJson(origin, '/v1/schema', {}, { token, timeoutMs }); - const failure = require2xx(resp, 'REST', 'schema'); - return failure ?? ok('REST', 'schema', 'schema returned'); + await pushChecked(results, "REST", "schema", async () => { + const resp = await getJson(origin, "/v1/schema", {}, { token, timeoutMs }); + const failure = require2xx(resp, "REST", "schema"); + return failure ?? ok("REST", "schema", "schema returned"); }); - await pushChecked(results, 'REST', 'schema.compact', async () => { - const resp = await getJson(origin, '/v1/schema', { view: 'compact' }, { token, timeoutMs }); - const failure = require2xx(resp, 'REST', 'schema.compact'); + await pushChecked(results, "REST", "schema.compact", async () => { + const resp = await getJson(origin, "/v1/schema", { view: "compact" }, { token, timeoutMs }); + const failure = require2xx(resp, "REST", "schema.compact"); if (failure) return failure; - return resp.json?.detail === 'compact' - ? ok('REST', 'schema.compact', 'compact schema returned') - : fail('REST', 'schema.compact', 'schema did not carry detail=compact', { body: resp.json }); + return resp.json?.detail === "compact" + ? ok("REST", "schema.compact", "compact schema returned") + : fail("REST", "schema.compact", "schema did not carry detail=compact", { body: resp.json }); }); - await pushChecked(results, 'REST', 'schema.scoped', async () => { + await pushChecked(results, "REST", "schema.scoped", async () => { const resp = await getJson( origin, - '/v1/schema', - { view: 'compact', stream, connection_id: connectionId }, - { token, timeoutMs }, + "/v1/schema", + { view: "compact", stream, connection_id: connectionId }, + { token, timeoutMs } ); - const failure = require2xx(resp, 'REST', 'schema.scoped'); + const failure = require2xx(resp, "REST", "schema.scoped"); if (failure) return failure; const verdict = classifyScopedSchema(resp.json, stream, connectionId); - return result(verdict.status, 'REST', 'schema.scoped', verdict.detail, verdict.extra); + return result(verdict.status, "REST", "schema.scoped", verdict.detail, verdict.extra); }); - await pushChecked(results, 'REST', 'schema.source_identity', async () => { + await pushChecked(results, "REST", "schema.source_identity", async () => { const resp = await getJson( origin, - '/v1/schema', - { view: 'compact', stream, connection_id: connectionId }, - { token, timeoutMs }, + "/v1/schema", + { view: "compact", stream, connection_id: connectionId }, + { token, timeoutMs } ); - const failure = require2xx(resp, 'REST', 'schema.source_identity'); + const failure = require2xx(resp, "REST", "schema.source_identity"); if (failure) return failure; const verdict = classifySourceIdentity(resp.json, connectionId); - return result(verdict.status, 'REST', 'schema.source_identity', verdict.detail, verdict.extra); + return result(verdict.status, "REST", "schema.source_identity", verdict.detail, verdict.extra); }); - await pushChecked(results, 'REST', 'list_streams.scoped', async () => { - const resp = await getJson(origin, '/v1/streams', { connection_id: connectionId }, { token, timeoutMs }); - const failure = require2xx(resp, 'REST', 'list_streams.scoped'); + await pushChecked(results, "REST", "list_streams.scoped", async () => { + const resp = await getJson(origin, "/v1/streams", { connection_id: connectionId }, { token, timeoutMs }); + const failure = require2xx(resp, "REST", "list_streams.scoped"); if (failure) return failure; const streams = extractListData(resp.json); - return ok('REST', 'list_streams.scoped', `${streams.length} stream(s) returned`); + return ok("REST", "list_streams.scoped", `${streams.length} stream(s) returned`); }); - await pushChecked(results, 'REST', 'query_records.basic', async () => { + await pushChecked(results, "REST", "query_records.basic", async () => { const resp = await getJson( origin, `/v1/streams/${encodeURIComponent(stream)}/records`, { limit: 1, connection_id: connectionId }, - { token, timeoutMs }, + { token, timeoutMs } ); - const failure = require2xx(resp, 'REST', 'query_records.basic'); + const failure = require2xx(resp, "REST", "query_records.basic"); if (failure) return failure; const records = extractListData(resp.json); firstRecordId = extractRecordId(records[0]); - return ok('REST', 'query_records.basic', `${records.length} record(s) returned`, { firstRecordId }); + return ok("REST", "query_records.basic", `${records.length} record(s) returned`, { firstRecordId }); }); - await pushChecked(results, 'REST', 'query_records.projection', async () => { + await pushChecked(results, "REST", "query_records.projection", async () => { const resp = await getJson( origin, `/v1/streams/${encodeURIComponent(stream)}/records`, - { limit: 1, connection_id: connectionId, fields: ['id'] }, - { token, timeoutMs }, + { limit: 1, connection_id: connectionId, fields: ["id"] }, + { token, timeoutMs } ); - const failure = require2xx(resp, 'REST', 'query_records.projection'); + const failure = require2xx(resp, "REST", "query_records.projection"); if (failure) return failure; - const verdict = classifyStrictProjection(extractListData(resp.json)[0], ['id']); - return result(verdict.status, 'REST', 'query_records.projection', verdict.detail, verdict.extra); + const verdict = classifyStrictProjection(extractListData(resp.json)[0], ["id"]); + return result(verdict.status, "REST", "query_records.projection", verdict.detail, verdict.extra); }); - await pushChecked(results, 'REST', 'query_records.omit_connection_id', async () => { + await pushChecked(results, "REST", "query_records.omit_connection_id", async () => { const resp = await getJson( origin, `/v1/streams/${encodeURIComponent(stream)}/records`, { limit: 1 }, - { token, timeoutMs }, + { token, timeoutMs } ); const verdict = classifyAmbiguousConnection(resp.status, resp.json); - return result(verdict.status, 'REST', 'query_records.omit_connection_id', verdict.detail); + return result(verdict.status, "REST", "query_records.omit_connection_id", verdict.detail); }); - await pushChecked(results, 'REST', 'query_records.sort', async () => { + await pushChecked(results, "REST", "query_records.sort", async () => { const resp = await getJson( origin, `/v1/streams/${encodeURIComponent(stream)}/records`, { limit: 1, connection_id: connectionId, sort: `-${dateField}` }, - { token, timeoutMs }, + { token, timeoutMs } ); - const failure = require2xx(resp, 'REST', 'query_records.sort'); - if (!failure) return ok('REST', 'query_records.sort', `sort=-${dateField} accepted`); - if (bodyErrorCode(resp.json) === 'unsupported_query') return warn('REST', 'query_records.sort', failure.detail); + const failure = require2xx(resp, "REST", "query_records.sort"); + if (!failure) return ok("REST", "query_records.sort", `sort=-${dateField} accepted`); + if (bodyErrorCode(resp.json) === "unsupported_query") return warn("REST", "query_records.sort", failure.detail); return failure; }); - await pushChecked(results, 'REST', 'query_records.count', async () => { + await pushChecked(results, "REST", "query_records.count", async () => { const resp = await getJson( origin, `/v1/streams/${encodeURIComponent(stream)}/records`, - { limit: 1, connection_id: connectionId, count: 'exact' }, - { token, timeoutMs }, + { limit: 1, connection_id: connectionId, count: "exact" }, + { token, timeoutMs } ); - const failure = require2xx(resp, 'REST', 'query_records.count'); + const failure = require2xx(resp, "REST", "query_records.count"); if (!failure) { const verdict = classifyPageHandles(resp.json); - return result(verdict.status, 'REST', 'query_records.count', `count=exact accepted; ${verdict.detail}`, verdict.extra); + return result( + verdict.status, + "REST", + "query_records.count", + `count=exact accepted; ${verdict.detail}`, + verdict.extra + ); } - if (bodyErrorCode(resp.json) === 'unsupported_query') return warn('REST', 'query_records.count', failure.detail); + if (bodyErrorCode(resp.json) === "unsupported_query") return warn("REST", "query_records.count", failure.detail); return failure; }); - await pushChecked(results, 'REST', 'query_records.filter_object', async () => { + await pushChecked(results, "REST", "query_records.filter_object", async () => { const resp = await getJson( origin, `/v1/streams/${encodeURIComponent(stream)}/records`, { limit: 1, connection_id: connectionId, [`filter[${dateField}][gte]`]: since }, - { token, timeoutMs }, + { token, timeoutMs } ); - const failure = require2xx(resp, 'REST', 'query_records.filter_object'); - return failure ? warn('REST', 'query_records.filter_object', failure.detail) : ok('REST', 'query_records.filter_object', 'typed bracket filter accepted'); + const failure = require2xx(resp, "REST", "query_records.filter_object"); + return failure + ? warn("REST", "query_records.filter_object", failure.detail) + : ok("REST", "query_records.filter_object", "typed bracket filter accepted"); }); - await pushChecked(results, 'REST', 'record_detail', async () => { - if (!firstRecordId) return skip('REST', 'record_detail', 'no record id returned by basic query'); + await pushChecked(results, "REST", "record_detail", async () => { + if (!firstRecordId) return skip("REST", "record_detail", "no record id returned by basic query"); const resp = await getJson( origin, `/v1/streams/${encodeURIComponent(stream)}/records/${encodeURIComponent(firstRecordId)}`, { connection_id: connectionId }, - { token, timeoutMs }, + { token, timeoutMs } ); - const failure = require2xx(resp, 'REST', 'record_detail'); - return failure ?? ok('REST', 'record_detail', `record ${firstRecordId} returned`); + const failure = require2xx(resp, "REST", "record_detail"); + return failure ?? ok("REST", "record_detail", `record ${firstRecordId} returned`); }); - await pushChecked(results, 'REST', 'record_detail.projection', async () => { - if (!firstRecordId) return skip('REST', 'record_detail.projection', 'no record id returned by basic query'); + await pushChecked(results, "REST", "record_detail.projection", async () => { + if (!firstRecordId) return skip("REST", "record_detail.projection", "no record id returned by basic query"); const resp = await getJson( origin, `/v1/streams/${encodeURIComponent(stream)}/records/${encodeURIComponent(firstRecordId)}`, - { connection_id: connectionId, fields: ['id'] }, - { token, timeoutMs }, + { connection_id: connectionId, fields: ["id"] }, + { token, timeoutMs } ); - const failure = require2xx(resp, 'REST', 'record_detail.projection'); + const failure = require2xx(resp, "REST", "record_detail.projection"); if (failure) return failure; - const verdict = classifyStrictProjection(resp.json?.data ?? resp.json, ['id']); - return result(verdict.status, 'REST', 'record_detail.projection', verdict.detail, verdict.extra); + const verdict = classifyStrictProjection(resp.json?.data ?? resp.json, ["id"]); + return result(verdict.status, "REST", "record_detail.projection", verdict.detail, verdict.extra); }); - await pushChecked(results, 'REST', 'search.lexical', async () => { + await pushChecked(results, "REST", "search.lexical", async () => { const resp = await getJson( origin, - '/v1/search', + "/v1/search", { q: searchQuery, streams: stream, limit: 1, connection_id: connectionId }, - { token, timeoutMs }, + { token, timeoutMs } ); - const failure = require2xx(resp, 'REST', 'search.lexical'); - return failure ?? ok('REST', 'search.lexical', 'lexical search returned'); + const failure = require2xx(resp, "REST", "search.lexical"); + return failure ?? ok("REST", "search.lexical", "lexical search returned"); }); - await pushChecked(results, 'REST', 'search.fan_in_limit_source_identity', async () => { + await pushChecked(results, "REST", "search.fan_in_limit_source_identity", async () => { const limit = 3; - const resp = await getJson( - origin, - '/v1/search', - { q: searchQuery, streams: stream, limit }, - { token, timeoutMs }, - ); - const failure = require2xx(resp, 'REST', 'search.fan_in_limit_source_identity'); + const resp = await getJson(origin, "/v1/search", { q: searchQuery, streams: stream, limit }, { token, timeoutMs }); + const failure = require2xx(resp, "REST", "search.fan_in_limit_source_identity"); if (failure) return failure; const verdict = classifySearchLimitAndSource(resp.json, limit); - return result(verdict.status, 'REST', 'search.fan_in_limit_source_identity', verdict.detail, verdict.extra); + return result(verdict.status, "REST", "search.fan_in_limit_source_identity", verdict.detail, verdict.extra); }); - await pushChecked(results, 'REST', 'aggregate.count', async () => { + await pushChecked(results, "REST", "aggregate.count", async () => { const resp = await getJson( origin, `/v1/streams/${encodeURIComponent(stream)}/aggregate`, - { metric: 'count', connection_id: connectionId }, - { token, timeoutMs }, + { metric: "count", connection_id: connectionId }, + { token, timeoutMs } ); - const failure = require2xx(resp, 'REST', 'aggregate.count'); - return failure ?? ok('REST', 'aggregate.count', 'count aggregate returned'); + const failure = require2xx(resp, "REST", "aggregate.count"); + return failure ?? ok("REST", "aggregate.count", "count aggregate returned"); }); - await pushChecked(results, 'REST', 'aggregate.group_by_time', async () => { + await pushChecked(results, "REST", "aggregate.group_by_time", async () => { const resp = await getJson( origin, `/v1/streams/${encodeURIComponent(stream)}/aggregate`, - { metric: 'count', group_by_time: dateField, granularity: 'day', limit: 7, connection_id: connectionId }, - { token, timeoutMs }, + { metric: "count", group_by_time: dateField, granularity: "day", limit: 7, connection_id: connectionId }, + { token, timeoutMs } ); - const failure = require2xx(resp, 'REST', 'aggregate.group_by_time'); - return failure ? warn('REST', 'aggregate.group_by_time', failure.detail) : ok('REST', 'aggregate.group_by_time', `${dateField}/day aggregate returned`); + const failure = require2xx(resp, "REST", "aggregate.group_by_time"); + return failure + ? warn("REST", "aggregate.group_by_time", failure.detail) + : ok("REST", "aggregate.group_by_time", `${dateField}/day aggregate returned`); }); - await pushChecked(results, 'REST', 'event_capabilities', async () => { - const resp = await getJson(origin, '/.well-known/oauth-protected-resource', {}, { timeoutMs }); - const failure = require2xx(resp, 'REST', 'event_capabilities'); + await pushChecked(results, "REST", "event_capabilities", async () => { + const resp = await getJson(origin, "/.well-known/oauth-protected-resource", {}, { timeoutMs }); + const failure = require2xx(resp, "REST", "event_capabilities"); if (failure) return failure; const supported = resp.json?.capabilities?.client_event_subscriptions?.supported; return supported === true - ? ok('REST', 'event_capabilities', 'client event subscriptions advertised') - : warn('REST', 'event_capabilities', 'client event subscriptions not advertised'); + ? ok("REST", "event_capabilities", "client event subscriptions advertised") + : warn("REST", "event_capabilities", "client event subscriptions not advertised"); }); - await pushChecked(results, 'REST', 'list_event_subscriptions', async () => { - const resp = await getJson(origin, '/v1/event-subscriptions', {}, { token, timeoutMs }); - const failure = require2xx(resp, 'REST', 'list_event_subscriptions'); - return failure ?? ok('REST', 'list_event_subscriptions', 'event subscriptions listed'); + await pushChecked(results, "REST", "list_event_subscriptions", async () => { + const resp = await getJson(origin, "/v1/event-subscriptions", {}, { token, timeoutMs }); + const failure = require2xx(resp, "REST", "list_event_subscriptions"); + return failure ?? ok("REST", "list_event_subscriptions", "event subscriptions listed"); }); - await pushChecked(results, 'REST', 'excluded_bearer', async () => { - const resp = await getJson(origin, '/v1/schema', {}, { token: NON_GRANT_BEARER, timeoutMs }); + await pushChecked(results, "REST", "excluded_bearer", async () => { + const resp = await getJson(origin, "/v1/schema", {}, { token: NON_GRANT_BEARER, timeoutMs }); const verdict = classifyExcludedBearer(resp.status, resp.json); - return result(verdict.status, 'REST', 'excluded_bearer', verdict.detail, verdict.extra); + return result(verdict.status, "REST", "excluded_bearer", verdict.detail, verdict.extra); }); return { results, firstRecordId }; @@ -965,127 +975,151 @@ async function runMcpChecks({ origin, token, connectionId, stream, searchQuery, let firstRecordId = null; const call = (name, args) => mcpPost(origin, token, mcpToolCallMessage(name, args, id++), timeoutMs); - await pushChecked(results, 'MCP', 'initialize', async () => { + await pushChecked(results, "MCP", "initialize", async () => { const resp = await mcpPost(origin, token, mcpInitializeMessage(id++), timeoutMs); - if (resp.status >= 200 && resp.status < 300 && !resp.rpc?.error) return ok('MCP', 'initialize', 'initialized'); - return fail('MCP', 'initialize', `HTTP ${resp.status}: ${resp.rpc?.error?.message ?? resp.text}`); + if (resp.status >= 200 && resp.status < 300 && !resp.rpc?.error) return ok("MCP", "initialize", "initialized"); + return fail("MCP", "initialize", `HTTP ${resp.status}: ${resp.rpc?.error?.message ?? resp.text}`); }); let toolNames = []; - await pushChecked(results, 'MCP', 'tools.list', async () => { + await pushChecked(results, "MCP", "tools.list", async () => { const resp = await mcpPost(origin, token, mcpToolsListMessage(id++), timeoutMs); if (resp.status < 200 || resp.status >= 300 || resp.rpc?.error) { - return fail('MCP', 'tools.list', `HTTP ${resp.status}: ${resp.rpc?.error?.message ?? resp.text}`); + return fail("MCP", "tools.list", `HTTP ${resp.status}: ${resp.rpc?.error?.message ?? resp.text}`); } const tools = resp.rpc?.result?.tools ?? []; toolNames = tools.map((tool) => tool?.name).filter(Boolean); const verdict = classifyToolNames(toolNames); if (verdict.missingCore.length > 0) { - return fail('MCP', 'tools.list', `missing core tool(s): ${verdict.missingCore.join(', ')}`); + return fail("MCP", "tools.list", `missing core tool(s): ${verdict.missingCore.join(", ")}`); } if (verdict.forbiddenPresent.length > 0 || verdict.unexpectedTools.length > 0) { const extra = [...new Set([...verdict.forbiddenPresent, ...verdict.unexpectedTools])]; - return fail('MCP', 'tools.list', `${verdict.detail}; unexpected normal-surface tool(s): ${extra.join(', ')}`); + return fail("MCP", "tools.list", `${verdict.detail}; unexpected normal-surface tool(s): ${extra.join(", ")}`); } - const schemaTool = tools.find((tool) => tool?.name === 'schema'); + const schemaTool = tools.find((tool) => tool?.name === "schema"); const schemaProperties = schemaTool?.inputSchema?.properties; if (!schemaProperties?.connection_id) { - return fail('MCP', 'tools.list', 'schema tool does not expose connection_id in inputSchema'); + return fail("MCP", "tools.list", "schema tool does not expose connection_id in inputSchema"); } - return ok('MCP', 'tools.list', `${verdict.detail}; exact normal read surface present`); + return ok("MCP", "tools.list", `${verdict.detail}; exact normal read surface present`); }); - await pushChecked(results, 'MCP', 'schema', async () => { - const resp = await call('schema', {}); - const failure = requireMcpOk(resp, 'schema'); - return failure ?? ok('MCP', 'schema', 'schema returned'); + await pushChecked(results, "MCP", "schema", async () => { + const resp = await call("schema", {}); + const failure = requireMcpOk(resp, "schema"); + return failure ?? ok("MCP", "schema", "schema returned"); }); - await pushChecked(results, 'MCP', 'schema.compact', async () => { - const resp = await call('schema', { detail: 'compact' }); - const failure = requireMcpOk(resp, 'schema.compact'); + await pushChecked(results, "MCP", "schema.compact", async () => { + const resp = await call("schema", { detail: "compact" }); + const failure = requireMcpOk(resp, "schema.compact"); if (failure) return failure; const body = extractMcpToolData(resp.rpc); - return body?.detail === 'compact' - ? ok('MCP', 'schema.compact', 'compact schema returned') - : warn('MCP', 'schema.compact', `compact schema detail was ${body?.detail ?? ''}`); + return body?.detail === "compact" + ? ok("MCP", "schema.compact", "compact schema returned") + : warn("MCP", "schema.compact", `compact schema detail was ${body?.detail ?? ""}`); }); - await pushChecked(results, 'MCP', 'schema.scoped_full', async () => { - const resp = await call('schema', { stream, connection_id: connectionId, detail: 'full' }); - const failure = requireMcpOk(resp, 'schema.scoped_full'); + await pushChecked(results, "MCP", "schema.scoped_full", async () => { + const resp = await call("schema", { stream, connection_id: connectionId, detail: "full" }); + const failure = requireMcpOk(resp, "schema.scoped_full"); if (failure) return failure; - const bytes = Buffer.byteLength(JSON.stringify(resp.rpc?.result?.structuredContent ?? {}), 'utf8'); + const bytes = Buffer.byteLength(JSON.stringify(resp.rpc?.result?.structuredContent ?? {}), "utf8"); if (bytes > SCOPED_FULL_SCHEMA_BYTE_BUDGET) { return fail( - 'MCP', - 'schema.scoped_full', - `scoped full schema exceeded ${SCOPED_FULL_SCHEMA_BYTE_BUDGET} bytes (${bytes}); likely ignored stream/connection scope`, + "MCP", + "schema.scoped_full", + `scoped full schema exceeded ${SCOPED_FULL_SCHEMA_BYTE_BUDGET} bytes (${bytes}); likely ignored stream/connection scope` ); } - return ok('MCP', 'schema.scoped_full', `scoped full schema stayed bounded (${bytes} bytes)`); + return ok("MCP", "schema.scoped_full", `scoped full schema stayed bounded (${bytes} bytes)`); }); - await pushChecked(results, 'MCP', 'schema.source_identity', async () => { - const resp = await call('schema', { stream, connection_id: connectionId, detail: 'compact' }); - const failure = requireMcpOk(resp, 'schema.source_identity'); + await pushChecked(results, "MCP", "schema.source_identity", async () => { + const resp = await call("schema", { stream, connection_id: connectionId, detail: "compact" }); + const failure = requireMcpOk(resp, "schema.source_identity"); if (failure) return failure; const verdict = classifySourceIdentity(extractMcpToolData(resp.rpc), connectionId); - return result(verdict.status, 'MCP', 'schema.source_identity', verdict.detail, verdict.extra); + return result(verdict.status, "MCP", "schema.source_identity", verdict.detail, verdict.extra); }); - await pushChecked(results, 'MCP', 'query_records.basic', async () => { - const resp = await call('query_records', { stream, limit: 1, connection_id: connectionId }); - const failure = requireMcpOk(resp, 'query_records.basic'); + await pushChecked(results, "MCP", "query_records.basic", async () => { + const resp = await call("query_records", { stream, limit: 1, connection_id: connectionId }); + const failure = requireMcpOk(resp, "query_records.basic"); if (failure) return failure; const records = extractListData(extractMcpToolData(resp.rpc)); firstRecordId = extractRecordId(records[0]); - return ok('MCP', 'query_records.basic', `${records.length} record(s) returned`, { firstRecordId }); + return ok("MCP", "query_records.basic", `${records.length} record(s) returned`, { firstRecordId }); }); - await pushChecked(results, 'MCP', 'query_records.projection', async () => { - const resp = await call('query_records', { stream, limit: 1, connection_id: connectionId, fields: ['id'] }); - const failure = requireMcpOk(resp, 'query_records.projection'); + await pushChecked(results, "MCP", "query_records.projection", async () => { + const resp = await call("query_records", { stream, limit: 1, connection_id: connectionId, fields: ["id"] }); + const failure = requireMcpOk(resp, "query_records.projection"); if (failure) return failure; - const verdict = classifyStrictProjection(extractListData(extractMcpToolData(resp.rpc))[0], ['id']); - return result(verdict.status, 'MCP', 'query_records.projection', verdict.detail, verdict.extra); + const verdict = classifyStrictProjection(extractListData(extractMcpToolData(resp.rpc))[0], ["id"]); + return result(verdict.status, "MCP", "query_records.projection", verdict.detail, verdict.extra); }); - await pushChecked(results, 'MCP', 'query_records.omit_connection_id', async () => { - const resp = await call('query_records', { stream, limit: 1 }); + await pushChecked(results, "MCP", "query_records.omit_connection_id", async () => { + const resp = await call("query_records", { stream, limit: 1 }); const toolError = extractMcpToolError(resp.rpc); if (!toolError && resp.status >= 200 && resp.status < 300) { - return ok('MCP', 'query_records.omit_connection_id', 'request succeeded without connection_id; grant may be single-source'); + return ok( + "MCP", + "query_records.omit_connection_id", + "request succeeded without connection_id; grant may be single-source" + ); } - if (toolError?.code === 'ambiguous_connection') { - return ok('MCP', 'query_records.omit_connection_id', 'returned typed ambiguous_connection'); + if (toolError?.code === "ambiguous_connection") { + return ok("MCP", "query_records.omit_connection_id", "returned typed ambiguous_connection"); } - return fail('MCP', 'query_records.omit_connection_id', `expected success or ambiguous_connection; got ${toolError?.code ?? `HTTP ${resp.status}`}`); + return fail( + "MCP", + "query_records.omit_connection_id", + `expected success or ambiguous_connection; got ${toolError?.code ?? `HTTP ${resp.status}`}` + ); }); - await pushChecked(results, 'MCP', 'query_records.sort_count', async () => { - const resp = await call('query_records', { stream, limit: 1, connection_id: connectionId, sort: `-${dateField}`, count: 'exact' }); - const failure = requireMcpOk(resp, 'query_records.sort_count'); + await pushChecked(results, "MCP", "query_records.sort_count", async () => { + const resp = await call("query_records", { + stream, + limit: 1, + connection_id: connectionId, + sort: `-${dateField}`, + count: "exact", + }); + const failure = requireMcpOk(resp, "query_records.sort_count"); if (!failure) { const verdict = classifyPageHandles(extractMcpToolData(resp.rpc)); - return result(verdict.status, 'MCP', 'query_records.sort_count', `sort=-${dateField} and count=exact accepted; ${verdict.detail}`, verdict.extra); + return result( + verdict.status, + "MCP", + "query_records.sort_count", + `sort=-${dateField} and count=exact accepted; ${verdict.detail}`, + verdict.extra + ); } - return failure.detail.includes('unsupported_query') ? warn('MCP', 'query_records.sort_count', failure.detail) : failure; + return failure.detail.includes("unsupported_query") + ? warn("MCP", "query_records.sort_count", failure.detail) + : failure; }); - await pushChecked(results, 'MCP', 'query_records.filter_object', async () => { - const resp = await call('query_records', { + await pushChecked(results, "MCP", "query_records.filter_object", async () => { + const resp = await call("query_records", { stream, limit: 1, connection_id: connectionId, filter: { [dateField]: { gte: since } }, }); - const failure = requireMcpOk(resp, 'query_records.filter_object'); - return failure ? warn('MCP', 'query_records.filter_object', failure.detail) : ok('MCP', 'query_records.filter_object', 'typed filter object accepted'); + const failure = requireMcpOk(resp, "query_records.filter_object"); + return failure + ? warn("MCP", "query_records.filter_object", failure.detail) + : ok("MCP", "query_records.filter_object", "typed filter object accepted"); }); - await pushChecked(results, 'MCP', 'query_records.filter_legacy_literal', async () => { - const resp = await call('query_records', { + await pushChecked(results, "MCP", "query_records.filter_legacy_literal", async () => { + const resp = await call("query_records", { stream, limit: 1, connection_id: connectionId, @@ -1093,13 +1127,17 @@ async function runMcpChecks({ origin, token, connectionId, stream, searchQuery, }); const toolError = extractMcpToolError(resp.rpc); if (toolError) { - return ok('MCP', 'query_records.filter_legacy_literal', `legacy string filter rejected (${toolError.code})`); + return ok("MCP", "query_records.filter_legacy_literal", `legacy string filter rejected (${toolError.code})`); } - return warn('MCP', 'query_records.filter_legacy_literal', 'legacy string filter unexpectedly accepted; MCP filters should be typed objects'); + return warn( + "MCP", + "query_records.filter_legacy_literal", + "legacy string filter unexpectedly accepted; MCP filters should be typed objects" + ); }); - await pushChecked(results, 'MCP', 'query_records.filter_legacy_encoded', async () => { - const resp = await call('query_records', { + await pushChecked(results, "MCP", "query_records.filter_legacy_encoded", async () => { + const resp = await call("query_records", { stream, limit: 1, connection_id: connectionId, @@ -1107,390 +1145,502 @@ async function runMcpChecks({ origin, token, connectionId, stream, searchQuery, }); const toolError = extractMcpToolError(resp.rpc); if (toolError) { - return ok('MCP', 'query_records.filter_legacy_encoded', `encoded raw filter rejected (${toolError.code})`); + return ok("MCP", "query_records.filter_legacy_encoded", `encoded raw filter rejected (${toolError.code})`); } - return warn('MCP', 'query_records.filter_legacy_encoded', 'encoded raw filter unexpectedly accepted; MCP filters should be typed objects'); + return warn( + "MCP", + "query_records.filter_legacy_encoded", + "encoded raw filter unexpectedly accepted; MCP filters should be typed objects" + ); }); - await pushChecked(results, 'MCP', 'fetch', async () => { - if (!firstRecordId) return skip('MCP', 'fetch', 'no record id returned by basic query'); - const resp = await call('fetch', { id: `${stream}:${firstRecordId}`, connection_id: connectionId }); - const failure = requireMcpOk(resp, 'fetch'); - return failure ?? ok('MCP', 'fetch', `fetched ${stream}:${firstRecordId}`); + await pushChecked(results, "MCP", "fetch", async () => { + if (!firstRecordId) return skip("MCP", "fetch", "no record id returned by basic query"); + const resp = await call("fetch", { id: `${stream}:${firstRecordId}`, connection_id: connectionId }); + const failure = requireMcpOk(resp, "fetch"); + return failure ?? ok("MCP", "fetch", `fetched ${stream}:${firstRecordId}`); }); - await pushChecked(results, 'MCP', 'fetch.projection', async () => { - if (!firstRecordId) return skip('MCP', 'fetch.projection', 'no record id returned by basic query'); - const resp = await call('fetch', { id: `${stream}:${firstRecordId}`, connection_id: connectionId, fields: ['id'] }); - const failure = requireMcpOk(resp, 'fetch.projection'); + await pushChecked(results, "MCP", "fetch.projection", async () => { + if (!firstRecordId) return skip("MCP", "fetch.projection", "no record id returned by basic query"); + const resp = await call("fetch", { id: `${stream}:${firstRecordId}`, connection_id: connectionId, fields: ["id"] }); + const failure = requireMcpOk(resp, "fetch.projection"); if (failure) return failure; const doc = extractMcpToolData(resp.rpc); let projected = null; try { - projected = typeof doc?.text === 'string' ? JSON.parse(doc.text) : null; + projected = typeof doc?.text === "string" ? JSON.parse(doc.text) : null; } catch { projected = null; } - if (!projected) return warn('MCP', 'fetch.projection', 'fetch returned a document but text was not JSON-projectable'); - const verdict = classifyStrictProjection(projected?.data ?? projected, ['id']); - return result(verdict.status, 'MCP', 'fetch.projection', verdict.detail, verdict.extra); + if (!projected) + return warn("MCP", "fetch.projection", "fetch returned a document but text was not JSON-projectable"); + const verdict = classifyStrictProjection(projected?.data ?? projected, ["id"]); + return result(verdict.status, "MCP", "fetch.projection", verdict.detail, verdict.extra); }); - await pushChecked(results, 'MCP', 'search.lexical', async () => { - const resp = await call('search', { q: searchQuery, streams: [stream], limit: 1, mode: 'lexical', connection_id: connectionId }); - const failure = requireMcpOk(resp, 'search.lexical'); - return failure ?? ok('MCP', 'search.lexical', 'lexical search returned'); + await pushChecked(results, "MCP", "search.lexical", async () => { + const resp = await call("search", { + q: searchQuery, + streams: [stream], + limit: 1, + mode: "lexical", + connection_id: connectionId, + }); + const failure = requireMcpOk(resp, "search.lexical"); + return failure ?? ok("MCP", "search.lexical", "lexical search returned"); }); - await pushChecked(results, 'MCP', 'search.fan_in_limit_source_identity', async () => { + await pushChecked(results, "MCP", "search.fan_in_limit_source_identity", async () => { const limit = 3; - const resp = await call('search', { q: searchQuery, streams: [stream], limit, mode: 'lexical' }); - const failure = requireMcpOk(resp, 'search.fan_in_limit_source_identity'); + const resp = await call("search", { q: searchQuery, streams: [stream], limit, mode: "lexical" }); + const failure = requireMcpOk(resp, "search.fan_in_limit_source_identity"); if (failure) return failure; const verdict = classifySearchLimitAndSource(extractMcpToolStructuredContent(resp.rpc), limit); - return result(verdict.status, 'MCP', 'search.fan_in_limit_source_identity', verdict.detail, verdict.extra); + return result(verdict.status, "MCP", "search.fan_in_limit_source_identity", verdict.detail, verdict.extra); }); - await pushChecked(results, 'MCP', 'aggregate.count', async () => { - const resp = await call('aggregate', { stream, metric: 'count', connection_id: connectionId }); - const failure = requireMcpOk(resp, 'aggregate.count'); - return failure ?? ok('MCP', 'aggregate.count', 'count aggregate returned'); + await pushChecked(results, "MCP", "aggregate.count", async () => { + const resp = await call("aggregate", { stream, metric: "count", connection_id: connectionId }); + const failure = requireMcpOk(resp, "aggregate.count"); + return failure ?? ok("MCP", "aggregate.count", "count aggregate returned"); }); - await pushChecked(results, 'MCP', 'aggregate.group_by_time', async () => { - const resp = await call('aggregate', { + await pushChecked(results, "MCP", "aggregate.group_by_time", async () => { + const resp = await call("aggregate", { stream, - metric: 'count', + metric: "count", group_by_time: dateField, - granularity: 'day', + granularity: "day", limit: 7, connection_id: connectionId, }); - const failure = requireMcpOk(resp, 'aggregate.group_by_time'); - return failure ? warn('MCP', 'aggregate.group_by_time', failure.detail) : ok('MCP', 'aggregate.group_by_time', `${dateField}/day aggregate returned`); + const failure = requireMcpOk(resp, "aggregate.group_by_time"); + return failure + ? warn("MCP", "aggregate.group_by_time", failure.detail) + : ok("MCP", "aggregate.group_by_time", `${dateField}/day aggregate returned`); }); - await pushChecked(results, 'MCP', 'excluded_bearer', async () => { + await pushChecked(results, "MCP", "excluded_bearer", async () => { const resp = await mcpPost(origin, NON_GRANT_BEARER, mcpToolsListMessage(id++), timeoutMs); if (resp.status >= 200 && resp.status < 300 && !resp.rpc?.error) { - return fail('MCP', 'excluded_bearer', 'MCP served a non-grant bearer; scoped-grant requirement not enforced'); + return fail("MCP", "excluded_bearer", "MCP served a non-grant bearer; scoped-grant requirement not enforced"); } const verdict = classifyExcludedBearer(resp.status, resp.json); - return result(verdict.status, 'MCP', 'excluded_bearer', verdict.detail, verdict.extra); + return result(verdict.status, "MCP", "excluded_bearer", verdict.detail, verdict.extra); }); - results.push(warn('ChatGPT host', 'direct_recipient_routing', 'direct MCP cannot reproduce ChatGPT host resource invalidation; rerun the ChatGPT-host checklist after this passes')); + results.push( + warn( + "ChatGPT host", + "direct_recipient_routing", + "direct MCP cannot reproduce ChatGPT host resource invalidation; rerun the ChatGPT-host checklist after this passes" + ) + ); return { results, toolNames }; } async function runCliChecks({ origin, token, connectionId, stream, searchQuery, dateField, timeoutMs }) { const results = []; - const { spawnSync } = await import('node:child_process'); - const cliBin = join(process.cwd(), 'packages/cli/bin/pdpp.js'); + const { spawnSync } = await import("node:child_process"); + const cliBin = join(process.cwd(), "packages/cli/bin/pdpp.js"); let parent = null; let cacheRoot = null; let firstRecordId = null; async function writeCredentialCache(root, credentialToken) { const cacheFile = cliCredentialCacheFile(root, origin); - await mkdir(join(root, 'clients'), { recursive: true, mode: 0o700 }); + await mkdir(join(root, "clients"), { recursive: true, mode: 0o700 }); await writeFile( cacheFile, `${JSON.stringify( { provider_url: normalizeOrigin(origin), authorization_server: normalizeOrigin(origin), - scope: 'pdpp:read', - client: { client_id: 'read-surface-smoke' }, - credential: { access_token: credentialToken, token_type: 'Bearer' }, + scope: "pdpp:read", + client: { client_id: "read-surface-smoke" }, + credential: { access_token: credentialToken, token_type: "Bearer" }, created_at: new Date().toISOString(), }, null, - 2, + 2 )}\n`, - { mode: 0o600 }, + { mode: 0o600 } ); } async function ensureCache() { if (cacheRoot) return cacheRoot; - parent = await mkdtemp(join(tmpdir(), 'pdpp-read-surface-cli-')); - cacheRoot = join(parent, '.pdpp'); + parent = await mkdtemp(join(tmpdir(), "pdpp-read-surface-cli-")); + cacheRoot = join(parent, ".pdpp"); await writeCredentialCache(cacheRoot, token); return cacheRoot; } function spawnCli(args) { - return spawnSync('node', [cliBin, ...args], { - encoding: 'utf8', + return spawnSync("node", [cliBin, ...args], { + encoding: "utf8", maxBuffer: 1024 * 1024, timeout: timeoutMs, }); } try { - await pushChecked(results, 'CLI', 'help', async () => { - const child = spawnCli(['--help']); - if (child.status !== 0) return fail('CLI', 'help', `pdpp --help exited ${child.status}: ${child.stderr || child.stdout}`); - const verdict = classifyCliHelp(child.stdout); - if (!verdict.hasCliHelp) return fail('CLI', 'help', 'help output did not identify the PDPP CLI'); - return ok('CLI', 'help', 'pdpp --help returned'); - }); + await pushChecked(results, "CLI", "help", async () => { + const child = spawnCli(["--help"]); + if (child.status !== 0) + return fail("CLI", "help", `pdpp --help exited ${child.status}: ${child.stderr || child.stdout}`); + const verdict = classifyCliHelp(child.stdout); + if (!verdict.hasCliHelp) return fail("CLI", "help", "help output did not identify the PDPP CLI"); + return ok("CLI", "help", "pdpp --help returned"); + }); - await pushChecked(results, 'CLI', 'token_cache', async () => { - const root = await ensureCache(); - const child = spawnCli(['token', normalizeOrigin(origin), '--cache-root', root]); - if (child.status !== 0) return fail('CLI', 'token_cache', `pdpp token exited ${child.status}: ${child.stderr || child.stdout}`); - if (child.stdout.trim() !== token) return fail('CLI', 'token_cache', 'pdpp token did not return the cached bearer'); - return ok('CLI', 'token_cache', 'stored credential can be read by pdpp token'); - }); + await pushChecked(results, "CLI", "token_cache", async () => { + const root = await ensureCache(); + const child = spawnCli(["token", normalizeOrigin(origin), "--cache-root", root]); + if (child.status !== 0) + return fail("CLI", "token_cache", `pdpp token exited ${child.status}: ${child.stderr || child.stdout}`); + if (child.stdout.trim() !== token) + return fail("CLI", "token_cache", "pdpp token did not return the cached bearer"); + return ok("CLI", "token_cache", "stored credential can be read by pdpp token"); + }); - await pushChecked(results, 'CLI', 'grant_scoped_read_commands', async () => { - const child = spawnCli(['--help']); - if (child.status !== 0) return fail('CLI', 'grant_scoped_read_commands', `pdpp --help exited ${child.status}`); - const verdict = classifyCliHelp(child.stdout); - return verdict.hasGrantScopedReadCommands - ? ok('CLI', 'grant_scoped_read_commands', 'grant-scoped read commands are advertised') - : warn('CLI', 'grant_scoped_read_commands', 'current pdpp CLI exposes connect/token but not query_records/search/aggregate/fetch read commands'); - }); + await pushChecked(results, "CLI", "grant_scoped_read_commands", async () => { + const child = spawnCli(["--help"]); + if (child.status !== 0) return fail("CLI", "grant_scoped_read_commands", `pdpp --help exited ${child.status}`); + const verdict = classifyCliHelp(child.stdout); + return verdict.hasGrantScopedReadCommands + ? ok("CLI", "grant_scoped_read_commands", "grant-scoped read commands are advertised") + : warn( + "CLI", + "grant_scoped_read_commands", + "current pdpp CLI exposes connect/token but not query_records/search/aggregate/fetch read commands" + ); + }); - await pushChecked(results, 'CLI', 'schema', async () => { - const root = await ensureCache(); - const child = spawnCli(['read', 'schema', normalizeOrigin(origin), '--cache-root', root, '--format', 'json']); - if (child.status !== 0) return fail('CLI', 'schema', `pdpp read schema exited ${child.status}: ${child.stderr || child.stdout}`); - const parsed = JSON.parse(child.stdout); - return parsed ? ok('CLI', 'schema', 'schema returned through cached grant') : fail('CLI', 'schema', 'empty schema output'); - }); + await pushChecked(results, "CLI", "schema", async () => { + const root = await ensureCache(); + const child = spawnCli(["read", "schema", normalizeOrigin(origin), "--cache-root", root, "--format", "json"]); + if (child.status !== 0) + return fail("CLI", "schema", `pdpp read schema exited ${child.status}: ${child.stderr || child.stdout}`); + const parsed = JSON.parse(child.stdout); + return parsed + ? ok("CLI", "schema", "schema returned through cached grant") + : fail("CLI", "schema", "empty schema output"); + }); - await pushChecked(results, 'CLI', 'schema.compact', async () => { - const root = await ensureCache(); - const child = spawnCli(['read', 'schema', normalizeOrigin(origin), '--view', 'compact', '--cache-root', root, '--format', 'json']); - if (child.status !== 0) return fail('CLI', 'schema.compact', `pdpp read schema compact exited ${child.status}: ${child.stderr || child.stdout}`); - const body = JSON.parse(child.stdout); - return body?.detail === 'compact' - ? ok('CLI', 'schema.compact', 'compact schema returned through cached grant') - : warn('CLI', 'schema.compact', `compact schema detail was ${body?.detail ?? ''}`); - }); + await pushChecked(results, "CLI", "schema.compact", async () => { + const root = await ensureCache(); + const child = spawnCli([ + "read", + "schema", + normalizeOrigin(origin), + "--view", + "compact", + "--cache-root", + root, + "--format", + "json", + ]); + if (child.status !== 0) + return fail( + "CLI", + "schema.compact", + `pdpp read schema compact exited ${child.status}: ${child.stderr || child.stdout}` + ); + const body = JSON.parse(child.stdout); + return body?.detail === "compact" + ? ok("CLI", "schema.compact", "compact schema returned through cached grant") + : warn("CLI", "schema.compact", `compact schema detail was ${body?.detail ?? ""}`); + }); - await pushChecked(results, 'CLI', 'schema.scoped', async () => { - const root = await ensureCache(); - const child = spawnCli([ - 'read', - 'schema', - normalizeOrigin(origin), - '--view', - 'compact', - '--stream', - stream, - '--connection-id', - connectionId, - '--cache-root', - root, - '--format', - 'json', - ]); - if (child.status !== 0) return fail('CLI', 'schema.scoped', `pdpp read schema scoped exited ${child.status}: ${child.stderr || child.stdout}`); - const verdict = classifyScopedSchema(JSON.parse(child.stdout), stream, connectionId); - return result(verdict.status, 'CLI', 'schema.scoped', verdict.detail, verdict.extra); - }); + await pushChecked(results, "CLI", "schema.scoped", async () => { + const root = await ensureCache(); + const child = spawnCli([ + "read", + "schema", + normalizeOrigin(origin), + "--view", + "compact", + "--stream", + stream, + "--connection-id", + connectionId, + "--cache-root", + root, + "--format", + "json", + ]); + if (child.status !== 0) + return fail( + "CLI", + "schema.scoped", + `pdpp read schema scoped exited ${child.status}: ${child.stderr || child.stdout}` + ); + const verdict = classifyScopedSchema(JSON.parse(child.stdout), stream, connectionId); + return result(verdict.status, "CLI", "schema.scoped", verdict.detail, verdict.extra); + }); - await pushChecked(results, 'CLI', 'schema.source_identity', async () => { - const root = await ensureCache(); - const child = spawnCli([ - 'read', - 'schema', - normalizeOrigin(origin), - '--view', - 'compact', - '--stream', - stream, - '--connection-id', - connectionId, - '--cache-root', - root, - '--format', - 'json', - ]); - if (child.status !== 0) return fail('CLI', 'schema.source_identity', `pdpp read schema scoped exited ${child.status}: ${child.stderr || child.stdout}`); - const verdict = classifySourceIdentity(JSON.parse(child.stdout), connectionId); - return result(verdict.status, 'CLI', 'schema.source_identity', verdict.detail, verdict.extra); - }); + await pushChecked(results, "CLI", "schema.source_identity", async () => { + const root = await ensureCache(); + const child = spawnCli([ + "read", + "schema", + normalizeOrigin(origin), + "--view", + "compact", + "--stream", + stream, + "--connection-id", + connectionId, + "--cache-root", + root, + "--format", + "json", + ]); + if (child.status !== 0) + return fail( + "CLI", + "schema.source_identity", + `pdpp read schema scoped exited ${child.status}: ${child.stderr || child.stdout}` + ); + const verdict = classifySourceIdentity(JSON.parse(child.stdout), connectionId); + return result(verdict.status, "CLI", "schema.source_identity", verdict.detail, verdict.extra); + }); - await pushChecked(results, 'CLI', 'query_records.basic', async () => { - const root = await ensureCache(); - const child = spawnCli([ - 'read', - 'query-records', - normalizeOrigin(origin), - stream, - '--connection-id', - connectionId, - '--limit', - '1', - '--sort', - `-${dateField}`, - '--cache-root', - root, - '--format', - 'json', - ]); - if (child.status !== 0) return fail('CLI', 'query_records.basic', `pdpp read query-records exited ${child.status}: ${child.stderr || child.stdout}`); - const records = extractListData(JSON.parse(child.stdout)); - firstRecordId = extractRecordId(records[0]); - return ok('CLI', 'query_records.basic', `${records.length} record(s) returned through cached grant`); - }); + await pushChecked(results, "CLI", "query_records.basic", async () => { + const root = await ensureCache(); + const child = spawnCli([ + "read", + "query-records", + normalizeOrigin(origin), + stream, + "--connection-id", + connectionId, + "--limit", + "1", + "--sort", + `-${dateField}`, + "--cache-root", + root, + "--format", + "json", + ]); + if (child.status !== 0) + return fail( + "CLI", + "query_records.basic", + `pdpp read query-records exited ${child.status}: ${child.stderr || child.stdout}` + ); + const records = extractListData(JSON.parse(child.stdout)); + firstRecordId = extractRecordId(records[0]); + return ok("CLI", "query_records.basic", `${records.length} record(s) returned through cached grant`); + }); - await pushChecked(results, 'CLI', 'query_records.projection', async () => { - const root = await ensureCache(); - const child = spawnCli([ - 'read', - 'query-records', - normalizeOrigin(origin), - stream, - '--connection-id', - connectionId, - '--limit', - '1', - '--fields', - 'id', - '--cache-root', - root, - '--format', - 'json', - ]); - if (child.status !== 0) return fail('CLI', 'query_records.projection', `pdpp read query-records projection exited ${child.status}: ${child.stderr || child.stdout}`); - const verdict = classifyStrictProjection(extractListData(JSON.parse(child.stdout))[0], ['id']); - return result(verdict.status, 'CLI', 'query_records.projection', verdict.detail, verdict.extra); - }); + await pushChecked(results, "CLI", "query_records.projection", async () => { + const root = await ensureCache(); + const child = spawnCli([ + "read", + "query-records", + normalizeOrigin(origin), + stream, + "--connection-id", + connectionId, + "--limit", + "1", + "--fields", + "id", + "--cache-root", + root, + "--format", + "json", + ]); + if (child.status !== 0) + return fail( + "CLI", + "query_records.projection", + `pdpp read query-records projection exited ${child.status}: ${child.stderr || child.stdout}` + ); + const verdict = classifyStrictProjection(extractListData(JSON.parse(child.stdout))[0], ["id"]); + return result(verdict.status, "CLI", "query_records.projection", verdict.detail, verdict.extra); + }); - await pushChecked(results, 'CLI', 'query_records.omit_connection_id', async () => { - const root = await ensureCache(); - const child = spawnCli([ - 'read', - 'query-records', - normalizeOrigin(origin), - stream, - '--limit', - '1', - '--cache-root', - root, - '--format', - 'json', - ]); - if (child.status === 0) { - return ok('CLI', 'query_records.omit_connection_id', 'request succeeded without connection_id; grant may be single-source'); - } - const combined = `${child.stderr || ''}\n${child.stdout || ''}`; - return /ambiguous_connection|connection_id/i.test(combined) - ? ok('CLI', 'query_records.omit_connection_id', 'returned typed ambiguity guidance') - : fail('CLI', 'query_records.omit_connection_id', `expected success or ambiguous_connection; got exit ${child.status}: ${combined.trim()}`); - }); + await pushChecked(results, "CLI", "query_records.omit_connection_id", async () => { + const root = await ensureCache(); + const child = spawnCli([ + "read", + "query-records", + normalizeOrigin(origin), + stream, + "--limit", + "1", + "--cache-root", + root, + "--format", + "json", + ]); + if (child.status === 0) { + return ok( + "CLI", + "query_records.omit_connection_id", + "request succeeded without connection_id; grant may be single-source" + ); + } + const combined = `${child.stderr || ""}\n${child.stdout || ""}`; + return /ambiguous_connection|connection_id/i.test(combined) + ? ok("CLI", "query_records.omit_connection_id", "returned typed ambiguity guidance") + : fail( + "CLI", + "query_records.omit_connection_id", + `expected success or ambiguous_connection; got exit ${child.status}: ${combined.trim()}` + ); + }); - await pushChecked(results, 'CLI', 'query_records.count', async () => { - const root = await ensureCache(); - const child = spawnCli([ - 'read', - 'query-records', - normalizeOrigin(origin), - stream, - '--connection-id', - connectionId, - '--limit', - '1', - '--count', - 'exact', - '--cache-root', - root, - '--format', - 'json', - ]); - if (child.status !== 0) return fail('CLI', 'query_records.count', `pdpp read query-records count exited ${child.status}: ${child.stderr || child.stdout}`); - const verdict = classifyPageHandles(JSON.parse(child.stdout)); - return result(verdict.status, 'CLI', 'query_records.count', `count=exact accepted; ${verdict.detail}`, verdict.extra); - }); + await pushChecked(results, "CLI", "query_records.count", async () => { + const root = await ensureCache(); + const child = spawnCli([ + "read", + "query-records", + normalizeOrigin(origin), + stream, + "--connection-id", + connectionId, + "--limit", + "1", + "--count", + "exact", + "--cache-root", + root, + "--format", + "json", + ]); + if (child.status !== 0) + return fail( + "CLI", + "query_records.count", + `pdpp read query-records count exited ${child.status}: ${child.stderr || child.stdout}` + ); + const verdict = classifyPageHandles(JSON.parse(child.stdout)); + return result( + verdict.status, + "CLI", + "query_records.count", + `count=exact accepted; ${verdict.detail}`, + verdict.extra + ); + }); - await pushChecked(results, 'CLI', 'fetch.projection', async () => { - if (!firstRecordId) return skip('CLI', 'fetch.projection', 'no record id returned by basic query'); - const root = await ensureCache(); - const child = spawnCli([ - 'read', - 'fetch', - normalizeOrigin(origin), - stream, - firstRecordId, - '--connection-id', - connectionId, - '--fields', - 'id', - '--cache-root', - root, - '--format', - 'json', - ]); - if (child.status !== 0) return fail('CLI', 'fetch.projection', `pdpp read fetch projection exited ${child.status}: ${child.stderr || child.stdout}`); - const parsed = JSON.parse(child.stdout); - const verdict = classifyStrictProjection(parsed?.data ?? parsed, ['id']); - return result(verdict.status, 'CLI', 'fetch.projection', verdict.detail, verdict.extra); - }); + await pushChecked(results, "CLI", "fetch.projection", async () => { + if (!firstRecordId) return skip("CLI", "fetch.projection", "no record id returned by basic query"); + const root = await ensureCache(); + const child = spawnCli([ + "read", + "fetch", + normalizeOrigin(origin), + stream, + firstRecordId, + "--connection-id", + connectionId, + "--fields", + "id", + "--cache-root", + root, + "--format", + "json", + ]); + if (child.status !== 0) + return fail( + "CLI", + "fetch.projection", + `pdpp read fetch projection exited ${child.status}: ${child.stderr || child.stdout}` + ); + const parsed = JSON.parse(child.stdout); + const verdict = classifyStrictProjection(parsed?.data ?? parsed, ["id"]); + return result(verdict.status, "CLI", "fetch.projection", verdict.detail, verdict.extra); + }); - await pushChecked(results, 'CLI', 'search.fan_in_limit_source_identity', async () => { - const root = await ensureCache(); - const limit = 3; - const child = spawnCli([ - 'read', - 'search', - normalizeOrigin(origin), - searchQuery, - '--streams', - stream, - '--limit', - String(limit), - '--cache-root', - root, - '--format', - 'json', - ]); - if (child.status !== 0) return fail('CLI', 'search.fan_in_limit_source_identity', `pdpp read search exited ${child.status}: ${child.stderr || child.stdout}`); - const verdict = classifySearchLimitAndSource(JSON.parse(child.stdout), limit); - return result(verdict.status, 'CLI', 'search.fan_in_limit_source_identity', verdict.detail, verdict.extra); - }); + await pushChecked(results, "CLI", "search.fan_in_limit_source_identity", async () => { + const root = await ensureCache(); + const limit = 3; + const child = spawnCli([ + "read", + "search", + normalizeOrigin(origin), + searchQuery, + "--streams", + stream, + "--limit", + String(limit), + "--cache-root", + root, + "--format", + "json", + ]); + if (child.status !== 0) + return fail( + "CLI", + "search.fan_in_limit_source_identity", + `pdpp read search exited ${child.status}: ${child.stderr || child.stdout}` + ); + const verdict = classifySearchLimitAndSource(JSON.parse(child.stdout), limit); + return result(verdict.status, "CLI", "search.fan_in_limit_source_identity", verdict.detail, verdict.extra); + }); - await pushChecked(results, 'CLI', 'aggregate.count', async () => { - const root = await ensureCache(); - const child = spawnCli([ - 'read', - 'aggregate', - normalizeOrigin(origin), - stream, - '--metric', - 'count', - '--connection-id', - connectionId, - '--cache-root', - root, - '--format', - 'json', - ]); - if (child.status !== 0) return fail('CLI', 'aggregate.count', `pdpp read aggregate exited ${child.status}: ${child.stderr || child.stdout}`); - const parsed = JSON.parse(child.stdout); - return parsed ? ok('CLI', 'aggregate.count', 'count aggregate returned through cached grant') : fail('CLI', 'aggregate.count', 'empty aggregate output'); - }); + await pushChecked(results, "CLI", "aggregate.count", async () => { + const root = await ensureCache(); + const child = spawnCli([ + "read", + "aggregate", + normalizeOrigin(origin), + stream, + "--metric", + "count", + "--connection-id", + connectionId, + "--cache-root", + root, + "--format", + "json", + ]); + if (child.status !== 0) + return fail( + "CLI", + "aggregate.count", + `pdpp read aggregate exited ${child.status}: ${child.stderr || child.stdout}` + ); + const parsed = JSON.parse(child.stdout); + return parsed + ? ok("CLI", "aggregate.count", "count aggregate returned through cached grant") + : fail("CLI", "aggregate.count", "empty aggregate output"); + }); - await pushChecked(results, 'CLI', 'excluded_bearer', async () => { - let junkParent = null; - try { - junkParent = await mkdtemp(join(tmpdir(), 'pdpp-read-surface-cli-junk-')); - const junkRoot = join(junkParent, '.pdpp'); - await writeCredentialCache(junkRoot, NON_GRANT_BEARER); - const child = spawnCli(['read', 'schema', normalizeOrigin(origin), '--cache-root', junkRoot, '--format', 'json']); - if (child.status === 0) { - return fail('CLI', 'excluded_bearer', 'CLI served a non-grant bearer; scoped-grant requirement not enforced'); + await pushChecked(results, "CLI", "excluded_bearer", async () => { + let junkParent = null; + try { + junkParent = await mkdtemp(join(tmpdir(), "pdpp-read-surface-cli-junk-")); + const junkRoot = join(junkParent, ".pdpp"); + await writeCredentialCache(junkRoot, NON_GRANT_BEARER); + const child = spawnCli([ + "read", + "schema", + normalizeOrigin(origin), + "--cache-root", + junkRoot, + "--format", + "json", + ]); + if (child.status === 0) { + return fail("CLI", "excluded_bearer", "CLI served a non-grant bearer; scoped-grant requirement not enforced"); + } + return ok( + "CLI", + "excluded_bearer", + `non-grant bearer rejected (exit ${child.status}); reads require the scoped grant` + ); + } finally { + if (junkParent) await rm(junkParent, { recursive: true, force: true }); } - return ok('CLI', 'excluded_bearer', `non-grant bearer rejected (exit ${child.status}); reads require the scoped grant`); - } finally { - if (junkParent) await rm(junkParent, { recursive: true, force: true }); - } - }); + }); } finally { if (parent) await rm(parent, { recursive: true, force: true }); } @@ -1525,20 +1675,22 @@ function printTextReport(origin, report) { process.stdout.write(` ${marker} ${entry.surface}.${entry.name}: ${entry.detail}\n`); } if (report.parityMatrix) { - process.stdout.write('\nParity matrix (shared read semantics):\n'); - const header = ['row'.padEnd(20), ...PARITY_TRANSPORTS.map((transport) => transport.padEnd(7))].join(' '); + process.stdout.write("\nParity matrix (shared read semantics):\n"); + const header = ["row".padEnd(20), ...PARITY_TRANSPORTS.map((transport) => transport.padEnd(7))].join(" "); process.stdout.write(` ${header}\n`); for (const { row, transports, diverged } of report.parityMatrix.rows) { - const cells = PARITY_TRANSPORTS.map((transport) => String(transports[transport]).padEnd(7)).join(' '); - const flag = diverged ? ' <- DIVERGED' : ''; + const cells = PARITY_TRANSPORTS.map((transport) => String(transports[transport]).padEnd(7)).join(" "); + const flag = diverged ? " <- DIVERGED" : ""; process.stdout.write(` ${row.padEnd(20)} ${cells}${flag}\n`); } if (report.parityMatrix.diverged) { - process.stdout.write(' parity FAILED: a shared behavior passed on one adapter and failed on another\n'); + process.stdout.write(" parity FAILED: a shared behavior passed on one adapter and failed on another\n"); } } const { counts } = report.summary; - process.stdout.write(`\nSummary: ${counts.pass} pass, ${counts.warn} warn, ${counts.skip} skip, ${counts.fail} fail\n`); + process.stdout.write( + `\nSummary: ${counts.pass} pass, ${counts.warn} warn, ${counts.skip} skip, ${counts.fail} fail\n` + ); } const USAGE = `Usage: node scripts/read-surface-smoke.mjs --origin --connection-id [options] @@ -1585,10 +1737,10 @@ async function main(argv) { let token = opts.token ?? process.env.PDPP_READ_SURFACE_TOKEN; if (!token) { const ownerPassword = opts.ownerPassword ?? process.env.PDPP_OWNER_PASSWORD; - const ownerSubject = opts.ownerSubject ?? process.env.PDPP_OWNER_SUBJECT_ID ?? 'owner_local'; + const ownerSubject = opts.ownerSubject ?? process.env.PDPP_OWNER_SUBJECT_ID ?? "owner_local"; if (!ownerPassword || !opts.connectorId) { process.stderr.write( - `--token/PDPP_READ_SURFACE_TOKEN or (--owner-password/PDPP_OWNER_PASSWORD plus --connector-id) is required.\n\n${USAGE}\n`, + `--token/PDPP_READ_SURFACE_TOKEN or (--owner-password/PDPP_OWNER_PASSWORD plus --connector-id) is required.\n\n${USAGE}\n` ); process.exit(2); } diff --git a/scripts/spec-check.mjs b/scripts/spec-check.mjs index 907e2a589..9c98fcbe3 100644 --- a/scripts/spec-check.mjs +++ b/scripts/spec-check.mjs @@ -178,10 +178,14 @@ function checkPair(file) { errors.push(`${file}: public-site copy must start with a Status/Date Callout`); } if (expectedMeta.status && actualMeta.status !== expectedMeta.status) { - errors.push(`${file}: site Status mismatch (root=${JSON.stringify(expectedMeta.status)} site=${JSON.stringify(actualMeta.status)})`); + errors.push( + `${file}: site Status mismatch (root=${JSON.stringify(expectedMeta.status)} site=${JSON.stringify(actualMeta.status)})` + ); } if (expectedMeta.date && actualMeta.date !== expectedMeta.date) { - errors.push(`${file}: site Date mismatch (root=${JSON.stringify(expectedMeta.date)} site=${JSON.stringify(actualMeta.date)})`); + errors.push( + `${file}: site Date mismatch (root=${JSON.stringify(expectedMeta.date)} site=${JSON.stringify(actualMeta.date)})` + ); } const expected = normalizeBody(stripTitleAndRootStatus(rootText)); diff --git a/scripts/stream-debug-summary.mjs b/scripts/stream-debug-summary.mjs index 51af2254b..4368ccbe9 100644 --- a/scripts/stream-debug-summary.mjs +++ b/scripts/stream-debug-summary.mjs @@ -134,7 +134,11 @@ function summarize(events) { } else if (type.startsWith("viewport.post.")) { increment(viewportPosts, `${type}:${data.status ?? data.ok ?? "unknown"}`); lastViewport = data.viewport ?? lastViewport; - } else if (type.startsWith("viewport.") || type.startsWith("neko.layout") || type.startsWith("neko.client.layout")) { + } else if ( + type.startsWith("viewport.") || + type.startsWith("neko.layout") || + type.startsWith("neko.client.layout") + ) { lastViewport = data.viewport ?? data.layout ?? lastViewport; } diff --git a/scripts/stream-evidence-inventory.mjs b/scripts/stream-evidence-inventory.mjs index 28d2abf24..b44bcd943 100644 --- a/scripts/stream-evidence-inventory.mjs +++ b/scripts/stream-evidence-inventory.mjs @@ -129,9 +129,7 @@ export function renderInventory(manifests) { lines.push("## Summary"); lines.push(""); - lines.push( - `${missingStrategyCount} stream(s) missing a coverage_strategy or freshness_strategy declaration (debt).` - ); + lines.push(`${missingStrategyCount} stream(s) missing a coverage_strategy or freshness_strategy declaration (debt).`); lines.push( `${requiredAcceptedAbsenceCount} stream(s) combine required=true/default-required with an accepted-absence coverage_policy (debt).` ); diff --git a/scripts/stream-health-audit/live.mjs b/scripts/stream-health-audit/live.mjs index 711e4918a..670b178eb 100644 --- a/scripts/stream-health-audit/live.mjs +++ b/scripts/stream-health-audit/live.mjs @@ -113,7 +113,12 @@ function asArrayList(raw) { */ export async function runLiveStreamHealthAudit({ origin, env = process.env, fetchImpl = fetch }) { const base = origin.replace(/\/+$/, ""); - const { header, mode, supported, error: authError } = await resolveOwnerAuthForStreamHealth({ + const { + header, + mode, + supported, + error: authError, + } = await resolveOwnerAuthForStreamHealth({ base, env, fetchImpl, diff --git a/scripts/stream-mint-probe.mjs b/scripts/stream-mint-probe.mjs index 421f42419..8d92db83c 100644 --- a/scripts/stream-mint-probe.mjs +++ b/scripts/stream-mint-probe.mjs @@ -29,25 +29,25 @@ * without a live run. */ -import { execFileSync } from 'node:child_process'; -import { appendFileSync, mkdirSync } from 'node:fs'; -import { dirname, join } from 'node:path'; -import { fileURLToPath } from 'node:url'; +import { execFileSync } from "node:child_process"; +import { appendFileSync, mkdirSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; const __dirname = dirname(fileURLToPath(import.meta.url)); -const REPO_ROOT = join(__dirname, '..'); -const DEBUG_DIR = join(REPO_ROOT, 'tmp', 'workstreams', 'stream-debug'); -const NOW = new Date().toISOString().replace(/[:.]/g, '-'); +const REPO_ROOT = join(__dirname, ".."); +const DEBUG_DIR = join(REPO_ROOT, "tmp", "workstreams", "stream-debug"); +const NOW = new Date().toISOString().replace(/[:.]/g, "-"); const FIXTURE_PATH = join(DEBUG_DIR, `${NOW}-stream-mint-probe.jsonl`); mkdirSync(DEBUG_DIR, { recursive: true }); const TEST_FILES = [ - 'reference-implementation/test/run-interaction-stream-routes.test.js', - 'reference-implementation/test/run-interaction-stream-neko-adapter.test.js', - 'reference-implementation/test/run-interaction-stream-playground.test.js', - 'reference-implementation/test/neko-surface-allocator-server.test.js', - 'reference-implementation/test/neko-surface-allocator.test.js', + "reference-implementation/test/run-interaction-stream-routes.test.js", + "reference-implementation/test/run-interaction-stream-neko-adapter.test.js", + "reference-implementation/test/run-interaction-stream-playground.test.js", + "reference-implementation/test/neko-surface-allocator-server.test.js", + "reference-implementation/test/neko-surface-allocator.test.js", ]; console.log(`[stream-mint-probe] running ${TEST_FILES.length} test files`); @@ -57,37 +57,42 @@ let exitCode = 0; const results = []; for (const file of TEST_FILES) { - const label = file.split('/').pop(); + const label = file.split("/").pop(); const startMs = Date.now(); - let output = ''; + let output = ""; let passed = 0; let failed = 0; - let status = 'pass'; + let status = "pass"; try { - output = execFileSync(process.execPath, ['--test', file], { + output = execFileSync(process.execPath, ["--test", file], { cwd: REPO_ROOT, - encoding: 'utf8', - stdio: ['ignore', 'pipe', 'pipe'], + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], }); // Parse node:test summary lines for counts. - for (const line of output.split('\n')) { + for (const line of output.split("\n")) { const passMatch = line.match(/^\u2139 pass (\d+)/); const failMatch = line.match(/^\u2139 fail (\d+)/); if (passMatch) passed = parseInt(passMatch[1], 10); if (failMatch) failed = parseInt(failMatch[1], 10); } - if (failed > 0) { status = 'fail'; exitCode = 1; } - console.log(` ${status === 'pass' ? 'PASS' : 'FAIL'} ${label} (${passed}p/${failed}f in ${Date.now() - startMs}ms)`); + if (failed > 0) { + status = "fail"; + exitCode = 1; + } + console.log( + ` ${status === "pass" ? "PASS" : "FAIL"} ${label} (${passed}p/${failed}f in ${Date.now() - startMs}ms)` + ); } catch (err) { - status = 'fail'; + status = "fail"; exitCode = 1; output = err.stdout || err.message; - console.error(` FAIL ${label} ERROR: ${err.message.split('\n')[0]}`); + console.error(` FAIL ${label} ERROR: ${err.message.split("\n")[0]}`); } const record = { - probe: 'stream-mint', + probe: "stream-mint", file: label, status, passed, @@ -96,13 +101,13 @@ for (const file of TEST_FILES) { capturedAt: new Date().toISOString(), // Trim verbose pino log lines from output before persisting output: output - .split('\n') + .split("\n") .filter((l) => !l.match(/^\[ntfy\]|\bINFO\b|\bDEBUG\b/)) - .join('\n') + .join("\n") .trim(), }; results.push(record); - appendFileSync(FIXTURE_PATH, JSON.stringify(record) + '\n', 'utf8'); + appendFileSync(FIXTURE_PATH, JSON.stringify(record) + "\n", "utf8"); } const total = results.reduce((s, r) => s + r.passed + r.failed, 0); diff --git a/scripts/stream-playground-parity-calibration.mjs b/scripts/stream-playground-parity-calibration.mjs index ba398de2e..76f3ff83a 100644 --- a/scripts/stream-playground-parity-calibration.mjs +++ b/scripts/stream-playground-parity-calibration.mjs @@ -2,35 +2,35 @@ // Copyright The PDP-Connect Contributors // SPDX-License-Identifier: Apache-2.0 -import process from 'node:process'; -import { spawn } from 'node:child_process'; -import { dirname, resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; +import process from "node:process"; +import { spawn } from "node:child_process"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; const referenceUrl = process.argv[2] || process.env.PDPP_STREAM_PARITY_REFERENCE_URL; -const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); if (!referenceUrl) { process.stderr.write( - 'usage: stream-playground-parity-calibration.mjs \n' + - ' (or set PDPP_STREAM_PARITY_REFERENCE_URL) — no default reference URL is provided.\n', + "usage: stream-playground-parity-calibration.mjs \n" + + " (or set PDPP_STREAM_PARITY_REFERENCE_URL) — no default reference URL is provided.\n" ); process.exit(1); } function report(result) { - process.stdout.write(`${JSON.stringify({ authority: 'informational-calibration-only', ...result })}\n`); + process.stdout.write(`${JSON.stringify({ authority: "informational-calibration-only", ...result })}\n`); } function runLocalOracle() { return new Promise((resolveLocal) => { - const child = spawn('pnpm', ['stream:parity:oracle'], { + const child = spawn("pnpm", ["stream:parity:oracle"], { cwd: repositoryRoot, - stdio: 'ignore', + stdio: "ignore", }); - child.once('error', (error) => resolveLocal({ status: 'unavailable', reason: error.message })); - child.once('exit', (code, signal) => { - resolveLocal(code === 0 ? { status: 'passed' } : { status: 'failed', exitCode: code, signal }); + child.once("error", (error) => resolveLocal({ status: "unavailable", reason: error.message })); + child.once("exit", (code, signal) => { + resolveLocal(code === 0 ? { status: "passed" } : { status: "failed", exitCode: code, signal }); }); }); } @@ -40,11 +40,14 @@ async function probe() { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 10_000); try { - const response = await fetch(referenceUrl, { redirect: 'follow', signal: controller.signal }); - report({ external: { referenceUrl, status: response.ok ? 'reachable' : 'http-error', statusCode: response.status }, localOracle }); + const response = await fetch(referenceUrl, { redirect: "follow", signal: controller.signal }); + report({ + external: { referenceUrl, status: response.ok ? "reachable" : "http-error", statusCode: response.status }, + localOracle, + }); } catch (error) { report({ - external: { referenceUrl, status: 'unavailable', reason: error instanceof Error ? error.message : String(error) }, + external: { referenceUrl, status: "unavailable", reason: error instanceof Error ? error.message : String(error) }, localOracle, }); } finally { From 5be873b2bcf802fdd78f298e396a5cd112cb5208 Mon Sep 17 00:00:00 2001 From: Tim Nunamaker Date: Wed, 22 Jul 2026 17:05:25 -0500 Subject: [PATCH 03/36] fix(tooling): use root biome in connector hooks Assisted-by: AI Signed-off-by: Tim Nunamaker --- lefthook.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lefthook.yml b/lefthook.yml index c481c065e..dbeb8d15d 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -18,7 +18,7 @@ pre-commit: - name: polyfill-connectors:format glob: "packages/polyfill-connectors/**/*.{ts,jsonc,json}" root: "packages/polyfill-connectors" - run: pnpm exec biome check --write --no-errors-on-unmatched {staged_files} + run: ../../node_modules/.bin/biome check --write --no-errors-on-unmatched {staged_files} stage_fixed: true # Lint .ts + config files (no-write; the format job already applied @@ -26,7 +26,7 @@ pre-commit: - name: polyfill-connectors:lint glob: "packages/polyfill-connectors/**/*.{ts,jsonc,json}" root: "packages/polyfill-connectors" - run: pnpm exec biome check --no-errors-on-unmatched {staged_files} + run: ../../node_modules/.bin/biome check --no-errors-on-unmatched {staged_files} # Type-check runs on the whole package, not just staged files, because # a change in one file can break types in another. From 5f8a6dd95fe8ee6190a7d74e5a830dbf5cc132ec Mon Sep 17 00:00:00 2001 From: Tim Nunamaker Date: Wed, 22 Jul 2026 17:05:51 -0500 Subject: [PATCH 04/36] style: apply biome safe fixes Assisted-by: AI Signed-off-by: Tim Nunamaker --- apps/console/next.config.mjs | 42 +- .../app/(console)/audit/[traceId]/page.tsx | 8 +- apps/console/src/app/(console)/audit/page.tsx | 58 +- .../components/deployment-readiness-panel.tsx | 24 +- .../deployment-readiness-rows.test.ts | 52 +- .../components/deployment-readiness-rows.ts | 72 +- .../(console)/components/segment-error.tsx | 4 +- .../components/source-setup-catalog.tsx | 10 +- .../components/views/standing-demo-data.ts | 154 +- .../views/standing-view-model.test.ts | 598 +++---- .../components/views/standing-view-model.ts | 156 +- .../components/web-push-settings.tsx | 144 +- .../[connectorId]/launch/launch-panel.tsx | 6 +- .../[connectorId]/launch/page.tsx | 2 +- .../browser-session/[connectorId]/page.tsx | 12 +- .../[connectorId]/manual-upload-form.tsx | 6 +- .../manual-upload/[connectorId]/page.tsx | 4 +- .../src/app/(console)/connect/page.tsx | 22 +- .../connect/static-secret-payload.test.ts | 2 +- .../static-secret/[connectorId]/page.tsx | 4 +- .../[connectorId]/static-secret-payload.ts | 2 +- .../connect/status/[connectionId]/page.tsx | 94 +- .../src/app/(console)/deployment/page.tsx | 2 +- .../(console)/deployment/tokens/actions.ts | 2 +- .../app/(console)/deployment/tokens/page.tsx | 10 +- .../app/(console)/device-exporters/actions.ts | 8 +- .../app/(console)/device-exporters/page.tsx | 6 +- .../(console)/device-exporters/render.test.ts | 2 +- apps/console/src/app/(console)/error.tsx | 4 +- .../event-subscriptions/disable-action.ts | 2 +- .../src/app/(console)/explore/actions.ts | 6 +- .../src/app/(console)/explore/error.tsx | 4 +- .../explore/explore-acceptance.test.ts | 130 +- .../app/(console)/explore/explore-canvas.tsx | 162 +- .../explore/explore-control-state.test.ts | 8 +- .../explore/explore-control-state.ts | 2 +- .../src/app/(console)/explore/explore-date.ts | 18 +- .../explore/explore-facet-groups.test.ts | 52 +- .../(console)/explore/explore-facet-groups.ts | Bin 8128 -> 8128 bytes .../explore/explore-facet-unification.test.ts | 42 +- .../explore/explore-feed-grouping.test.ts | 68 +- .../explore/explore-feed-grouping.ts | 24 +- ...explore-frontend-review-hold-fixes.test.ts | 108 +- .../(console)/explore/explore-grammar.test.ts | 34 +- .../app/(console)/explore/explore-grammar.ts | 62 +- .../explore/explore-navigation.test.ts | 2 +- .../(console)/explore/explore-navigation.ts | 22 +- .../explore/explore-p2-search-sort.test.ts | 166 +- .../explore-peek-relationships.test.ts | 32 +- .../explore/explore-peek-relationships.ts | 8 +- .../explore/explore-query-input.test.ts | 92 +- .../explore/explore-saved-views.test.ts | 12 +- .../(console)/explore/explore-saved-views.ts | 2 +- .../(console)/explore/explorer-url.test.ts | 46 +- .../app/(console)/explore/over-time-chart.tsx | 4 +- .../src/app/(console)/explore/page.tsx | 6 +- .../app/(console)/explore/peek-url.test.ts | 12 +- .../explore/search-hit-attribution.test.ts | 24 +- .../app/(console)/grants/[grantId]/page.tsx | 2 +- .../console/src/app/(console)/grants/page.tsx | 24 +- .../app/(console)/grants/pending-actions.ts | 4 +- .../app/(console)/grants/request/actions.ts | 16 +- .../grants/request/connection-pin.test.ts | 30 +- .../src/app/(console)/grants/request/page.tsx | 10 +- apps/console/src/app/(console)/layout.tsx | 2 +- .../(console)/lib/collection-report.test.ts | 52 +- .../app/(console)/lib/collection-report.ts | 10 +- .../(console)/lib/connection-catalog.test.ts | 22 +- .../app/(console)/lib/connection-catalog.ts | 2 +- .../lib/connection-control-result.test.ts | 2 +- .../(console)/lib/connection-evidence.test.ts | 368 ++-- .../app/(console)/lib/connection-evidence.ts | 238 +-- .../lib/connection-label-ambiguity.test.ts | 28 +- .../lib/connection-summary-stats.test.ts | 28 +- .../(console)/lib/connection-summary-stats.ts | 6 +- .../src/app/(console)/lib/data-source.ts | 40 +- .../app/(console)/lib/describe-error.test.ts | 4 +- .../lib/grant-request-connection-pin.ts | 6 +- .../(console)/lib/hybrid-preference.test.ts | 12 +- .../src/app/(console)/lib/next-action.ts | 8 +- .../app/(console)/lib/operator-approvals.ts | 24 +- .../app/(console)/lib/operator-bootstrap.ts | 98 +- .../(console)/lib/operator-grant-request.ts | 92 +- .../src/app/(console)/lib/operator-runs.ts | 32 +- .../src/app/(console)/lib/owner-token.ts | 16 +- .../app/(console)/lib/read-envelope.test.ts | 6 +- .../src/app/(console)/lib/read-envelope.ts | 2 +- .../app/(console)/lib/record-timestamps.ts | 4 +- .../src/app/(console)/lib/ref-client.ts | 14 +- .../src/app/(console)/lib/rs-client.ts | 102 +- .../app/(console)/lib/run-assistance.test.ts | 2 +- .../src/app/(console)/lib/run-assistance.ts | 16 +- .../src/app/(console)/lib/run-gaps.test.ts | 26 +- .../console/src/app/(console)/lib/run-gaps.ts | 2 +- .../lib/source-actionability.test.ts | 26 +- .../app/(console)/lib/source-actionability.ts | 32 +- .../(console)/lib/source-add-support.test.ts | 8 +- .../app/(console)/lib/source-add-support.ts | 10 +- .../lib/source-copy-negative.test.ts | 4 +- .../app/(console)/lib/source-groups.test.ts | 28 +- .../src/app/(console)/lib/source-groups.ts | 4 +- .../lib/source-recovery-state.test.ts | 34 +- .../(console)/lib/source-recovery-state.ts | 24 +- .../(console)/lib/stream-evidence-state.ts | 20 +- .../lib/version-churn-summary.test.ts | 50 +- .../(console)/lib/version-churn-summary.ts | 30 +- apps/console/src/app/(console)/page.tsx | 36 +- .../src/app/(console)/schedules/actions.ts | 8 +- .../app/(console)/schedules/schedule-row.tsx | 2 +- .../console/src/app/(console)/search/page.tsx | 2 +- .../[connector]/[stream]/[recordKey]/page.tsx | 10 +- .../[recordKey]/record-fields-display.test.ts | 6 +- .../[recordKey]/record-fields-display.ts | 8 +- .../[connector]/[stream]/health/page.tsx | 10 +- .../sources/[connector]/[stream]/page.tsx | 22 +- .../record-list-filter-roundtrip.test.ts | 2 +- .../relationship-navigation-smoke.test.ts | 6 +- .../(console)/sources/[connector]/actions.ts | 14 +- .../[connector]/connection-diagnostics.tsx | 8 +- .../(console)/sources/[connector]/page.tsx | 52 +- .../sources/[connector]/rename-connection.tsx | 4 +- .../[connector]/source-detail-demo-data.ts | 6 +- .../src/app/(console)/sources/actions.ts | 10 +- .../src/app/(console)/sources/error.tsx | 4 +- .../(console)/sources/last-known-read.test.ts | 6 +- .../sources/last-known-sync-start.test.ts | 8 +- .../sources/lib/relationships.test.ts | 158 +- .../(console)/sources/lib/relationships.ts | 14 +- .../src/app/(console)/sources/page.tsx | 2 +- .../(console)/sources/sources-demo-data.ts | 104 +- .../sources/sources-view-model.test.ts | 152 +- .../(console)/sources/sources-view-model.ts | 42 +- .../app/(console)/sources/sources-view.css | 172 +- .../app/(console)/stream-playground/page.tsx | 2 +- .../app/(console)/syncs/[runId]/actions.ts | 12 +- .../src/app/(console)/syncs/[runId]/page.tsx | 14 +- .../(console)/syncs/[runId]/stream/actions.ts | 4 +- .../[runId]/stream/neko-client-api-shim.ts | 58 +- .../[runId]/stream/neko-client-config.test.ts | 2 +- .../syncs/[runId]/stream/neko-client.test.ts | 52 +- .../syncs/[runId]/stream/neko-client.ts | 80 +- .../stream/neko-media-contain-fit.test.ts | 8 +- .../(console)/syncs/[runId]/stream/page.tsx | 2 +- .../stream/stream-dialog-layout.test.ts | 10 +- .../[runId]/stream/stream-geometry.test.ts | 110 +- .../stream/stream-keyboard-focus.test.ts | 4 +- .../stream/stream-media-settle.test.ts | 116 +- ...stream-orientation-shell-landscape.test.ts | 6 +- .../stream/stream-reach-diagnostics.test.ts | 8 +- .../stream/stream-reach-diagnostics.ts | 4 +- .../stream-surface-measure-gate.test.ts | 30 +- .../stream/stream-surface-measure-gate.ts | 14 +- .../stream/stream-viewer-cdp-transport.ts | 32 +- .../stream/stream-viewer-control.test.ts | 62 +- .../[runId]/stream/stream-viewer-geometry.ts | 6 +- .../stream/stream-viewer-input-router.test.ts | 16 +- .../stream/stream-viewer-protocol.test.ts | 16 +- .../[runId]/stream/stream-viewer-protocol.ts | 2 +- .../syncs/[runId]/stream/stream-viewer.tsx | 145 +- .../stream/stream-viewport-classifier.test.ts | 48 +- .../stream/stream-visual-quality.test.ts | 18 +- .../console/src/app/(console)/syncs/error.tsx | 4 +- .../src/app/(console)/syncs/syncs-demo.ts | 74 +- .../app/(console)/syncs/syncs-model.test.ts | 74 +- .../src/app/(console)/syncs/syncs-model.ts | 46 +- .../src/app/(console)/syncs/syncs-view.tsx | 6 +- .../src/app/api/stream-debug/route.test.ts | 8 +- .../console/src/app/api/stream-debug/route.ts | 4 +- apps/console/src/app/apple-icon.tsx | 10 +- apps/console/src/app/design-system/page.tsx | 36 +- apps/console/src/app/globals.css | 120 +- apps/console/src/app/icon.svg | 10 +- apps/console/src/app/layout.tsx | 36 +- apps/console/src/app/manifest.ts | 16 +- apps/console/src/app/reference-proxy.ts | 2 +- .../app/well-known/skills/[...path]/route.ts | 2 +- .../src/app/well-known/skills/route.test.ts | 2 +- apps/console/src/lib/agent-skills/catalog.ts | 42 +- apps/console/src/lib/pdpp-cli-command.test.ts | 22 +- apps/site/next.config.mjs | 74 +- apps/site/source.config.ts | 6 +- apps/site/src/app/apple-icon.tsx | 10 +- apps/site/src/app/design/page.tsx | 826 ++++----- apps/site/src/app/docs/[[...slug]]/page.tsx | 2 +- apps/site/src/app/docs/layout.tsx | 2 +- apps/site/src/app/globals.css | 112 +- apps/site/src/app/icon.svg | 10 +- apps/site/src/app/layout.tsx | 16 +- apps/site/src/app/manifest.ts | 20 +- apps/site/src/app/opengraph-image.tsx | 38 +- apps/site/src/app/page.tsx | 6 +- apps/site/src/app/palette/page.tsx | 152 +- apps/site/src/app/reference/layout.tsx | 2 +- apps/site/src/app/reference/page.tsx | 46 +- .../src/app/sandbox/_demo/builders.test.ts | 4 +- apps/site/src/app/sandbox/_demo/builders.ts | 202 +-- .../app/sandbox/_demo/components/shell.tsx | 2 +- .../site/src/app/sandbox/_demo/data-source.ts | 495 +++-- .../src/app/sandbox/_demo/dataset.test.ts | 8 +- apps/site/src/app/sandbox/_demo/dataset.ts | 578 +++--- .../app/sandbox/_demo/operations-fixtures.ts | 266 +-- .../_demo/route-operation-parity.test.ts | 4 +- .../site/src/app/sandbox/_demo/routes.test.ts | 14 +- .../src/app/sandbox/api-examples/page.tsx | 106 +- .../src/app/sandbox/grants/[grantId]/page.tsx | 2 +- apps/site/src/app/sandbox/grants/page.tsx | 56 +- .../site/src/app/sandbox/overview-content.tsx | 16 +- apps/site/src/app/sandbox/overview/page.tsx | 2 +- apps/site/src/app/sandbox/page.tsx | 2 +- .../app/sandbox/records/explore-redirect.ts | 4 +- apps/site/src/app/sandbox/ref/grants/route.ts | 4 +- apps/site/src/app/sandbox/ref/runs/route.ts | 4 +- apps/site/src/app/sandbox/ref/traces/route.ts | 2 +- .../src/app/sandbox/runs/[runId]/page.tsx | 2 +- apps/site/src/app/sandbox/runs/page.tsx | 66 +- .../src/app/sandbox/traces/[traceId]/page.tsx | 2 +- apps/site/src/app/sandbox/traces/page.tsx | 60 +- apps/site/src/app/sandbox/v1/_helpers.ts | 10 +- apps/site/src/app/sandbox/v1/search/route.ts | 2 +- .../[stream]/records/[recordId]/route.ts | 2 +- .../v1/streams/[stream]/records/route.ts | 2 +- apps/site/src/app/sandbox/v1/streams/route.ts | 10 +- .../site/src/app/sandbox/walkthrough/page.tsx | 8 +- .../walkthrough/sandbox-walkthrough.tsx | 44 +- .../src/app/sandbox/walkthrough/scenario.ts | 46 +- .../site/src/app/sandbox/walkthrough/state.ts | 102 +- .../oauth-authorization-server/route.ts | 2 +- .../app/well-known/skills/[...path]/route.ts | 2 +- .../src/app/well-known/skills/route.test.ts | 2 +- apps/site/src/components/hero.tsx | 18 +- apps/site/src/components/reference-app.tsx | 198 +- .../src/components/reference-hero-proof.tsx | 2 +- apps/site/src/lib/agent-skills/catalog.ts | 42 +- apps/site/src/lib/consent-connection-label.ts | 4 +- apps/site/src/lib/docs-shared.tsx | 2 +- apps/site/src/lib/docs-source.ts | 2 +- apps/site/src/lib/longview-world.ts | 98 +- apps/site/src/lib/mock-server.ts | 80 +- apps/site/src/lib/openspec/parse.ts | 2 +- apps/site/src/lib/purpose-docs.ts | 40 +- apps/site/src/lib/seed-data.ts | 418 ++--- apps/site/src/lib/seed-gmail.ts | 96 +- apps/site/src/lib/spec-refs.ts | 20 +- apps/site/src/lib/use-protocol.ts | 42 +- deploy/railway/core-first-boot.mjs | 26 +- deploy/railway/core-supervisor.mjs | 4 +- .../project/Ink Carbon - System Sheet.html | 25 +- .../ink-carbon/project/PDPP Round 3.html | 4 +- .../project/PDPP Vibe Studies v2.html | 4 +- .../ink-carbon/project/PDPP Vibe Studies.html | 4 +- .../project/Recordroom - Console.html | 4 +- .../Watcher - What Longview Can See.html | 7 +- .../ink-carbon/project/assets/logo-mark.svg | 14 +- .../ink-carbon/project/assets/wordmark.svg | 28 +- .../ink-carbon/project/colors_and_type.css | 26 +- .../ink-carbon/project/explorer/app.jsx | 47 +- .../project/explorer/assets/logo-mark.svg | 14 +- .../project/explorer/colors_and_type.css | 32 +- .../ink-carbon/project/explorer/data.js | 1590 ++++++++--------- .../ink-carbon/project/explorer/discover.jsx | 162 +- .../ink-carbon/project/explorer/dispatch.js | 28 +- .../ink-carbon/project/explorer/explorer.css | 825 ++++----- .../ink-carbon/project/explorer/feed.jsx | 51 +- .../ink-carbon/project/explorer/peek.jsx | 51 +- .../project/explorer/primitives.jsx | 98 +- .../ink-carbon/project/explorer/query-bar.jsx | 47 +- .../ink-carbon/project/explorer/query.js | 114 +- .../project/explorer/tweaks-panel.jsx | 145 +- .../ink-carbon/project/explorer/views-1.jsx | 60 +- .../ink-carbon/project/explorer/views-2.jsx | 52 +- .../ink-carbon/project/labs/LivingGrant.jsx | 146 +- .../ink-carbon/project/labs/SpecElements.jsx | 76 +- .../ink-carbon/project/labs/TheAtlas.jsx | 80 +- .../ink-carbon/project/labs/TheContract.jsx | 88 +- .../ink-carbon/project/labs/ThePurposes.jsx | 8 +- .../ink-carbon/project/labs/TheSpecimen.jsx | 66 +- .../ink-carbon/project/labs/ThermalField.jsx | 46 +- .../ink-carbon/project/labs/labs.css | 62 +- .../project/preview/brand-logo.html | 26 +- .../project/preview/colors-primary.html | 4 +- .../project/preview/colors-semantic.html | 4 +- .../project/preview/colors-washes.html | 10 +- .../project/preview/components-badges.html | 6 +- .../project/preview/components-buttons.html | 6 +- .../project/preview/components-cards.html | 10 +- .../project/preview/components-code.html | 10 +- .../preview/components-consent-row.html | 2 +- .../project/preview/components-dataflow.html | 2 +- .../preview/components-grant-card.html | 4 +- .../project/preview/components-inputs.html | 6 +- .../project/preview/iconography.html | 10 +- .../project/preview/layout-grid.html | 12 +- .../ink-carbon/project/preview/radii.html | 14 +- .../ink-carbon/project/preview/shadows.html | 14 +- .../project/preview/spacing-scale.html | 6 +- .../ink-carbon/project/preview/type-body.html | 8 +- .../project/preview/type-display.html | 6 +- .../ink-carbon/project/preview/type-mono.html | 10 +- .../ink-carbon/project/recordroom/console.css | 174 +- .../project/recordroom/console2.css | 154 +- .../ink-carbon/project/recordroom/explore.css | 186 +- .../project/recordroom/image-slot.js | 168 +- .../ink-carbon/project/recordroom/mobile.css | 36 +- .../project/recordroom/overview.css | 72 +- .../ink-carbon/project/recordroom/rr-app.jsx | 210 +-- .../project/recordroom/rr-components.jsx | 64 +- .../ink-carbon/project/recordroom/rr-data.js | 350 ++-- .../project/recordroom/rr-explore-data.js | 543 +++--- .../project/recordroom/rr-explore.jsx | 205 ++- .../project/recordroom/rr-overview.jsx | 72 +- .../project/recordroom/rr-record.jsx | 176 +- .../project/recordroom/rr-sources.jsx | 4 +- .../project/recordroom/rr-syncs.jsx | 124 +- .../project/recordroom/rr-views2.jsx | 60 +- .../ink-carbon/project/recordroom/sources.css | 72 +- .../ink-carbon/project/recordroom/syncs.css | 94 +- .../project/recordroom/tweaks-panel.jsx | 145 +- .../ink-carbon/project/recordroom/watcher.css | 116 +- .../project/reinvention/boards-envelope.jsx | 24 +- .../project/reinvention/boards-round2.jsx | 28 +- .../project/reinvention/boards-round3.jsx | 24 +- .../project/reinvention/design-canvas.jsx | 467 +++-- .../project/reinvention/envelope.css | 170 +- .../ink-carbon/project/reinvention/round2.css | 298 +-- .../ink-carbon/project/reinvention/round3.css | 150 +- .../project/ui_kits/web/ConsentCard.jsx | 64 +- .../project/ui_kits/web/GrantInspector.jsx | 22 +- .../project/ui_kits/web/GrantsList.jsx | 70 +- .../ink-carbon/project/ui_kits/web/Hero.jsx | 34 +- .../project/ui_kits/web/SiteHeader.jsx | 52 +- .../project/ui_kits/web/StreamInventory.jsx | 36 +- .../project/ui_kits/web/Teaching.jsx | 34 +- .../ink-carbon/project/ui_kits/web/index.html | 70 +- .../ink-carbon/project/ui_kits/web/kit.css | 100 +- docs/explorer/uat/harness/capture.mjs | 36 +- docs/explorer/uat/harness/fixtures.mjs | 120 +- docs/explorer/uat/harness/seed-and-serve.mjs | 60 +- packages/cli/src/cache-layout.js | 6 +- packages/cli/src/collector/runner.js | 6 +- packages/cli/src/connect/flow.js | 40 +- packages/cli/src/index.js | 47 +- packages/cli/src/owner-agent/command.js | 77 +- packages/cli/src/owner-agent/control.js | 4 +- .../cli/src/owner-agent/credential-store.js | 46 +- packages/cli/src/owner-agent/device-flow.js | 20 +- packages/cli/src/owner-agent/discovery.js | 18 +- packages/cli/src/owner-agent/lifecycle.js | 14 +- packages/cli/src/owner-agent/setup.js | 6 +- packages/cli/src/package-info.d.ts | 8 +- packages/cli/src/package-info.js | 8 +- packages/cli/src/read/commands.js | 54 +- packages/cli/src/ref/auth.js | 14 +- packages/cli/src/ref/commands/call.js | 28 +- packages/cli/src/ref/commands/connectors.js | 108 +- .../src/ref/commands/event-subscriptions.js | 52 +- packages/cli/src/ref/commands/grant.js | 2 +- packages/cli/src/ref/commands/login.js | 18 +- packages/cli/src/ref/commands/run.js | 2 +- packages/cli/src/ref/commands/trace.js | 2 +- packages/cli/src/ref/errors.js | 14 +- packages/cli/src/ref/fetch.js | 12 +- packages/cli/src/ref/session.js | 54 +- .../bin/pdpp-local-collector.ts | 113 +- .../scripts/pack-install-run.mjs | 12 +- packages/local-collector/src/errors.ts | 4 +- packages/local-collector/src/runner.ts | 78 +- packages/mcp-server/src/credentials.js | 8 +- packages/mcp-server/src/index.js | 16 +- packages/mcp-server/src/rs-client.js | 28 +- packages/mcp-server/src/server.js | 33 +- packages/mcp-server/src/tools.js | 1125 +++++++----- packages/mcp-server/test/smoke-stdio.mjs | 22 +- packages/pdpp-brand/base.css | 14 +- packages/pdpp-brand/chrome.ts | 8 +- packages/pdpp-brand/docs.css | 72 +- packages/pdpp-brand/launch-colors.ts | 4 +- packages/pdpp-brand/record-format.ts | 2 +- packages/read-core/src/index.js | 201 ++- .../remote-surface-optional.ts | 8 +- .../runtime/connection-health.ts | 2 +- .../runtime/rendered-verdict.ts | 2 +- .../scheduler-source-pressure-cooldown.ts | 2 +- .../server/connector-summary-read-model.ts | 14 +- .../server/explore-timeline-substrate.ts | 8 +- .../server/polyfill-manifest-reconcile.ts | 8 +- .../server/record-source-checkpoint.ts | 10 +- .../server/ref-control.ts | 176 +- .../server/routes/ref-dataset.ts | 2 +- .../server/routes/rs-hosted-mcp.ts | 4 +- .../server/streaming/cdp-companion.ts | 24 +- scripts/check-console-health-surfaces.mjs | 22 +- scripts/check-dist-tag-posture.mjs | 15 +- scripts/check-flyio-deploy-env.mjs | 2 +- scripts/check-owner-journey-acceptance.mjs | 14 +- scripts/check-package-release-policy.mjs | 6 +- scripts/check-public-tree-hygiene.mjs | 18 +- scripts/check-railway-deploy-env.mjs | 42 +- scripts/check-railway-ghcr-public.mjs | 32 +- scripts/check-stream-health-audit.mjs | 3 +- scripts/ci-mode.mjs | 4 +- scripts/cli-acceptance-smoke.mjs | 20 +- scripts/dev.mjs | 2 +- scripts/event-subscription-test-receiver.mjs | 28 +- scripts/lib/owner-session.mjs | 18 +- scripts/manual-action-stream-smoke.mjs | 172 +- scripts/openspec-archive-check.mjs | 20 +- scripts/openspec-status.mjs | 12 +- .../owner-journey-acceptance/clean-shell.mjs | 22 +- scripts/owner-journey-acceptance/harness.mjs | 20 +- scripts/owner-journey-acceptance/live.mjs | 274 +-- scripts/owner-journey-acceptance/scan.mjs | 84 +- .../surface-manifest.mjs | 72 +- scripts/perf/bench.mjs | 86 +- scripts/railway-mcp-query-smoke.mjs | 185 +- scripts/read-surface-smoke.mjs | 682 ++++--- scripts/semantic-release-github-output.mjs | 10 +- scripts/spec-check.mjs | 10 +- scripts/stream-evidence-inventory.mjs | 4 +- scripts/stream-health-audit/audit.mjs | 14 +- scripts/stream-health-audit/live.mjs | 56 +- scripts/stream-mint-probe.mjs | 20 +- .../stream-playground-parity-calibration.mjs | 9 +- scripts/sync-agent-skill.mjs | 4 +- 423 files changed, 13096 insertions(+), 11696 deletions(-) diff --git a/apps/console/next.config.mjs b/apps/console/next.config.mjs index a9884cff6..39f71dc54 100644 --- a/apps/console/next.config.mjs +++ b/apps/console/next.config.mjs @@ -23,63 +23,63 @@ const manualUploadBodyLimit = "1024mb"; /** @type {import('next').NextConfig} */ const nextConfig = { ...(allowedDevOrigins.length > 0 ? { allowedDevOrigins } : {}), + experimental: { + cpus: buildWorkers, + proxyClientMaxBodySize: manualUploadBodyLimit, + serverActions: { + bodySizeLimit: manualUploadBodyLimit, + }, + }, output: "standalone", - outputFileTracingRoot: path.join(__dirname, "../.."), outputFileTracingIncludes: { + "/llms-full.txt": ["../../docs/agent-skills/**/*.md", "../../openspec/README.md", "../../pnpm-workspace.yaml"], + "/llms.txt": ["../../docs/agent-skills/**/*.md", "../../openspec/README.md", "../../pnpm-workspace.yaml"], "/well-known/skills/**": [ "../../docs/agent-skills/**/*.md", "../../openspec/README.md", "../../pnpm-workspace.yaml", ], - "/llms-full.txt": ["../../docs/agent-skills/**/*.md", "../../openspec/README.md", "../../pnpm-workspace.yaml"], - "/llms.txt": ["../../docs/agent-skills/**/*.md", "../../openspec/README.md", "../../pnpm-workspace.yaml"], }, + outputFileTracingRoot: path.join(__dirname, "../.."), reactStrictMode: true, - experimental: { - cpus: buildWorkers, - proxyClientMaxBodySize: manualUploadBodyLimit, - serverActions: { - bodySizeLimit: manualUploadBodyLimit, - }, - }, - // Transpile the reference-implementation workspace package so Next can - // consume its TypeScript sources directly once shim pairs (.js + .d.ts) - // collapse into single .ts exports. Without this, Next's bundler would - // reject .ts entries from a node_modules-resolved workspace package. - transpilePackages: ["pdpp-reference-implementation", "@pdpp/brand", "@pdpp/brand-react", "@pdpp/operator-ui"], async redirects() { return [ { - source: "/favicon.ico", destination: "/brand/pdpp-favicon.svg", permanent: false, + source: "/favicon.ico", }, ]; }, async rewrites() { return [ { - source: "/.well-known/oauth-authorization-server", destination: "/well-known/oauth-authorization-server", + source: "/.well-known/oauth-authorization-server", }, { - source: "/.well-known/oauth-protected-resource/:path*", destination: "/well-known/oauth-protected-resource/:path*", + source: "/.well-known/oauth-protected-resource/:path*", }, { - source: "/.well-known/oauth-protected-resource", destination: "/well-known/oauth-protected-resource", + source: "/.well-known/oauth-protected-resource", }, { - source: "/.well-known/skills/:path*", destination: "/well-known/skills/:path*", + source: "/.well-known/skills/:path*", }, { - source: "/.well-known/llms.txt", destination: "/llms.txt", + source: "/.well-known/llms.txt", }, ]; }, + // Transpile the reference-implementation workspace package so Next can + // consume its TypeScript sources directly once shim pairs (.js + .d.ts) + // collapse into single .ts exports. Without this, Next's bundler would + // reject .ts entries from a node_modules-resolved workspace package. + transpilePackages: ["pdpp-reference-implementation", "@pdpp/brand", "@pdpp/brand-react", "@pdpp/operator-ui"], webpack(config) { config.resolve.alias = { ...config.resolve.alias, diff --git a/apps/console/src/app/(console)/audit/[traceId]/page.tsx b/apps/console/src/app/(console)/audit/[traceId]/page.tsx index b54f89ae1..0ec27fe18 100644 --- a/apps/console/src/app/(console)/audit/[traceId]/page.tsx +++ b/apps/console/src/app/(console)/audit/[traceId]/page.tsx @@ -176,12 +176,12 @@ function groupTimeline(events: SpineEvent[]): TimelineNode[] { } const runLen = j - i; if (runLen >= PROGRESS_GROUP_THRESHOLD) { - nodes.push({ kind: "progress-group", events: events.slice(i, j), start: i }); + nodes.push({ events: events.slice(i, j), kind: "progress-group", start: i }); i = j; continue; } } - nodes.push({ kind: "event", event: current, index: i }); + nodes.push({ event: current, index: i, kind: "event" }); i += 1; } return nodes; @@ -413,10 +413,10 @@ export default async function TraceDetailPage({ href={`/grants/${encodeURIComponent(id)}`} key={`grant:${id}`} style={{ + alignItems: "center", border: "1px solid var(--color-border)", color: "var(--color-foreground)", display: "inline-flex", - alignItems: "center", padding: "var(--space-1) var(--space-2-5)", }} > @@ -433,10 +433,10 @@ export default async function TraceDetailPage({ href={`/syncs/${encodeURIComponent(id)}`} key={`run:${id}`} style={{ + alignItems: "center", border: "1px solid var(--color-border)", color: "var(--color-foreground)", display: "inline-flex", - alignItems: "center", padding: "var(--space-1) var(--space-2-5)", }} > diff --git a/apps/console/src/app/(console)/audit/page.tsx b/apps/console/src/app/(console)/audit/page.tsx index f71edb6da..2ecdc24c7 100644 --- a/apps/console/src/app/(console)/audit/page.tsx +++ b/apps/console/src/app/(console)/audit/page.tsx @@ -151,9 +151,9 @@ function TracesHeader() {

@@ -221,7 +221,7 @@ function TraceFilterBand({ params, hasFilters }: { params: Params; hasFilters: b Apply {hasFilters ? ( - + Reset ) : null} @@ -263,7 +263,7 @@ function TracesDesktopTable({ traces, params }: { traces: TraceSummary[]; params {traces.map((trace) => { const peeked = params.peek === trace.trace_id; - const peekHref = listHref(params, { peek: peeked ? undefined : trace.trace_id, cursor: undefined }); + const peekHref = listHref(params, { cursor: undefined, peek: peeked ? undefined : trace.trace_id }); const label = traceRowLabel(trace); const kinds = trace.kinds.slice(0, 3).join(", "); return ( @@ -298,6 +298,7 @@ function TraceSubjectLink({ href, label, traceId }: { href: string; label: strin {label} @@ -313,10 +313,10 @@ function TraceSubjectLink({ href, label, traceId }: { href: string; label: strin {traceId} @@ -361,6 +361,7 @@ function TraceMobileCard({ {label} {trace.trace_id}

{params.cursor ? ( - + ← First page ) : null} {result.has_more && result.next_cursor ? ( Next → @@ -470,18 +470,18 @@ function TracesEmptyState({ hasFilters }: { hasFilters: boolean }) { return (

@@ -489,9 +489,9 @@ function TracesEmptyState({ hasFilters }: { hasFilters: boolean }) {

@@ -518,12 +518,12 @@ function TracesPeekPanel({ traceId, envelope, cliCommand, listParams }: PeekPane return ( -

+
Trace {traceId}
- +
@@ -532,19 +532,19 @@ function TracesPeekPanel({ traceId, envelope, cliCommand, listParams }: PeekPane {/* Summary meta */}
- + {envelope.events.length} events {firstEvent ? ( - + actor{" "} {firstEvent.actor_type}/{firstEvent.actor_id} @@ -559,19 +559,19 @@ function TracesPeekPanel({ traceId, envelope, cliCommand, listParams }: PeekPane
-
+
{event.event_type} @@ -584,8 +584,8 @@ function TracesPeekPanel({ traceId, envelope, cliCommand, listParams }: PeekPane {event.status} @@ -603,20 +603,20 @@ function TracesPeekPanel({ traceId, envelope, cliCommand, listParams }: PeekPane {/* CLI command */}

CLI diff --git a/apps/console/src/app/(console)/components/deployment-readiness-panel.tsx b/apps/console/src/app/(console)/components/deployment-readiness-panel.tsx index 90d435195..33b0b70cf 100644 --- a/apps/console/src/app/(console)/components/deployment-readiness-panel.tsx +++ b/apps/console/src/app/(console)/components/deployment-readiness-panel.tsx @@ -89,7 +89,7 @@ function useRefreshTokenAdvertisement(): RefreshTokenProbe { const grants = Array.isArray(body.grant_types_supported) ? body.grant_types_supported : []; const refreshTokenSupported = grants.some((g) => g === "refresh_token"); if (!cancelled) { - setProbe({ state: "loaded", refreshTokenSupported }); + setProbe({ refreshTokenSupported, state: "loaded" }); } } catch { if (!cancelled) { @@ -118,32 +118,32 @@ function verdictPresentation(verdict: Verdict): { label: string; body: string; t switch (verdict) { case "ready": return { - label: "Ready to share with Claude / ChatGPT", body: "Owner gate, origin, storage, embeddings, and refresh-token metadata all check out.", + label: "Ready to share with Claude / ChatGPT", toneClass: "border-[color:var(--success)]/30 bg-[color:var(--success-wash)] text-[color:var(--success)]", }; case "attention": return { - label: "Attention needed before sharing", body: "Some rows are usable but suboptimal. Read the hints below.", + label: "Attention needed before sharing", toneClass: "border-[color:var(--warning)]/30 bg-[color:var(--warning-wash)] text-[color:var(--warning)]", }; case "blocked": return { - label: "Not yet ready to share", body: "At least one row is in an error state. Fix it before handing the MCP URL to an agent.", + label: "Not yet ready to share", toneClass: "border-destructive/30 bg-destructive/5 text-destructive", }; case "unknown": return { - label: "Some checks still running", body: "Browser-side probes have not returned yet.", + label: "Some checks still running", toneClass: "border-border/80 bg-muted/40 text-foreground", }; default: return { - label: "Some checks still running", body: "Browser-side probes have not returned yet.", + label: "Some checks still running", toneClass: "border-border/80 bg-muted/40 text-foreground", }; } @@ -163,27 +163,27 @@ function ReadinessRowItem({ row }: { row: ReadinessRow }) { } const STATUS_TONE: Record = { - ok: "bg-[color:var(--success-wash)] text-[color:var(--success)]", - warn: "bg-[color:var(--warning-wash)] text-[color:var(--warning)]", error: "bg-destructive/10 text-destructive", info: "bg-muted text-muted-foreground", + ok: "bg-[color:var(--success-wash)] text-[color:var(--success)]", unknown: "bg-muted text-muted-foreground", + warn: "bg-[color:var(--warning-wash)] text-[color:var(--warning)]", }; const STATUS_LABEL: Record = { - ok: "ready", - warn: "attention", error: "blocked", info: "n/a", + ok: "ready", unknown: "checking", + warn: "attention", }; const STATUS_BADGE_TONE: Record = { - ok: "success", - warn: "warning", error: "danger", info: "neutral", + ok: "success", unknown: "neutral", + warn: "warning", }; function StatusChip({ status }: { status: ReadinessStatus }) { diff --git a/apps/console/src/app/(console)/components/deployment-readiness-rows.test.ts b/apps/console/src/app/(console)/components/deployment-readiness-rows.test.ts index d3a5417ce..47bb846d3 100644 --- a/apps/console/src/app/(console)/components/deployment-readiness-rows.test.ts +++ b/apps/console/src/app/(console)/components/deployment-readiness-rows.test.ts @@ -30,25 +30,25 @@ import { } from "./deployment-readiness-rows.ts"; const HEALTHY_DISK_HEADROOM: DiskHeadroomInputs = { - path: "/data", freeBytesOnDataFs: 20 * 1024 * 1024 * 1024, // 20 GiB - totalBytesOnDataFs: 100 * 1024 * 1024 * 1024, largestRelationBytes: null, largestRelationName: null, mountLabel: null, + path: "/data", + totalBytesOnDataFs: 100 * 1024 * 1024 * 1024, }; const baseInputs: ServerInputs = { - ownerPasswordProvenance: "redacted", - referenceOriginConfigured: "https://example.com", - embeddingBackendConfigured: true, + databasePath: "/data/pdpp.db", + diskHeadroom: [HEALTHY_DISK_HEADROOM], embeddingBackendAvailable: true, - embeddingModelCachePresent: true, + embeddingBackendConfigured: true, embeddingDownloadAllowed: true, + embeddingModelCachePresent: true, + ownerPasswordProvenance: "redacted", + referenceOriginConfigured: "https://example.com", vectorIndexKind: "sqlite-vec", vectorIndexState: "built", - databasePath: "/data/pdpp.db", - diskHeadroom: [HEALTHY_DISK_HEADROOM], }; const OWNER_PASSWORD_ENV_RE = /PDPP_OWNER_PASSWORD/; @@ -138,8 +138,8 @@ test("embeddingCacheRow is error when uncached and download disabled", () => { const row = embeddingCacheRow({ ...baseInputs, embeddingBackendAvailable: false, - embeddingModelCachePresent: false, embeddingDownloadAllowed: false, + embeddingModelCachePresent: false, }); assert.equal(row.status, "error"); }); @@ -148,8 +148,8 @@ test("embeddingCacheRow is warn while cache is warming", () => { const row = embeddingCacheRow({ ...baseInputs, embeddingBackendAvailable: false, - embeddingModelCachePresent: false, embeddingDownloadAllowed: true, + embeddingModelCachePresent: false, }); assert.equal(row.status, "warn"); }); @@ -167,11 +167,11 @@ test("refreshTokenRow is warn when well-known is unreachable", () => { }); test("refreshTokenRow is ok when refresh_token is advertised", () => { - assert.equal(refreshTokenRow({ state: "loaded", refreshTokenSupported: true }).status, "ok"); + assert.equal(refreshTokenRow({ refreshTokenSupported: true, state: "loaded" }).status, "ok"); }); test("refreshTokenRow is error when refresh_token is missing", () => { - const row = refreshTokenRow({ state: "loaded", refreshTokenSupported: false }); + const row = refreshTokenRow({ refreshTokenSupported: false, state: "loaded" }); assert.equal(row.status, "error"); assert.match(row.hint ?? "", DOCKER_COMPOSE_PULL_RE); }); @@ -179,31 +179,31 @@ test("refreshTokenRow is error when refresh_token is missing", () => { // ─── Overall verdict ──────────────────────────────────────────────────────── test("overallVerdict ready when every row is ok", () => { - const rows: ReadinessRow[] = [{ check: "x", status: "ok", detail: "" }]; + const rows: ReadinessRow[] = [{ check: "x", detail: "", status: "ok" }]; assert.equal(overallVerdict(rows), "ready"); }); test("overallVerdict blocked when any row is error", () => { const rows: ReadinessRow[] = [ - { check: "x", status: "ok", detail: "" }, - { check: "y", status: "error", detail: "" }, - { check: "z", status: "warn", detail: "" }, + { check: "x", detail: "", status: "ok" }, + { check: "y", detail: "", status: "error" }, + { check: "z", detail: "", status: "warn" }, ]; assert.equal(overallVerdict(rows), "blocked"); }); test("overallVerdict attention when any row is warn but none is error", () => { const rows: ReadinessRow[] = [ - { check: "x", status: "ok", detail: "" }, - { check: "y", status: "warn", detail: "" }, + { check: "x", detail: "", status: "ok" }, + { check: "y", detail: "", status: "warn" }, ]; assert.equal(overallVerdict(rows), "attention"); }); test("overallVerdict unknown when probes still loading and nothing worse", () => { const rows: ReadinessRow[] = [ - { check: "x", status: "ok", detail: "" }, - { check: "y", status: "unknown", detail: "" }, + { check: "x", detail: "", status: "ok" }, + { check: "y", detail: "", status: "unknown" }, ]; assert.equal(overallVerdict(rows), "unknown"); }); @@ -279,9 +279,9 @@ test("diskHeadroomRow includes workload hint when free < largestRelationBytes", makeEntry({ // 4 GiB free — below warn (5 GiB) and below largest relation (6 GiB). freeBytesOnDataFs: 4 * GiB, - totalBytesOnDataFs: 100 * GiB, largestRelationBytes: 6 * GiB, largestRelationName: "records", + totalBytesOnDataFs: 100 * GiB, }), ], }); @@ -297,9 +297,9 @@ test("diskHeadroomRow omits workload hint when free >= largestRelationBytes", () makeEntry({ // 4 GiB free — below warn but ABOVE largest relation (3 GiB). freeBytesOnDataFs: 4 * GiB, - totalBytesOnDataFs: 100 * GiB, largestRelationBytes: 3 * GiB, largestRelationName: "records", + totalBytesOnDataFs: 100 * GiB, }), ], }); @@ -313,9 +313,9 @@ test("diskHeadroomRow omits workload hint when largestRelationBytes is null (SQL diskHeadroom: [ makeEntry({ freeBytesOnDataFs: 4 * GiB, - totalBytesOnDataFs: 100 * GiB, largestRelationBytes: null, largestRelationName: null, + totalBytesOnDataFs: 100 * GiB, }), ], }); @@ -330,7 +330,7 @@ test("diskHeadroomRows returns one row per entry", () => { ...baseInputs, diskHeadroom: [ makeEntry({ freeBytesOnDataFs: 20 * GiB, mountLabel: "data" }), - makeEntry({ path: "/var/lib/postgresql/data", freeBytesOnDataFs: 8 * GiB, mountLabel: "postgres" }), + makeEntry({ freeBytesOnDataFs: 8 * GiB, mountLabel: "postgres", path: "/var/lib/postgresql/data" }), ], }); assert.equal(rows.length, 2); @@ -356,10 +356,10 @@ test("diskHeadroomRows: unmeasured postgres entry shows info, not a false green" diskHeadroom: [ makeEntry({ freeBytesOnDataFs: 20 * GiB, mountLabel: "data" }), makeEntry({ - path: "/var/lib/postgresql/data", freeBytesOnDataFs: null, - totalBytesOnDataFs: null, mountLabel: "postgres", + path: "/var/lib/postgresql/data", + totalBytesOnDataFs: null, }), ], }); diff --git a/apps/console/src/app/(console)/components/deployment-readiness-rows.ts b/apps/console/src/app/(console)/components/deployment-readiness-rows.ts index fdd679223..0d05b8db0 100644 --- a/apps/console/src/app/(console)/components/deployment-readiness-rows.ts +++ b/apps/console/src/app/(console)/components/deployment-readiness-rows.ts @@ -68,23 +68,23 @@ export function extractReadinessInputs(report: DeploymentDiagnostics): ServerInp const largestRelation = report.database.top_relations?.[0] ?? null; const dhEntries = report.disk_headroom ?? []; return { - ownerPasswordProvenance: owner?.provenance ?? "absent", - referenceOriginConfigured: origin?.provenance === "present" ? origin.value : null, - embeddingBackendConfigured: report.semantic.backend.configured, - embeddingBackendAvailable: report.semantic.backend.available, - embeddingModelCachePresent: report.semantic.backend.model_cache_present, - embeddingDownloadAllowed: report.semantic.backend.download_allowed, - vectorIndexKind: report.semantic.index.kind, - vectorIndexState: report.semantic.index.state, databasePath: report.database.path, diskHeadroom: dhEntries.map((dh) => ({ - path: dh.path, freeBytesOnDataFs: dh.free_bytes, - totalBytesOnDataFs: dh.total_bytes, - mountLabel: dh.mount_label ?? null, largestRelationBytes: largestRelation?.bytes ?? null, largestRelationName: largestRelation?.name ?? null, + mountLabel: dh.mount_label ?? null, + path: dh.path, + totalBytesOnDataFs: dh.total_bytes, })), + embeddingBackendAvailable: report.semantic.backend.available, + embeddingBackendConfigured: report.semantic.backend.configured, + embeddingDownloadAllowed: report.semantic.backend.download_allowed, + embeddingModelCachePresent: report.semantic.backend.model_cache_present, + ownerPasswordProvenance: owner?.provenance ?? "absent", + referenceOriginConfigured: origin?.provenance === "present" ? origin.value : null, + vectorIndexKind: report.semantic.index.kind, + vectorIndexState: report.semantic.index.state, }; } @@ -92,15 +92,15 @@ export function ownerPasswordRow(inputs: ServerInputs): ReadinessRow { if (inputs.ownerPasswordProvenance === "redacted") { return { check: "Owner password gate", - status: "ok", detail: "PDPP_OWNER_PASSWORD is set; owner surfaces require sign-in.", + status: "ok", }; } return { check: "Owner password gate", - status: "error", detail: "PDPP_OWNER_PASSWORD is not set.", hint: "Set `PDPP_OWNER_PASSWORD` in your env and restart; otherwise `/owner`, `/device`, `/consent`, and `/` are reachable without auth.", + status: "error", }; } @@ -108,17 +108,17 @@ export function referenceOriginRow(inputs: ServerInputs, browserOrigin: string | if (!inputs.referenceOriginConfigured) { return { check: "Reference origin alignment", - status: "warn", detail: "PDPP_REFERENCE_ORIGIN is not set. The deployment will infer the origin from request headers, which is brittle behind proxies.", hint: "Set `PDPP_REFERENCE_ORIGIN` to the URL you are visiting (e.g. `https://-3002.proxy.runpod.net`). Mismatches break the MCP and OAuth callback flows.", + status: "warn", }; } if (browserOrigin === null) { return { check: "Reference origin alignment", - status: "unknown", detail: `PDPP_REFERENCE_ORIGIN=${inputs.referenceOriginConfigured}. Browser origin not yet observed.`, + status: "unknown", }; } const configured = stripTrailingSlash(inputs.referenceOriginConfigured); @@ -126,15 +126,15 @@ export function referenceOriginRow(inputs: ServerInputs, browserOrigin: string | if (configured === observed) { return { check: "Reference origin alignment", - status: "ok", detail: `Configured origin matches the browser origin (${observed}).`, + status: "ok", }; } return { check: "Reference origin alignment", - status: "warn", detail: `PDPP_REFERENCE_ORIGIN=${configured}; you are viewing this dashboard from ${observed}.`, hint: "Set `PDPP_REFERENCE_ORIGIN` to the URL you are visiting (e.g. `https://-3002.proxy.runpod.net`). Mismatches break the MCP and OAuth callback flows.", + status: "warn", }; } @@ -142,29 +142,29 @@ export function storageBackendRow(inputs: ServerInputs): ReadinessRow { if (inputs.vectorIndexKind === null && inputs.vectorIndexState === null) { return { check: "Storage backend", - status: "info", detail: `Database at ${inputs.databasePath}. No vector index configured yet.`, + status: "info", }; } if (inputs.vectorIndexState === "stale") { return { check: "Storage backend", - status: "warn", detail: `Database at ${inputs.databasePath}; vector index (${inputs.vectorIndexKind ?? "unknown"}) is stale.`, hint: "Storage backend reports unhealthy. See `docs/operator/selfhost-quickstart.md` for storage layout.", + status: "warn", }; } if (inputs.vectorIndexState === "building") { return { check: "Storage backend", - status: "info", detail: `Database at ${inputs.databasePath}; vector index (${inputs.vectorIndexKind ?? "unknown"}) is still building.`, + status: "info", }; } return { check: "Storage backend", - status: "ok", detail: `Database at ${inputs.databasePath}; vector index (${inputs.vectorIndexKind ?? "n/a"}) is built.`, + status: "ok", }; } @@ -172,30 +172,30 @@ export function embeddingCacheRow(inputs: ServerInputs): ReadinessRow { if (!inputs.embeddingBackendConfigured) { return { check: "Embedding cache", - status: "info", detail: "No semantic embedding backend configured. Lexical retrieval still works.", + status: "info", }; } if (inputs.embeddingModelCachePresent === true && inputs.embeddingBackendAvailable) { return { check: "Embedding cache", - status: "ok", detail: "Embedding model is cached and the backend is available.", + status: "ok", }; } if (inputs.embeddingModelCachePresent === false && inputs.embeddingDownloadAllowed === false) { return { check: "Embedding cache", - status: "error", detail: "Embedding model is not cached and download is disabled.", hint: "Embedding cache is still downloading or missing. Wait for first-boot download to finish, or set `PDPP_EMBEDDING_DOWNLOAD_ALLOWED=0` if you do not need semantic search yet.", + status: "error", }; } return { check: "Embedding cache", - status: "warn", detail: "Embedding model cache is still warming up or the backend is not yet ready.", hint: "Embedding cache is still downloading or missing. Wait for first-boot download to finish, or set `PDPP_EMBEDDING_DOWNLOAD_ALLOWED=0` if you do not need semantic search yet.", + status: "warn", }; } @@ -203,30 +203,30 @@ export function refreshTokenRow(probe: RefreshTokenProbe): ReadinessRow { if (probe.state === "loading") { return { check: "MCP refresh-token advertisement", - status: "unknown", detail: "Checking the authorization-server metadata…", + status: "unknown", }; } if (probe.state === "unreachable") { return { check: "MCP refresh-token advertisement", - status: "warn", detail: "Could not reach `/.well-known/oauth-authorization-server` from this origin.", hint: "If your `AS_ISSUER` is not co-located with the dashboard origin, this check may show `warn` even on a healthy deployment. Confirm `grant_types_supported` includes `refresh_token` on your AS metadata directly.", + status: "warn", }; } if (probe.refreshTokenSupported) { return { check: "MCP refresh-token advertisement", - status: "ok", detail: "Authorization-server metadata advertises `refresh_token`.", + status: "ok", }; } return { check: "MCP refresh-token advertisement", - status: "error", detail: "Authorization-server metadata does not advertise `refresh_token`.", hint: "Reference image is too old to advertise `refresh_token`. `docker compose pull` to the current image.", + status: "error", }; } @@ -266,8 +266,8 @@ function diskHeadroomEntryRow(dh: DiskHeadroomInputs): ReadinessRow { if (dh.freeBytesOnDataFs === null) { return { check: checkLabel, - status: "info", detail: `Disk headroom could not be measured${dh.path ? ` on ${dh.path}` : ""}.`, + status: "info", }; } const free = dh.freeBytesOnDataFs; @@ -275,23 +275,23 @@ function diskHeadroomEntryRow(dh: DiskHeadroomInputs): ReadinessRow { if (free < DISK_ERROR_BYTES) { return { check: checkLabel, - status: "error", detail: `Only ${formatBytes(free)} free${pathLabel}. A restart or image build will very likely fail with "No space left on device".${workloadSuffix(dh, free)}`, hint: "Run `docker builder prune` or `docker system prune` to reclaim build cache and stopped containers. Inspect Docker volumes manually before removing any volume data.", + status: "error", }; } if (free < DISK_WARN_BYTES) { return { check: checkLabel, - status: "warn", detail: `${formatBytes(free)} free${pathLabel}. Disk space is running low.${workloadSuffix(dh, free)}`, hint: "Consider running `docker system prune` to reclaim build cache before the next restart.", + status: "warn", }; } return { check: checkLabel, - status: "ok", detail: `${formatBytes(free)} free${pathLabel}.`, + status: "ok", }; } @@ -303,8 +303,8 @@ export function diskHeadroomRows(inputs: ServerInputs): ReadinessRow[] { return [ { check: "Disk headroom", - status: "info", detail: "Disk headroom could not be measured on this filesystem.", + status: "info", }, ]; } @@ -318,8 +318,8 @@ export function diskHeadroomRow(inputs: ServerInputs): ReadinessRow { if (inputs.diskHeadroom.length === 0) { return { check: "Disk headroom", - status: "info", detail: "Disk headroom could not be measured on this filesystem.", + status: "info", }; } const first = inputs.diskHeadroom[0]; @@ -328,8 +328,8 @@ export function diskHeadroomRow(inputs: ServerInputs): ReadinessRow { if (!first) { return { check: "Disk headroom", - status: "info", detail: "Disk headroom could not be measured on this filesystem.", + status: "info", }; } return diskHeadroomEntryRow(first); diff --git a/apps/console/src/app/(console)/components/segment-error.tsx b/apps/console/src/app/(console)/components/segment-error.tsx index bb290fbc1..a7c701e48 100644 --- a/apps/console/src/app/(console)/components/segment-error.tsx +++ b/apps/console/src/app/(console)/components/segment-error.tsx @@ -54,10 +54,10 @@ export function SegmentError({

{title}

{description}

diff --git a/apps/console/src/app/(console)/components/source-setup-catalog.tsx b/apps/console/src/app/(console)/components/source-setup-catalog.tsx index e58f7079a..20614d8a9 100644 --- a/apps/console/src/app/(console)/components/source-setup-catalog.tsx +++ b/apps/console/src/app/(console)/components/source-setup-catalog.tsx @@ -186,13 +186,13 @@ function ExistingSourceLinks({
Open in Explore Source details @@ -263,11 +263,11 @@ function SourceSetupCard({ {action ? ( <> Next - + {action.label} {secondaryAction ? ( - + {secondaryAction.label} ) : null} @@ -305,7 +305,7 @@ function ServerSetupSummary({ entries }: { entries: readonly ConnectorCatalogEnt

{entry.displayName}

{sourceMethodLine(entry, 0)}

- + Open server settings diff --git a/apps/console/src/app/(console)/components/views/standing-demo-data.ts b/apps/console/src/app/(console)/components/views/standing-demo-data.ts index c4d5142e6..38de770d2 100644 --- a/apps/console/src/app/(console)/components/views/standing-demo-data.ts +++ b/apps/console/src/app/(console)/components/views/standing-demo-data.ts @@ -32,169 +32,169 @@ function iso(daysAgo: number): string { } const BEARERS: OwnerIssuedClient[] = [ - { client_id: "cli_claude_desktop", client_name: "Claude Desktop", active_token_count: 1, created_at: iso(40) }, - { client_id: "cli_framework", client_name: "CLI on framework", active_token_count: 2, created_at: iso(120) }, + { active_token_count: 1, client_id: "cli_claude_desktop", client_name: "Claude Desktop", created_at: iso(40) }, + { active_token_count: 2, client_id: "cli_framework", client_name: "CLI on framework", created_at: iso(120) }, // Worst case: a real owner bearer with NO human name — the long machine // client_id IS the identity and must truncate without breaking the row. { + active_token_count: 1, client_id: "single-use-proof-1781473829100-7f3a9c2e-b04d-4e51-9a2f-6c8e1d0b5a73", client_name: null, - active_token_count: 1, created_at: iso(3), }, // Named client whose machine id is a full OAuth client URL — name stays // prominent, the long id rides beneath it, truncated. { + active_token_count: 4, client_id: "https://chatgpt.com/connector_platform_oauth_client/2f1a8b3c-9d4e-4f0a-8c7b-1e2d3f4a5b6c", client_name: "ChatGPT", - active_token_count: 4, created_at: iso(7), }, ]; const GRANTS: GrantSummary[] = [ { - object: "grant_summary", - grant_id: "grant_atlas", client_id: "Atlas Mortgage", connector_id: "plaid", - status: "active", - first_at: iso(30), - last_at: iso(0), event_count: 12, - kinds: ["pay_statements", "transactions"], failure: null, + first_at: iso(30), + grant_id: "grant_atlas", + kinds: ["pay_statements", "transactions"], + last_at: iso(0), + object: "grant_summary", + status: "active", }, { - object: "grant_summary", - grant_id: "grant_northstar", client_id: "Northstar HR", connector_id: "employment", - status: "issued", - first_at: iso(14), - last_at: iso(2), event_count: 4, - kinds: ["employment"], failure: null, + first_at: iso(14), + grant_id: "grant_northstar", + kinds: ["employment"], + last_at: iso(2), + object: "grant_summary", + status: "issued", }, ]; const TRACES: TraceSummary[] = [ { - object: "trace_summary", - trace_id: "trace_1", - status: "succeeded", actor_id: "Claude Desktop", actor_type: "client", client_id: "Claude Desktop", - grant_id: null, - run_id: null, - request_id: null, - first_at: iso(0), - last_at: iso(0), event_count: 412, - kinds: ["transactions"], failure: null, - }, - { + first_at: iso(0), + grant_id: null, + kinds: ["transactions"], + last_at: iso(0), object: "trace_summary", - trace_id: "trace_2", + request_id: null, + run_id: null, status: "succeeded", + trace_id: "trace_1", + }, + { actor_id: "Atlas Mortgage", actor_type: "client", client_id: "Atlas Mortgage", - grant_id: "grant_atlas", - run_id: null, - request_id: null, - first_at: iso(1), - last_at: iso(1), event_count: 38, - kinds: ["pay_statements"], failure: null, + first_at: iso(1), + grant_id: "grant_atlas", + kinds: ["pay_statements"], + last_at: iso(1), + object: "trace_summary", + request_id: null, + run_id: null, + status: "succeeded", + trace_id: "trace_2", }, { - object: "trace_summary", - trace_id: "trace_3", - status: "denied", actor_id: "Unknown app", actor_type: "client", client_id: "Unknown app", - grant_id: null, - run_id: null, - request_id: null, - first_at: iso(2), - last_at: iso(2), event_count: 0, - kinds: ["browsing"], failure: { event_type: "trace.denied", reason: "scope not granted" }, + first_at: iso(2), + grant_id: null, + kinds: ["browsing"], + last_at: iso(2), + object: "trace_summary", + request_id: null, + run_id: null, + status: "denied", + trace_id: "trace_3", }, ]; const FAILED_RUNS: RunSummary[] = [ { - object: "run_summary", - run_id: "run_meridian", - status: "failed", connector_id: "current_activity", - grant_id: null, - provider_id: null, - first_at: iso(2), - last_at: iso(2), event_count: 3, + failure_reason: "First Meridian's connection expired. Reconnect to resume syncing.", + first_at: iso(2), + grant_id: null, kinds: ["run.failed"], + last_at: iso(2), needs_input: false, - failure_reason: "First Meridian's connection expired. Reconnect to resume syncing.", + object: "run_summary", + provider_id: null, + run_id: "run_meridian", + status: "failed", }, ]; const PENDING: PendingApproval[] = [ { - object: "approval", approval_id: "appr_atlas", client_id: "Atlas Mortgage", created_at: iso(0), - kind: "consent", - user_code: "WXYZ-1234", grant_preview: { source: null, streams: [{ name: "pay_statements" }, { name: "employment" }, { name: "transactions" }], }, + kind: "consent", + object: "approval", + user_code: "WXYZ-1234", }, ]; const SUMMARY = { - object: "dataset_summary" as const, - record_count: 48_120, - connector_count: 10, - stream_count: 24, - total_retained_bytes: 1_073_741_824, blob_bytes: 0, - record_json_bytes: 0, - record_changes_json_bytes: 0, - earliest_record_time: iso(365), - latest_record_time: iso(0), + connector_count: 10, earliest_ingested_at: iso(365), + earliest_record_time: iso(365), latest_ingested_at: iso(0), + latest_record_time: iso(0), + object: "dataset_summary" as const, + projection: { computed_at: iso(0), state: "fresh" as const }, + record_changes_json_bytes: 0, + record_count: 48_120, + record_json_bytes: 0, + stream_count: 24, top_connectors: [], - projection: { state: "fresh" as const, computed_at: iso(0) }, + total_retained_bytes: 1_073_741_824, }; export function buildDemoInputs(scenario: DemoScenario, hrefs: StandingHrefs): StandingInputs { const base: StandingInputs = { - now: NOW, - hrefs, - summary: SUMMARY, - bearerClients: BEARERS, - grants: GRANTS, - traces: TRACES, - pendingApprovals: [], - failedTraces: [], - failedRuns: [], - sourceWork: EMPTY_SOURCE_WORK_GROUPS, advisoryOwnerActions: [], attentionConnections: [], + bearerClients: BEARERS, + failedRuns: [], + failedTraces: [], + grants: GRANTS, + hrefs, + now: NOW, overviewLoadIssues: [], + pendingApprovals: [], sourceIssues: [], + sourceWork: EMPTY_SOURCE_WORK_GROUPS, + summary: SUMMARY, + traces: TRACES, }; if (scenario === "decide") { return { ...base, pendingApprovals: PENDING }; @@ -202,17 +202,17 @@ export function buildDemoInputs(scenario: DemoScenario, hrefs: StandingHrefs): S if (scenario === "alarm") { return { ...base, - failedRuns: FAILED_RUNS, attentionConnections: [ { + actionLabel: "Check the collector", connectorKey: "claude-code", - routeId: "cin_demo_claude_code", deviceLocal: true, label: "Claude Code on workstation", + routeId: "cin_demo_claude_code", what: "Check the collector before this source can make progress.", - actionLabel: "Check the collector", }, ], + failedRuns: FAILED_RUNS, }; } return base; diff --git a/apps/console/src/app/(console)/components/views/standing-view-model.test.ts b/apps/console/src/app/(console)/components/views/standing-view-model.test.ts index d2e4b71a5..986a3732e 100644 --- a/apps/console/src/app/(console)/components/views/standing-view-model.test.ts +++ b/apps/console/src/app/(console)/components/views/standing-view-model.test.ts @@ -33,18 +33,18 @@ import { } from "./standing-view-model.ts"; const HREFS: StandingHrefs = { - grants: "/grants", - grantPackages: "/grants/packages", - notifications: "/notifications", - runs: "/syncs", - sources: "/sources", - traces: "/audit", + connection: (id) => `/sources/${id}`, deployment: "/deployment", deploymentTokens: "/deployment/tokens", - connection: (id) => `/sources/${id}`, grant: (id) => `/grants/${id}`, + grantPackages: "/grants/packages", + grants: "/grants", + notifications: "/notifications", run: (id) => `/syncs/${id}`, + runs: "/syncs", + sources: "/sources", trace: (id) => `/audit/${id}`, + traces: "/audit", }; const NOW = new Date("2026-06-13T12:00:00Z"); @@ -78,35 +78,35 @@ const WILL_NOT_CLAIM_ALL_CLEAR_RE = /will not claim all-clear from partial data/ function baseInputs(over: Partial = {}): StandingInputs { return { - now: NOW, + advisoryOwnerActions: [], + attentionConnections: [], + bearerClients: [], + failedRuns: [], + failedTraces: [], + grants: [], hrefs: HREFS, + now: NOW, + overviewLoadIssues: [], + pendingApprovals: [], + sourceIssues: [], + sourceWork: EMPTY_SOURCE_WORK_GROUPS, summary: { - object: "dataset_summary", - record_count: 48_120, - connector_count: 10, - stream_count: 24, - total_retained_bytes: 0, blob_bytes: 0, - record_json_bytes: 0, - record_changes_json_bytes: 0, - earliest_record_time: null, - latest_record_time: null, + connector_count: 10, earliest_ingested_at: null, + earliest_record_time: null, latest_ingested_at: null, - top_connectors: [], + latest_record_time: null, + object: "dataset_summary", projection: { state: "fresh" }, + record_changes_json_bytes: 0, + record_count: 48_120, + record_json_bytes: 0, + stream_count: 24, + top_connectors: [], + total_retained_bytes: 0, }, - bearerClients: [], - grants: [], traces: [], - pendingApprovals: [], - failedTraces: [], - failedRuns: [], - sourceWork: EMPTY_SOURCE_WORK_GROUPS, - advisoryOwnerActions: [], - attentionConnections: [], - overviewLoadIssues: [], - sourceIssues: [], ...over, }; } @@ -141,18 +141,18 @@ test("grantEndorseStatus collapses the live vocab to active", () => { }); test("grantReads humanizes kinds, falls back to connector, then generic", () => { - const withKinds = { kinds: ["pay_statements", "transactions"], connector_id: "plaid" } as GrantSummary; + const withKinds = { connector_id: "plaid", kinds: ["pay_statements", "transactions"] } as GrantSummary; assert.equal(grantReads(withKinds), "reads only your pay and your spending"); - const onlyConnector = { kinds: ["read"], connector_id: "employment" } as GrantSummary; + const onlyConnector = { connector_id: "employment", kinds: ["read"] } as GrantSummary; assert.equal(grantReads(onlyConnector), "reads only your employment history"); - const nothing = { kinds: [], connector_id: null } as unknown as GrantSummary; + const nothing = { connector_id: null, kinds: [] } as unknown as GrantSummary; assert.equal(grantReads(nothing), "reads a scoped slice of your data"); }); test("grantReads humanizes protocol audit stream names instead of raw event ids", () => { const auditGrant = { - kinds: ["consent.approved", "token.issued", "query.received", "disclosure.served", "query.rejected"], connector_id: "pdpp", + kinds: ["consent.approved", "token.issued", "query.received", "disclosure.served", "query.rejected"], } as GrantSummary; assert.equal( @@ -175,12 +175,12 @@ test("relDay produces calm relative labels", () => { test("hero is DECIDE when an approval is pending", () => { const pending: PendingApproval = { - object: "approval", approval_id: "a1", client_id: "Atlas Mortgage", created_at: NOW.toISOString(), - kind: "consent", grant_preview: { streams: [{ name: "pay_statements" }, { name: "transactions" }] }, + kind: "consent", + object: "approval", }; const hero = computeHero(baseInputs({ pendingApprovals: [pending] })); assert.equal(hero.tone, "decide"); @@ -196,12 +196,12 @@ test("hero ALARM for a DEVICE-LOCAL recovery: CTA NAVIGATES (does not restate th baseInputs({ attentionConnections: [ { + actionLabel: "Recover local collector uploads", connectorKey: "claude-code", - routeId: "ci_laptop", deviceLocal: true, label: "laptop Claude Code", + routeId: "ci_laptop", what: "The local collector has saved records on its host that did not upload to this server.", - actionLabel: "Recover local collector uploads", }, ], }) @@ -224,12 +224,12 @@ test("hero ALARM for a DASHBOARD-ACTIONABLE recovery keeps its action verb", () baseInputs({ attentionConnections: [ { + actionLabel: "Reconnect", connectorKey: "chase", - routeId: "cin_chase", deviceLocal: false, label: "Chase - Personal", + routeId: "cin_chase", what: "Reconnect Chase.", - actionLabel: "Reconnect", }, ], }) @@ -242,20 +242,20 @@ test("hero ALARM with several attention connections → CTA routes to the syncs baseInputs({ attentionConnections: [ { + actionLabel: "Check the collector", connectorKey: "claude-code", - routeId: "ci_laptop", deviceLocal: true, label: "laptop Claude Code", + routeId: "ci_laptop", what: "Check the collector.", - actionLabel: "Check the collector", }, { + actionLabel: "Reconnect", connectorKey: "chase", - routeId: "cin_chase", deviceLocal: false, label: "Chase - Personal", + routeId: "cin_chase", what: "Reconnect Chase.", - actionLabel: "Reconnect", }, ], }) @@ -268,29 +268,29 @@ test("hero ALARM with several attention connections → CTA routes to the syncs test("failed syncs/traces alone do NOT drive the alarm — only the rendered-verdict attention set does", () => { // The old bug: a failed YNAB trace inflated the alarm while YNAB was healthy. - const run = { run_id: "r1", connector_id: "current_activity", failure_reason: "expired" } as RunSummary; - const trace = { trace_id: "t1", client_id: "ynab" } as TraceSummary; - const hero = computeHero(baseInputs({ failedRuns: [run], failedTraces: [trace], attentionConnections: [] })); + const run = { connector_id: "current_activity", failure_reason: "expired", run_id: "r1" } as RunSummary; + const trace = { client_id: "ynab", trace_id: "t1" } as TraceSummary; + const hero = computeHero(baseInputs({ attentionConnections: [], failedRuns: [run], failedTraces: [trace] })); assert.notEqual(hero.tone, "alarm"); }); test("decide wins over alarm", () => { const pending = { - object: "approval", approval_id: "a1", created_at: NOW.toISOString(), kind: "consent", + object: "approval", } as PendingApproval; const both = computeHero( baseInputs({ attentionConnections: [ { + actionLabel: "Reconnect", connectorKey: "chase", - routeId: "cin_chase", deviceLocal: false, label: "Chase - Personal", + routeId: "cin_chase", what: "x", - actionLabel: "Reconnect", }, ], pendingApprovals: [pending], @@ -343,18 +343,17 @@ function verdict( over: Partial> ): RefConnectorSummary["rendered_verdict"] { return { + annotations: [], channel: "attention", - pill: { label: "Can't collect", tone: "red" }, + detail: undefined, forward_statement: "Check the collector before this source can make progress.", + pill: { label: "Can't collect", tone: "red" }, required_actions: [ { affects: [], audience: "owner", cta: "Check the collector", kind: "add_info", - satisfied_when: { kind: "attention_resolved" }, - terminal: false, - urgency: "now", remediation: { cause: "dead_letter_backlog", commands: [], @@ -363,17 +362,18 @@ function verdict( summary: "The local collector has saved records on its host that did not upload to this server.", target: { identity_source: "source_instance_bindings", kind: "local_device" }, }, + satisfied_when: { kind: "attention_resolved" }, + terminal: false, + urgency: "now", }, ], - annotations: [], - detail: undefined, ...over, } as RefConnectorSummary["rendered_verdict"]; } test("attention truth: only attention-channel connections with an owner-satisfiable action count", () => { const connectors: RefConnectorSummary[] = [ - connector({ connector_id: "claude-code", connection_id: "cin_laptop", rendered_verdict: verdict({}) }), // ✓ attention + owner action + connector({ connection_id: "cin_laptop", connector_id: "claude-code", rendered_verdict: verdict({}) }), // ✓ attention + owner action connector({ connector_id: "calm-source", rendered_verdict: verdict({ channel: "calm" }) }), // ✗ calm connector({ connector_id: "ynab", rendered_verdict: null }), // ✗ no verdict (e.g. healthy) connector({ @@ -392,7 +392,7 @@ test("attention truth: only attention-channel connections with an owner-satisfia ], }), }), // ✗ attention but no owner-satisfiable action (S1 — code_fix is the maintainer's, not the owner's) - connector({ connector_id: "revoked", revoked_at: "2026-06-01T00:00:00Z", rendered_verdict: verdict({}) }), // ✗ revoked + connector({ connector_id: "revoked", rendered_verdict: verdict({}), revoked_at: "2026-06-01T00:00:00Z" }), // ✗ revoked ]; const attention = attentionConnectionsFromConnectors(connectors); assert.deepEqual( @@ -409,9 +409,9 @@ test("attention truth: only attention-channel connections with an owner-satisfia test("attention truth falls back to legacy blocked health when rendered verdict is absent", () => { const connectors: RefConnectorSummary[] = [ connector({ - connector_id: "chase", - connection_id: "cin_chase", connection_health: legacyHealth("blocked"), + connection_id: "cin_chase", + connector_id: "chase", display_name: "Chase", rendered_verdict: null, }), @@ -430,13 +430,13 @@ test("attention truth falls back to legacy blocked health when rendered verdict test("source issues show non-owner material verdicts without alarming as owner attention", () => { const connectors: RefConnectorSummary[] = [ connector({ - connector_id: "chase", connection_id: "cin_chase", + connector_id: "chase", display_name: "Chase", rendered_verdict: verdict({ channel: "advisory", - pill: { label: "Can't collect", tone: "red" }, forward_statement: "This connector needs a code fix before it can collect again.", + pill: { label: "Can't collect", tone: "red" }, required_actions: [ { affects: [], @@ -456,8 +456,8 @@ test("source issues show non-owner material verdicts without alarming as owner a }), connector({ connector_id: "revoked", - revoked_at: "2026-06-01T00:00:00Z", rendered_verdict: verdict({ pill: { label: "Can't collect", tone: "red" } }), + revoked_at: "2026-06-01T00:00:00Z", }), ]; @@ -479,13 +479,13 @@ test("source issues show non-owner material verdicts without alarming as owner a test("advisory owner actions surface non-urgent Amazon retry work without calm all-clear copy", () => { const connectors: RefConnectorSummary[] = [ connector({ - connector_id: "amazon", connection_id: "cin_amazon", + connector_id: "amazon", display_name: "Amazon - Personal", rendered_verdict: verdict({ channel: "advisory", - pill: { label: "Degraded", tone: "amber" }, forward_statement: "Some order detail is still outstanding. Retry this source to collect the missing detail.", + pill: { label: "Degraded", tone: "amber" }, required_actions: [ { affects: ["orders"], @@ -525,13 +525,13 @@ test("advisory owner actions surface non-urgent Amazon retry work without calm a test("advisory owner actions surface Reddit refresh work in the home summary", () => { const connectors: RefConnectorSummary[] = [ connector({ - connector_id: "reddit", connection_id: "cin_reddit", + connector_id: "reddit", display_name: "Reddit", rendered_verdict: verdict({ channel: "advisory", - pill: { label: "Healthy", tone: "green" }, forward_statement: "Run a refresh when you want the latest saved posts.", + pill: { label: "Healthy", tone: "green" }, required_actions: [ { affects: [], @@ -558,13 +558,13 @@ test("advisory owner actions surface Reddit refresh work in the home summary", ( test("source actionability groups live-shaped rows with scoped counts", () => { const connectors: RefConnectorSummary[] = [ connector({ - connector_id: "chatgpt", connection_id: "cin_chatgpt", + connector_id: "chatgpt", display_name: "ChatGPT - personal", rendered_verdict: verdict({ channel: "attention", - pill: { label: "Can't collect", tone: "red" }, forward_statement: "Reconnect this account and collection resumes.", + pill: { label: "Can't collect", tone: "red" }, required_actions: [ { affects: [], @@ -579,13 +579,13 @@ test("source actionability groups live-shaped rows with scoped counts", () => { }), }), connector({ - connector_id: "usaa", connection_id: "cin_usaa", + connector_id: "usaa", display_name: "USAA - Personal", rendered_verdict: verdict({ channel: "attention", - pill: { label: "Can't collect", tone: "red" }, forward_statement: "Reconnect this account and collection resumes.", + pill: { label: "Can't collect", tone: "red" }, required_actions: [ { affects: [], @@ -600,19 +600,19 @@ test("source actionability groups live-shaped rows with scoped counts", () => { }), }), connector({ - connector_id: "claude-code", connection_id: "cin_claude", + connector_id: "claude-code", display_name: "Local Claude Code", rendered_verdict: verdict({}), }), connector({ - connector_id: "amazon", connection_id: "cin_amazon", + connector_id: "amazon", display_name: "Amazon - Personal", rendered_verdict: verdict({ channel: "advisory", - pill: { label: "Degraded", tone: "amber" }, forward_statement: "Run a refresh to bring this up to date.", + pill: { label: "Degraded", tone: "amber" }, required_actions: [ { affects: [], @@ -627,13 +627,13 @@ test("source actionability groups live-shaped rows with scoped counts", () => { }), }), connector({ - connector_id: "chase", connection_id: "cin_chase", + connector_id: "chase", display_name: "Chase - Personal", rendered_verdict: verdict({ channel: "advisory", - pill: { label: "Degraded", tone: "amber" }, forward_statement: "Latest collection completed with known coverage gaps.", + pill: { label: "Degraded", tone: "amber" }, required_actions: [ { affects: [], @@ -648,13 +648,13 @@ test("source actionability groups live-shaped rows with scoped counts", () => { }), }), connector({ - connector_id: "github", connection_id: "cin_github", + connector_id: "github", display_name: "GitHub - Personal", rendered_verdict: verdict({ channel: "calm", - pill: { label: "Not measured", tone: "grey" }, forward_statement: "Coverage has not been measured yet.", + pill: { label: "Not measured", tone: "grey" }, required_actions: [], }), }), @@ -694,8 +694,8 @@ const DASHBOARD_PASSIVE_RECOVERY_RE = /catching up|syncing details|safe to retry function deferredRecoveryVerdict(): RefConnectorSummary["rendered_verdict"] { return verdict({ channel: "calm", - pill: { label: "Degraded", tone: "amber" }, forward_statement: "The next run is expected to fill the remaining data.", + pill: { label: "Degraded", tone: "amber" }, required_actions: [ { affects: [], @@ -732,13 +732,13 @@ function inactiveRecoveryHealth(): RefConnectorSummary["connection_health"] { test("dashboard cross-surface: every source-work section count equals its rendered row count", () => { const connectors: RefConnectorSummary[] = [ connector({ - connector_id: "chatgpt", connection_id: "cin_chatgpt", + connector_id: "chatgpt", display_name: "ChatGPT - personal", rendered_verdict: verdict({ channel: "attention", - pill: { label: "Can't collect", tone: "red" }, forward_statement: "Reconnect this account and collection resumes.", + pill: { label: "Can't collect", tone: "red" }, required_actions: [ { affects: [], @@ -753,21 +753,21 @@ test("dashboard cross-surface: every source-work section count equals its render }), }), connector({ - connector_id: "amazon", - connection_id: "cin_amazon", - display_name: "Amazon - Personal", // Durable, inactive recovery backlog → passive progress ("PDPP is working"). connection_health: inactiveRecoveryHealth(), + connection_id: "cin_amazon", + connector_id: "amazon", + display_name: "Amazon - Personal", rendered_verdict: deferredRecoveryVerdict(), }), connector({ - connector_id: "reddit", connection_id: "cin_reddit", + connector_id: "reddit", display_name: "Reddit - Personal", rendered_verdict: verdict({ channel: "advisory", - pill: { label: "Degraded", tone: "amber" }, forward_statement: "Run a refresh to bring this up to date.", + pill: { label: "Degraded", tone: "amber" }, required_actions: [ { affects: [], @@ -782,13 +782,13 @@ test("dashboard cross-surface: every source-work section count equals its render }), }), connector({ - connector_id: "chase", connection_id: "cin_chase", + connector_id: "chase", display_name: "Chase - Personal", rendered_verdict: verdict({ channel: "advisory", - pill: { label: "Can't collect", tone: "red" }, forward_statement: "Connector code needs a fix before this can collect again.", + pill: { label: "Can't collect", tone: "red" }, required_actions: [ { affects: [], @@ -831,10 +831,10 @@ test("dashboard cross-surface: every source-work section count equals its render test("dashboard cross-surface: an inactive queued recovery row is passive progress, never a Checking or degraded row", () => { const connectors: RefConnectorSummary[] = [ connector({ - connector_id: "amazon", + connection_health: inactiveRecoveryHealth(), connection_id: "cin_amazon", + connector_id: "amazon", display_name: "Amazon - Personal", - connection_health: inactiveRecoveryHealth(), rendered_verdict: deferredRecoveryVerdict(), }), ]; @@ -859,13 +859,13 @@ test("dashboard cross-surface: an inactive queued recovery row is passive progre test("reviewable degraded source appears once rather than as review plus source issue", () => { const connectors: RefConnectorSummary[] = [ connector({ - connector_id: "amazon", connection_id: "cin_amazon", + connector_id: "amazon", display_name: "Amazon - Personal", rendered_verdict: verdict({ channel: "advisory", - pill: { label: "Degraded", tone: "amber" }, forward_statement: "Retry now to give the recoverable gap another run.", + pill: { label: "Degraded", tone: "amber" }, required_actions: [ { affects: [], @@ -894,13 +894,13 @@ test("reviewable degraded source appears once rather than as review plus source test("source actionability follows primary-action parity with push policy", () => { const connectors: RefConnectorSummary[] = [ connector({ - connector_id: "mixed", connection_id: "cin_mixed", + connector_id: "mixed", display_name: "Mixed-action source", rendered_verdict: verdict({ channel: "attention", - pill: { label: "Can't collect", tone: "red" }, forward_statement: "Connector code needs a fix before this can collect again.", + pill: { label: "Can't collect", tone: "red" }, required_actions: [ { affects: [], @@ -935,13 +935,13 @@ test("source actionability follows primary-action parity with push policy", () = test("maintainer-only actions are not advisory owner actions", () => { const connectors: RefConnectorSummary[] = [ connector({ - connector_id: "maintainer-only", connection_id: "cin_maintainer", + connector_id: "maintainer-only", display_name: "Maintainer-only source", rendered_verdict: verdict({ channel: "advisory", - pill: { label: "Degraded", tone: "amber" }, forward_statement: "This source needs a connector code fix before it can make progress.", + pill: { label: "Degraded", tone: "amber" }, required_actions: [ { affects: [], @@ -965,13 +965,13 @@ test("maintainer-only actions are not advisory owner actions", () => { test("source issues omit healthy advisory refresh hints", () => { const connectors: RefConnectorSummary[] = [ connector({ - connector_id: "reddit", connection_id: "cin_reddit", + connector_id: "reddit", display_name: "Reddit - dondochaka", rendered_verdict: verdict({ channel: "advisory", - pill: { label: "Healthy", tone: "green" }, forward_statement: "Run a refresh to bring this up to date.", + pill: { label: "Healthy", tone: "green" }, required_actions: [ { affects: [], @@ -998,13 +998,13 @@ test("source issues omit healthy advisory refresh hints", () => { test("source issues surface attention verdicts that have no owner action, even with a green pill", () => { const connectors: RefConnectorSummary[] = [ connector({ - connector_id: "maintainer-only", connection_id: "cin_maintainer", + connector_id: "maintainer-only", display_name: "Maintainer-only source", rendered_verdict: verdict({ channel: "attention", - pill: { label: "Healthy", tone: "green" }, forward_statement: "This source needs a maintainer action before it can make progress.", + pill: { label: "Healthy", tone: "green" }, required_actions: [ { affects: [], @@ -1033,9 +1033,9 @@ test("source issues surface attention verdicts that have no owner action, even w test("source issues fall back to legacy degraded health when rendered verdict is absent", () => { const connectors: RefConnectorSummary[] = [ connector({ - connector_id: "usaa", - connection_id: "cin_usaa", connection_health: legacyHealth("degraded"), + connection_id: "cin_usaa", + connector_id: "usaa", display_name: "USAA - Personal", rendered_verdict: null, }), @@ -1064,13 +1064,13 @@ test("attention routeId targets the EXACT connection instance, not the connector // the CTA lands on the connection that actually needs the owner. const connectors: RefConnectorSummary[] = [ connector({ - connector_id: "claude-code", connection_id: "cin_simon", + connector_id: "claude-code", rendered_verdict: verdict({ channel: "calm" }), }), // healthy, first connector({ - connector_id: "claude-code", connection_id: "cin_laptop", + connector_id: "claude-code", connector_instance_id: "ci_laptop", rendered_verdict: verdict({}), }), // the attention one @@ -1091,8 +1091,8 @@ test("hero ALARMs on a stale projection even with no failures", () => { stale.summary = { ...stale.summary, projection: { - state: "stale", last_error: "bulk write on unknown connection", + state: "stale", }, } as StandingInputs["summary"]; const hero = computeHero(stale); @@ -1113,8 +1113,8 @@ test("hero uses owner-safe copy for failed projection details", () => { failed.summary = { ...failed.summary, projection: { - state: "failed", last_error: "SQL failed: bulk write on unknown connection", + state: "failed", }, } as StandingInputs["summary"]; const hero = computeHero(failed); @@ -1146,7 +1146,7 @@ test("hero ALARMs when dashboard inputs fail instead of claiming all-clear from test("hero is CALM with reassurance when all is well", () => { const clients: OwnerIssuedClient[] = [ - { client_id: "c1", client_name: "Claude Desktop", active_token_count: 1, created_at: NOW.toISOString() }, + { active_token_count: 1, client_id: "c1", client_name: "Claude Desktop", created_at: NOW.toISOString() }, ]; const hero = computeHero(baseInputs({ bearerClients: clients })); assert.equal(hero.tone, "calm"); @@ -1156,8 +1156,8 @@ test("hero is CALM with reassurance when all is well", () => { test("bearer section and hero count only active owner tokens", () => { const clients: OwnerIssuedClient[] = [ - { client_id: "inactive", client_name: "Old smoke client", active_token_count: 0, created_at: NOW.toISOString() }, - { client_id: "active", client_name: "Claude Desktop", active_token_count: 2, created_at: NOW.toISOString() }, + { active_token_count: 0, client_id: "inactive", client_name: "Old smoke client", created_at: NOW.toISOString() }, + { active_token_count: 2, client_id: "active", client_name: "Claude Desktop", created_at: NOW.toISOString() }, ]; const data = buildStandingData(baseInputs({ bearerClients: clients })); @@ -1169,7 +1169,7 @@ test("bearer section and hero count only active owner tokens", () => { test("hero says no owner token can act when all issued clients are inactive", () => { const clients: OwnerIssuedClient[] = [ - { client_id: "inactive", client_name: "Old smoke client", active_token_count: 0, created_at: NOW.toISOString() }, + { active_token_count: 0, client_id: "inactive", client_name: "Old smoke client", created_at: NOW.toISOString() }, ]; const data = buildStandingData(baseInputs({ bearerClients: clients })); @@ -1181,7 +1181,7 @@ test("hero says no owner token can act when all issued clients are inactive", () test("bearer row carries the raw created_at for the shared IcTimestamp, not a prebaked date string", () => { const clients: OwnerIssuedClient[] = [ - { client_id: "c1", client_name: "Claude Desktop", active_token_count: 1, created_at: "2026-06-01T00:00:00Z" }, + { active_token_count: 1, client_id: "c1", client_name: "Claude Desktop", created_at: "2026-06-01T00:00:00Z" }, ]; const data = buildStandingData(baseInputs({ bearerClients: clients })); @@ -1197,10 +1197,10 @@ test("bearer row carries the raw created_at for the shared IcTimestamp, not a pr test('single-token bearer labels created_at "issued"; multi-token bearer degrades to "first issued"', () => { const single: OwnerIssuedClient[] = [ - { client_id: "one", client_name: "Solo", active_token_count: 1, created_at: NOW.toISOString() }, + { active_token_count: 1, client_id: "one", client_name: "Solo", created_at: NOW.toISOString() }, ]; const multi: OwnerIssuedClient[] = [ - { client_id: "many", client_name: "Legacy", active_token_count: 3, created_at: NOW.toISOString() }, + { active_token_count: 3, client_id: "many", client_name: "Legacy", created_at: NOW.toISOString() }, ]; const singleData = buildStandingData(baseInputs({ bearerClients: single })); @@ -1216,9 +1216,9 @@ test('single-token bearer labels created_at "issued"; multi-token bearer degrade test("bearer block previews the most-recent few and reports the hidden overflow count", () => { const clients: OwnerIssuedClient[] = Array.from({ length: 7 }, (_, i) => ({ + active_token_count: 1, client_id: `c${i}`, client_name: `Client ${i}`, - active_token_count: 1, created_at: NOW.toISOString(), })); const data = buildStandingData(baseInputs({ bearerClients: clients })); @@ -1231,7 +1231,7 @@ test("bearer block previews the most-recent few and reports the hidden overflow test("bearer overflow is zero when active bearers fit within the preview cap", () => { const clients: OwnerIssuedClient[] = [ - { client_id: "c1", client_name: "Claude Desktop", active_token_count: 1, created_at: NOW.toISOString() }, + { active_token_count: 1, client_id: "c1", client_name: "Claude Desktop", created_at: NOW.toISOString() }, ]; const data = buildStandingData(baseInputs({ bearerClients: clients })); @@ -1244,43 +1244,43 @@ test("bearer overflow is zero when active bearers fit within the preview cap", ( test("grant packages are surfaced from loaded grants without a count endpoint", () => { const grants: GrantSummary[] = [ { - object: "grant_summary", - grant_id: "g1", client_id: "App A", connector_id: "pdpp", - status: "active", - first_at: "2026-06-01T00:00:00Z", - last_at: "2026-06-12T00:00:00Z", event_count: 5, - kinds: ["query.received"], failure: null, + first_at: "2026-06-01T00:00:00Z", + grant_id: "g1", grant_package_id: "pkg_alpha", + kinds: ["query.received"], + last_at: "2026-06-12T00:00:00Z", + object: "grant_summary", + status: "active", }, { - object: "grant_summary", - grant_id: "g2", client_id: "App A", connector_id: "pdpp", - status: "active", - first_at: "2026-06-01T00:00:00Z", - last_at: "2026-06-12T00:00:00Z", event_count: 3, - kinds: ["query.received"], failure: null, + first_at: "2026-06-01T00:00:00Z", + grant_id: "g2", grant_package_id: "pkg_alpha", + kinds: ["query.received"], + last_at: "2026-06-12T00:00:00Z", + object: "grant_summary", + status: "active", }, { - object: "grant_summary", - grant_id: "g3", client_id: "App B", connector_id: "pdpp", - status: "active", - first_at: "2026-06-01T00:00:00Z", - last_at: "2026-06-12T00:00:00Z", event_count: 1, - kinds: ["query.received"], failure: null, + first_at: "2026-06-01T00:00:00Z", + grant_id: "g3", grant_package_id: "pkg_beta", + kinds: ["query.received"], + last_at: "2026-06-12T00:00:00Z", + object: "grant_summary", + status: "active", }, ]; @@ -1300,20 +1300,20 @@ test("the authoritative grant-package count drives the overview badge when prese // The overview must trust the endpoint (exact), not the loaded-grants floor, // so packages not represented in the preview still surface. const grant: GrantSummary = { - object: "grant_summary", - grant_id: "g1", client_id: "App A", connector_id: "pdpp", - status: "active", - first_at: "2026-06-01T00:00:00Z", - last_at: "2026-06-12T00:00:00Z", event_count: 5, - kinds: ["query.received"], failure: null, + first_at: "2026-06-01T00:00:00Z", + grant_id: "g1", grant_package_id: "pkg_alpha", + kinds: ["query.received"], + last_at: "2026-06-12T00:00:00Z", + object: "grant_summary", + status: "active", }; - const data = buildStandingData(baseInputs({ grants: [grant], grantPackageCount: 4 })); + const data = buildStandingData(baseInputs({ grantPackageCount: 4, grants: [grant] })); assert.equal(data.grantPackages?.count, 4); assert.equal(data.grantPackages?.exact, true); assert.equal(data.grantPackages?.href, HREFS.grantPackages); @@ -1321,55 +1321,55 @@ test("the authoritative grant-package count drives the overview badge when prese test("an authoritative zero grant-package count collapses the badge even if a stale grant carries a package id", () => { const grant: GrantSummary = { - object: "grant_summary", - grant_id: "g1", client_id: "App A", connector_id: "pdpp", - status: "active", - first_at: "2026-06-01T00:00:00Z", - last_at: "2026-06-12T00:00:00Z", event_count: 5, - kinds: ["query.received"], failure: null, + first_at: "2026-06-01T00:00:00Z", + grant_id: "g1", grant_package_id: "pkg_alpha", + kinds: ["query.received"], + last_at: "2026-06-12T00:00:00Z", + object: "grant_summary", + status: "active", }; - const data = buildStandingData(baseInputs({ grants: [grant], grantPackageCount: 0 })); + const data = buildStandingData(baseInputs({ grantPackageCount: 0, grants: [grant] })); assert.equal(data.grantPackages, null); }); test("a null grant-package count falls back to the loaded-grants floor", () => { const grant: GrantSummary = { - object: "grant_summary", - grant_id: "g1", client_id: "App A", connector_id: "pdpp", - status: "active", - first_at: "2026-06-01T00:00:00Z", - last_at: "2026-06-12T00:00:00Z", event_count: 5, - kinds: ["query.received"], failure: null, + first_at: "2026-06-01T00:00:00Z", + grant_id: "g1", grant_package_id: "pkg_alpha", + kinds: ["query.received"], + last_at: "2026-06-12T00:00:00Z", + object: "grant_summary", + status: "active", }; - const data = buildStandingData(baseInputs({ grants: [grant], grantPackageCount: null })); + const data = buildStandingData(baseInputs({ grantPackageCount: null, grants: [grant] })); assert.equal(data.grantPackages?.count, 1); assert.equal(data.grantPackages?.exact, false); }); test("grant packages are absent when no loaded grant carries a package id", () => { const grant: GrantSummary = { - object: "grant_summary", - grant_id: "g1", client_id: "App A", connector_id: "pdpp", - status: "active", - first_at: "2026-06-01T00:00:00Z", - last_at: "2026-06-12T00:00:00Z", event_count: 5, - kinds: ["query.received"], failure: null, + first_at: "2026-06-01T00:00:00Z", + grant_id: "g1", + kinds: ["query.received"], + last_at: "2026-06-12T00:00:00Z", + object: "grant_summary", + status: "active", }; const data = buildStandingData(baseInputs({ grants: [grant] })); @@ -1378,17 +1378,17 @@ test("grant packages are absent when no loaded grant carries a package id", () = test("a fully-revoked package does not advertise from the overview", () => { const grant: GrantSummary = { - object: "grant_summary", - grant_id: "g1", client_id: "App A", connector_id: "pdpp", - status: "revoked", - first_at: "2026-06-01T00:00:00Z", - last_at: "2026-06-12T00:00:00Z", event_count: 5, - kinds: ["query.received"], failure: null, + first_at: "2026-06-01T00:00:00Z", + grant_id: "g1", grant_package_id: "pkg_dead", + kinds: ["query.received"], + last_at: "2026-06-12T00:00:00Z", + object: "grant_summary", + status: "revoked", }; const data = buildStandingData(baseInputs({ grants: [grant] })); @@ -1399,35 +1399,35 @@ test("a fully-revoked package does not advertise from the overview", () => { test("buildStandingData wires bearers, relationships, lately, attention", () => { const clients: OwnerIssuedClient[] = [ - { client_id: "c1", client_name: "Claude Desktop", active_token_count: 2, created_at: "2026-06-01T00:00:00Z" }, + { active_token_count: 2, client_id: "c1", client_name: "Claude Desktop", created_at: "2026-06-01T00:00:00Z" }, ]; const grant: GrantSummary = { - object: "grant_summary", - grant_id: "g1", client_id: "Atlas Mortgage", connector_id: "plaid", - status: "active", - first_at: "2026-06-01T00:00:00Z", - last_at: "2026-06-12T00:00:00Z", event_count: 5, - kinds: ["pay_statements"], failure: null, + first_at: "2026-06-01T00:00:00Z", + grant_id: "g1", + kinds: ["pay_statements"], + last_at: "2026-06-12T00:00:00Z", + object: "grant_summary", + status: "active", }; const readTrace: TraceSummary = { - object: "trace_summary", - trace_id: "t1", - status: "succeeded", actor_id: "Claude Desktop", actor_type: "client", client_id: "Claude Desktop", - grant_id: null, - run_id: null, - request_id: null, - first_at: "2026-06-13T00:00:00Z", - last_at: "2026-06-13T00:00:00Z", event_count: 412, - kinds: ["transactions"], failure: null, + first_at: "2026-06-13T00:00:00Z", + grant_id: null, + kinds: ["transactions"], + last_at: "2026-06-13T00:00:00Z", + object: "trace_summary", + request_id: null, + run_id: null, + status: "succeeded", + trace_id: "t1", }; const data = buildStandingData(baseInputs({ bearerClients: clients, grants: [grant], traces: [readTrace] })); assert.equal(data.bearers.length, 1); @@ -1444,25 +1444,25 @@ test("buildStandingData wires bearers, relationships, lately, attention", () => test("lately uses trace client metadata instead of raw client ids", () => { const trace: TraceSummary = { - object: "trace_summary", - trace_id: "trc_named", - status: "succeeded", actor_id: "client", actor_type: "client", - client_id: "cli_named", client: { client_id: "cli_named", client_name: "Claude", registration_mode: "dynamic", }, - grant_id: null, - run_id: null, - request_id: null, - first_at: "2026-06-13T00:00:00Z", - last_at: "2026-06-13T00:00:00Z", + client_id: "cli_named", event_count: 3, - kinds: ["query.received"], failure: null, + first_at: "2026-06-13T00:00:00Z", + grant_id: null, + kinds: ["query.received"], + last_at: "2026-06-13T00:00:00Z", + object: "trace_summary", + request_id: null, + run_id: null, + status: "succeeded", + trace_id: "trc_named", }; const data = buildStandingData(baseInputs({ traces: [trace] })); @@ -1474,23 +1474,23 @@ test("lately uses trace client metadata instead of raw client ids", () => { test("lately humanizes live denial reason codes instead of rendering raw diagnostics", () => { const trace: TraceSummary = { - object: "trace_summary", - trace_id: "trc_orphaned", - status: "denied", actor_id: "slack", actor_type: "client", client_id: "slack", - grant_id: null, - run_id: "run_orphaned", - request_id: null, - first_at: "2026-06-13T00:00:00Z", - last_at: "2026-06-13T00:00:00Z", event_count: 1, - kinds: ["query.rejected"], failure: { event_type: "run.started", reason: "orphaned_started_run", }, + first_at: "2026-06-13T00:00:00Z", + grant_id: null, + kinds: ["query.rejected"], + last_at: "2026-06-13T00:00:00Z", + object: "trace_summary", + request_id: null, + run_id: "run_orphaned", + status: "denied", + trace_id: "trc_orphaned", }; const data = buildStandingData(baseInputs({ traces: [trace] })); @@ -1502,23 +1502,23 @@ test("lately humanizes live denial reason codes instead of rendering raw diagnos test("lately does not fall through to unknown snake-case denial reasons", () => { const trace: TraceSummary = { - object: "trace_summary", - trace_id: "trc_unknown_denial", - status: "denied", actor_id: "client", actor_type: "client", client_id: "client", - grant_id: null, - run_id: null, - request_id: null, - first_at: "2026-06-13T00:00:00Z", - last_at: "2026-06-13T00:00:00Z", event_count: 1, - kinds: ["query.rejected"], failure: { event_type: "query.rejected", reason: "new_internal_reason_code", }, + first_at: "2026-06-13T00:00:00Z", + grant_id: null, + kinds: ["query.rejected"], + last_at: "2026-06-13T00:00:00Z", + object: "trace_summary", + request_id: null, + run_id: null, + status: "denied", + trace_id: "trc_unknown_denial", }; const data = buildStandingData(baseInputs({ traces: [trace] })); @@ -1531,42 +1531,42 @@ test("lately does not fall through to unknown snake-case denial reasons", () => test("lately does not overclaim off-surface expired or credential scope failures", () => { const traces: TraceSummary[] = [ { - object: "trace_summary", - trace_id: "trc_state_expired", - status: "denied", actor_id: "state_client", actor_type: "client", client_id: "state_client", - grant_id: null, - run_id: null, - request_id: null, - first_at: "2026-06-13T00:00:00Z", - last_at: "2026-06-13T00:00:00Z", event_count: 1, - kinds: ["query.rejected"], failure: { event_type: "query.rejected", reason: "state_expired", }, - }, - { + first_at: "2026-06-13T00:00:00Z", + grant_id: null, + kinds: ["query.rejected"], + last_at: "2026-06-13T00:00:00Z", object: "trace_summary", - trace_id: "trc_credential_scope", + request_id: null, + run_id: null, status: "denied", + trace_id: "trc_state_expired", + }, + { actor_id: "credential_client", actor_type: "client", client_id: "credential_client", - grant_id: null, - run_id: null, - request_id: null, - first_at: "2026-06-13T00:00:01Z", - last_at: "2026-06-13T00:00:01Z", event_count: 1, - kinds: ["query.rejected"], failure: { event_type: "query.rejected", reason: "github_credential_insufficient_scope", }, + first_at: "2026-06-13T00:00:01Z", + grant_id: null, + kinds: ["query.rejected"], + last_at: "2026-06-13T00:00:01Z", + object: "trace_summary", + request_id: null, + run_id: null, + status: "denied", + trace_id: "trc_credential_scope", }, ]; @@ -1580,20 +1580,20 @@ test("lately does not overclaim off-surface expired or credential scope failures test("lately does not bold raw technical client ids when metadata is missing", () => { const trace: TraceSummary = { - object: "trace_summary", - trace_id: "trc_raw", - status: "succeeded", actor_id: "cli_raw", actor_type: "client", client_id: "cli_raw", - grant_id: null, - run_id: null, - request_id: null, - first_at: "2026-06-13T00:00:00Z", - last_at: "2026-06-13T00:00:00Z", event_count: 3, - kinds: ["query.received"], failure: null, + first_at: "2026-06-13T00:00:00Z", + grant_id: null, + kinds: ["query.received"], + last_at: "2026-06-13T00:00:00Z", + object: "trace_summary", + request_id: null, + run_id: null, + status: "succeeded", + trace_id: "trc_raw", }; const data = buildStandingData(baseInputs({ traces: [trace] })); @@ -1606,20 +1606,20 @@ test("lately does not bold raw technical client ids when metadata is missing", ( test("lately does not render bare opaque client ids as names", () => { const opaqueClientId = "d9f1c1bb7a5c4a6f9e8d7c6b5a4f3210"; const trace: TraceSummary = { - object: "trace_summary", - trace_id: "trc_opaque", - status: "succeeded", actor_id: opaqueClientId, actor_type: "unknown", client_id: opaqueClientId, - grant_id: null, - run_id: null, - request_id: null, - first_at: "2026-06-13T00:00:00Z", - last_at: "2026-06-13T00:00:00Z", event_count: 3, - kinds: ["query.received"], failure: null, + first_at: "2026-06-13T00:00:00Z", + grant_id: null, + kinds: ["query.received"], + last_at: "2026-06-13T00:00:00Z", + object: "trace_summary", + request_id: null, + run_id: null, + status: "succeeded", + trace_id: "trc_opaque", }; const data = buildStandingData(baseInputs({ traces: [trace] })); @@ -1633,37 +1633,37 @@ test("lately summarizes identical recent reads instead of repeating the same row const repeated = Array.from( { length: 5 }, (_, i): TraceSummary => ({ - object: "trace_summary", - trace_id: `trc_longview_${i}`, - status: "succeeded", actor_id: "client", actor_type: "client", - client_id: "cli_longview", client: { client_id: "cli_longview", client_name: "Longview CLI", registration_mode: "pre_registered_public", }, - grant_id: null, - run_id: null, - request_id: null, - first_at: "2026-06-13T00:00:00Z", - last_at: "2026-06-13T00:00:00Z", + client_id: "cli_longview", event_count: 3, - kinds: ["query.received"], failure: null, + first_at: "2026-06-13T00:00:00Z", + grant_id: null, + kinds: ["query.received"], + last_at: "2026-06-13T00:00:00Z", + object: "trace_summary", + request_id: null, + run_id: null, + status: "succeeded", + trace_id: `trc_longview_${i}`, }) ); const baseRepeated = repeated[0]; assert.ok(baseRepeated); const different: TraceSummary = { ...baseRepeated, - trace_id: "trc_controller", actor_id: "controller", actor_type: "runtime", - client_id: null, client: undefined, + client_id: null, event_count: 1, + trace_id: "trc_controller", }; const data = buildStandingData(baseInputs({ traces: [...repeated, different] })); @@ -1678,28 +1678,28 @@ test("lately summarizes identical recent reads instead of repeating the same row test("relationships summarize grants by client instead of repeating one row per grant", () => { const grants: GrantSummary[] = [ { - object: "grant_summary", - grant_id: "g1", client_id: "CLI agent", connector_id: "pdpp", - status: "active", - first_at: "2026-06-01T00:00:00Z", - last_at: "2026-06-10T00:00:00Z", event_count: 5, - kinds: ["token.issued", "query.received"], failure: null, + first_at: "2026-06-01T00:00:00Z", + grant_id: "g1", + kinds: ["token.issued", "query.received"], + last_at: "2026-06-10T00:00:00Z", + object: "grant_summary", + status: "active", }, { - object: "grant_summary", - grant_id: "g2", client_id: "CLI agent", connector_id: "pdpp", - status: "active", - first_at: "2026-06-02T00:00:00Z", - last_at: "2026-06-12T12:00:00Z", event_count: 7, - kinds: ["disclosure.served", "query.rejected"], failure: null, + first_at: "2026-06-02T00:00:00Z", + grant_id: "g2", + kinds: ["disclosure.served", "query.rejected"], + last_at: "2026-06-12T12:00:00Z", + object: "grant_summary", + status: "active", }, ]; @@ -1719,16 +1719,16 @@ test("relationships summarize grants by client instead of repeating one row per test("relationships use known client names without replacing the verified client id", () => { const grants: GrantSummary[] = [ { - object: "grant_summary", - grant_id: "g1", client_id: "cli_known", connector_id: "pdpp", - status: "active", - first_at: "2026-06-01T00:00:00Z", - last_at: "2026-06-12T12:00:00Z", event_count: 7, - kinds: ["query.received"], failure: null, + first_at: "2026-06-01T00:00:00Z", + grant_id: "g1", + kinds: ["query.received"], + last_at: "2026-06-12T12:00:00Z", + object: "grant_summary", + status: "active", }, ]; const clients: OwnerIssuedClient[] = [ @@ -1751,21 +1751,21 @@ test("relationships use known client names without replacing the verified client test("relationships prefer grant client metadata over owner-token labels", () => { const grants: GrantSummary[] = [ { - object: "grant_summary", - grant_id: "g1", - client_id: "cli_known", client: { client_id: "cli_known", client_name: "Claude Code", registration_mode: "dynamic", }, + client_id: "cli_known", connector_id: "pdpp", - status: "active", - first_at: "2026-06-01T00:00:00Z", - last_at: "2026-06-12T12:00:00Z", event_count: 7, - kinds: ["query.received"], failure: null, + first_at: "2026-06-01T00:00:00Z", + grant_id: "g1", + kinds: ["query.received"], + last_at: "2026-06-12T12:00:00Z", + object: "grant_summary", + status: "active", }, ]; const clients: OwnerIssuedClient[] = [ @@ -1788,16 +1788,16 @@ test("relationships do not render raw URL client ids as owner-facing names", () const urlClientId = "https://chatgpt.com/oauth/Dyp26IIu2iQg/client.json?token_endpoint_auth_method=none"; const grants: GrantSummary[] = [ { - object: "grant_summary", - grant_id: "g1", client_id: urlClientId, connector_id: "pdpp", - status: "active", - first_at: "2026-06-01T00:00:00Z", - last_at: "2026-06-12T12:00:00Z", event_count: 7, - kinds: ["query.received"], failure: null, + first_at: "2026-06-01T00:00:00Z", + grant_id: "g1", + kinds: ["query.received"], + last_at: "2026-06-12T12:00:00Z", + object: "grant_summary", + status: "active", }, ]; @@ -1813,16 +1813,16 @@ test("relationships do not render bare UUID or opaque client ids as owner-facing for (const clientId of ["0b643449-9516-45e0-b375-7feb9ecb7a58", "d9f1c1bb7a5c4a6f9e8d7c6b5a4f3210"]) { const grants: GrantSummary[] = [ { - object: "grant_summary", - grant_id: "g1", client_id: clientId, connector_id: "pdpp", - status: "active", - first_at: "2026-06-01T00:00:00Z", - last_at: "2026-06-12T12:00:00Z", event_count: 7, - kinds: ["query.received"], failure: null, + first_at: "2026-06-01T00:00:00Z", + grant_id: "g1", + kinds: ["query.received"], + last_at: "2026-06-12T12:00:00Z", + object: "grant_summary", + status: "active", }, ]; @@ -1837,16 +1837,16 @@ test("relationships do not render bare UUID or opaque client ids as owner-facing test("revoked grants are excluded from relationships", () => { const revoked: GrantSummary = { - object: "grant_summary", - grant_id: "g2", client_id: "Old App", connector_id: null, - status: "revoked", - first_at: "2026-05-01T00:00:00Z", - last_at: "2026-05-02T00:00:00Z", event_count: 0, - kinds: [], failure: null, + first_at: "2026-05-01T00:00:00Z", + grant_id: "g2", + kinds: [], + last_at: "2026-05-02T00:00:00Z", + object: "grant_summary", + status: "revoked", }; const data = buildStandingData(baseInputs({ grants: [revoked] })); assert.equal(data.relationships.length, 0); diff --git a/apps/console/src/app/(console)/components/views/standing-view-model.ts b/apps/console/src/app/(console)/components/views/standing-view-model.ts index f8deaa706..05018560d 100644 --- a/apps/console/src/app/(console)/components/views/standing-view-model.ts +++ b/apps/console/src/app/(console)/components/views/standing-view-model.ts @@ -41,31 +41,31 @@ import { // back to its own prettified name. We never invent a meaning we can't justify. const SCOPE_HUMAN: Record = { - pay_statements: "your pay", - paystubs: "your pay", - income: "your pay", - employment: "your employment history", - listening_history: "what you listen to", - watch_history: "what you watch", - transactions: "your spending", - current_activity: "your spending", - statements: "your statements", - tax_docs: "your tax documents", - tax_documents: "your tax documents", - browsing: "your browsing", browser_history: "your browsing", - messages: "your messages", + browsing: "your browsing", + "consent.approved": "grant decisions", conversations: "your conversations", + current_activity: "your spending", + "disclosure.served": "data disclosures", emails: "your email", - orders: "your orders", - purchases: "your purchases", + employment: "your employment history", health: "your health records", + income: "your pay", + listening_history: "what you listen to", location: "where you've been", - "consent.approved": "grant decisions", - "disclosure.served": "data disclosures", + messages: "your messages", + orders: "your orders", + pay_statements: "your pay", + paystubs: "your pay", + purchases: "your purchases", "query.received": "read requests", "query.rejected": "rejected reads", + statements: "your statements", + tax_docs: "your tax documents", + tax_documents: "your tax documents", "token.issued": "token activity", + transactions: "your spending", + watch_history: "what you watch", }; const READ_SUFFIX_RE = /\.read$/; @@ -100,15 +100,15 @@ export function joinHuman(parts: readonly string[]): string { } const STREAM_RECORD_NOUN: Record = { - pay_statements: "pay records", + current_activity: "transactions", + emails: "emails", employment: "employment records", listening_history: "listening records", - tax_docs: "tax records", - transactions: "transactions", - current_activity: "transactions", messages: "messages", - emails: "emails", orders: "orders", + pay_statements: "pay records", + tax_docs: "tax records", + transactions: "transactions", }; /** "transactions" / "pay records" — the plural noun for a stream of records. */ @@ -528,13 +528,13 @@ function toBearers(clients: OwnerIssuedClient[], hrefs: StandingHrefs): BearerVi const tokenWord = count === 1 ? "token" : "tokens"; return { clientId: c.client_id, - who: clientLabel(c.client_name, c.client_id), how: `owner token · ${count} active ${tokenWord}`, + issuedAt: c.created_at, // A client with more than one active token registered before the newest // token was issued, so `created_at` is a "first issued", not "issued". issuedLabel: count > 1 ? "first issued" : "issued", - issuedAt: c.created_at, revokeHref: hrefs.deploymentTokens, + who: clientLabel(c.client_name, c.client_id), }; }); } @@ -741,18 +741,18 @@ function toLately(traces: TraceSummary[], now: Date): LatelyView[] { let row: LatelyView; if (isDeny) { row = { + deny: true, id: tr.trace_id, + text: { rest: `tried to read — turned away, ${denyReason(tr.failure?.reason ?? null)}.`, who }, when: relDay(tr.last_at, now), - deny: true, - text: { who, rest: `tried to read — turned away, ${denyReason(tr.failure?.reason ?? null)}.` }, }; } else { const noun = tr.event_count === 1 ? "record" : "records"; row = { + deny: false, id: tr.trace_id, + text: { rest: `read ${fmtInt(tr.event_count)} ${noun}.`, who }, when: relDay(tr.last_at, now), - deny: false, - text: { who, rest: `read ${fmtInt(tr.event_count)} ${noun}.` }, }; } const key = `${row.deny ? "deny" : "read"}|${row.when}|${row.text.who}|${row.text.rest}`; @@ -829,19 +829,19 @@ export function advisoryOwnerActionsFromConnectors( function toAttention(attention: AttentionConnection[], hrefs: StandingHrefs): AttentionRowView[] { return attention.map((a) => ({ + href: hrefs.connection(a.routeId), id: `connection:${a.routeId}`, what: `${a.label} needs you`, why: a.what, - href: hrefs.connection(a.routeId), })); } function toSourceIssues(sourceIssues: SourceIssueConnection[], hrefs: StandingHrefs): AttentionRowView[] { return sourceIssues.map((issue) => ({ + href: hrefs.connection(issue.routeId), id: `source-issue:${issue.routeId}`, what: `${issue.label} ${issue.status}`, why: issue.what, - href: hrefs.connection(issue.routeId), })); } @@ -850,10 +850,10 @@ function toAdvisoryOwnerActions( hrefs: StandingHrefs ): AttentionRowView[] { return advisoryOwnerActions.map((action) => ({ + href: hrefs.connection(action.routeId), id: `advisory-owner-action:${action.routeId}`, what: `${action.label} has an action to review`, why: action.what, - href: hrefs.connection(action.routeId), })); } @@ -864,13 +864,13 @@ function toOverviewIssues(loadIssues: readonly string[], hrefs: StandingHrefs): const count = loadIssues.length; return [ { + href: hrefs.deployment, id: "overview:partial-data", what: "Overview could not check everything", why: count === 1 ? "One dashboard check did not load. Refresh this page; if it keeps happening, check deployment." : `${count} dashboard checks did not load. Refresh this page; if it keeps happening, check deployment.`, - href: hrefs.deployment, }, ]; } @@ -958,10 +958,10 @@ function sourceWorkRow(item: SourceWorkItem, hrefs: StandingHrefs): AttentionRow what = `${item.label} ${item.statusLabel}`; } return { + href: hrefs.connection(item.routeId), id: item.id, what, why: item.group === "working" || item.group === "notMeasured" ? item.statusLabel : item.what, - href: hrefs.connection(item.routeId), }; } @@ -977,52 +977,52 @@ function toSourceWorkSections( const sections: SourceWorkSectionView[] = []; if (groups.needsOwner.length > 0) { sections.push({ + countLabel: pluralSource(groups.needsOwner.length), id: "needsOwner", - title: SOURCE_WORK_GROUP_COPY.needsOwner.label, note: SOURCE_WORK_GROUP_COPY.needsOwner.note, - countLabel: pluralSource(groups.needsOwner.length), - tone: "owner", rows: groups.needsOwner.map((item) => sourceWorkRow(item, hrefs)), + title: SOURCE_WORK_GROUP_COPY.needsOwner.label, + tone: "owner", }); } if (groups.review.length > 0) { sections.push({ + countLabel: pluralSource(groups.review.length), id: "review", - title: SOURCE_WORK_GROUP_COPY.review.label, note: SOURCE_WORK_GROUP_COPY.review.note, - countLabel: pluralSource(groups.review.length), - tone: "review", rows: groups.review.map((item) => sourceWorkRow(item, hrefs)), + title: SOURCE_WORK_GROUP_COPY.review.label, + tone: "review", }); } if (groups.systemIssues.length > 0 || overviewIssues.length > 0) { sections.push({ + countLabel: pluralSource(groups.systemIssues.length + overviewIssues.length), id: "systemIssue", - title: SOURCE_WORK_GROUP_COPY.systemIssue.label, note: SOURCE_WORK_GROUP_COPY.systemIssue.note, - countLabel: pluralSource(groups.systemIssues.length + overviewIssues.length), - tone: "system", rows: [...groups.systemIssues.map((item) => sourceWorkRow(item, hrefs)), ...overviewIssues], + title: SOURCE_WORK_GROUP_COPY.systemIssue.label, + tone: "system", }); } if (groups.working.length > 0) { sections.push({ + countLabel: pluralSource(groups.working.length), id: "working", - title: SOURCE_WORK_GROUP_COPY.working.label, note: SOURCE_WORK_GROUP_COPY.working.note, - countLabel: pluralSource(groups.working.length), - tone: "muted", rows: groups.working.map((item) => sourceWorkRow(item, hrefs)), + title: SOURCE_WORK_GROUP_COPY.working.label, + tone: "muted", }); } if (groups.notMeasured.length > 0) { sections.push({ + countLabel: pluralSource(groups.notMeasured.length), id: "notMeasured", - title: SOURCE_WORK_GROUP_COPY.notMeasured.label, note: SOURCE_WORK_GROUP_COPY.notMeasured.note, - countLabel: pluralSource(groups.notMeasured.length), - tone: "muted", rows: groups.notMeasured.map((item) => sourceWorkRow(item, hrefs)), + title: SOURCE_WORK_GROUP_COPY.notMeasured.label, + tone: "muted", }); } return sections; @@ -1038,11 +1038,11 @@ function buildDecideHero(pending: PendingApproval[], hrefs: StandingHrefs): Stan const reads = first ? approvalReads(first) : "parts of your data"; const moreSub = `Nothing leaves until you say so — review each request one at a time. ${more} more after this one.`; return { - tone: "decide", + cta: { href: hrefs.grants, human: true, label: "Review the request" }, kicker: pending.length === 1 ? "A request is waiting on you" : `${pending.length} requests are waiting`, - line: { text: `${who} wants to read `, emphasis: reads, tail: "." }, + line: { emphasis: reads, tail: ".", text: `${who} wants to read ` }, sub: more > 0 ? moreSub : "Nothing leaves until you say so — approve it one piece at a time.", - cta: { label: "Review the request", href: hrefs.grants, human: true }, + tone: "decide", }; } @@ -1055,27 +1055,27 @@ function buildFailureHero(attention: AttentionConnection[], hrefs: StandingHrefs const [only] = attention; if (count === 1 && only) { return { - tone: "alarm", - kicker: "One thing needs you", - line: { text: `${only.label} `, emphasis: "needs you", tail: "." }, - sub: only.what, // A device-local recovery is not performed by clicking — the CTA only // navigates to where the commands are. Use a navigation label ("See what // to do") so the owner doesn't click expecting the dashboard to run it. // A dashboard-actionable verdict (reauth, refresh) keeps its action verb. cta: { - label: only.deviceLocal ? "See what to do" : only.actionLabel, href: hrefs.connection(only.routeId), human: true, + label: only.deviceLocal ? "See what to do" : only.actionLabel, }, + kicker: "One thing needs you", + line: { emphasis: "needs you", tail: ".", text: `${only.label} ` }, + sub: only.what, + tone: "alarm", }; } return { - tone: "alarm", + cta: { href: hrefs.runs, label: "See what needs you" }, kicker: `${count} things need you`, - line: { text: `${count} connections `, emphasis: "need a look", tail: "." }, + line: { emphasis: "need a look", tail: ".", text: `${count} connections ` }, sub: "Nothing you already have is lost — open each one to see what it needs.", - cta: { label: "See what needs you", href: hrefs.runs }, + tone: "alarm", }; } @@ -1084,32 +1084,32 @@ function buildStaleHero(summary: DatasetSummary | null, hrefs: StandingHrefs): S const projectionState = summary?.projection?.state; const isFailed = projectionState === "failed"; return { - tone: "alarm", + cta: { href: hrefs.deployment, label: "View status" }, kicker: isFailed ? "Totals update delayed" : "Totals updating", line: { - text: "Records ", emphasis: "are still available", tail: ".", + text: "Records ", }, sub: isFailed ? "Dashboard totals are using the last completed update." : "Dashboard totals may use the last completed update for a few minutes.", - cta: { label: "View status", href: hrefs.deployment }, + tone: "alarm", }; } function buildPartialDataHero(loadIssues: readonly string[], hrefs: StandingHrefs): StandingHero { const count = loadIssues.length; return { - tone: "alarm", + cta: { href: hrefs.deployment, label: "Check deployment" }, kicker: "Overview is incomplete", line: { - text: count === 1 ? "One dashboard check " : `${count} dashboard checks `, emphasis: "did not load", tail: ".", + text: count === 1 ? "One dashboard check " : `${count} dashboard checks `, }, sub: "Refresh this page; if it keeps happening, check deployment. This page will not claim all-clear from partial data.", - cta: { label: "Check deployment", href: hrefs.deployment }, + tone: "alarm", }; } @@ -1120,19 +1120,19 @@ function buildAdvisoryHero(actions: AdvisoryOwnerActionConnection[], hrefs: Stan // now"), not the "ready for review" taxonomy phrasing. const action = only.actionLabel ?? "Run the available action"; return { - tone: "decide", + cta: { href: hrefs.connection(only.routeId), human: true, label: action }, kicker: "One optional action is available", - line: { text: `${only.label}: `, emphasis: action, tail: "." }, + line: { emphasis: action, tail: ".", text: `${only.label}: ` }, sub: only.what, - cta: { label: action, href: hrefs.connection(only.routeId), human: true }, + tone: "decide", }; } return { - tone: "decide", + cta: { href: hrefs.sources, label: "See available actions" }, kicker: `${actions.length} optional actions are available`, - line: { text: "Refreshes and retries ", emphasis: "are available", tail: "." }, + line: { emphasis: "are available", tail: ".", text: "Refreshes and retries " }, sub: "Nothing is broken — run these refreshes and retries when you like. Records remain available.", - cta: { label: "See available actions", href: hrefs.sources }, + tone: "decide", }; } @@ -1151,10 +1151,10 @@ function buildCalmHero(input: StandingInputs): StandingHero { const withBearers = `${activeClients.length} ${clientWord} ${fmtInt(activeTokenCount)} active owner ${tokenWord} with full access. ${liveGrants.length} ${grantWord} only the slices you granted. Revoke any of them instantly.`; const noBearers = `No owner token can act as you yet. ${liveGrants.length} ${grantWord} only the slices you granted. Revoke any of them instantly.`; return { - tone: "calm", kicker: "Where you stand", - line: { text: `${records} records from ${sources} ${sourceWord} — `, emphasis: "all yours to read", tail: "." }, + line: { emphasis: "all yours to read", tail: ".", text: `${records} records from ${sources} ${sourceWord} — ` }, sub: activeTokenCount > 0 ? withBearers : noBearers, + tone: "calm", }; } @@ -1178,9 +1178,9 @@ export function computeHero(input: StandingInputs): StandingHero { sourceWork.needsOwner.map((item) => ({ actionLabel: item.actionLabel ?? "Review source", connectorKey: "", - routeId: item.routeId, deviceLocal: item.deviceLocal, label: item.label, + routeId: item.routeId, what: item.what, })), input.hrefs @@ -1215,15 +1215,15 @@ export function buildStandingData(input: StandingInputs): StandingData { const allBearers = toBearers(input.bearerClients, input.hrefs); const bearers = allBearers.slice(0, BEARER_PREVIEW_LIMIT); return { - hero: computeHero(input), + advisoryOwnerActions: toAdvisoryOwnerActions(input.advisoryOwnerActions, input.hrefs), + attention: toAttention(input.attentionConnections, input.hrefs), bearers, bearersOverflow: allBearers.length - bearers.length, grantPackages: toGrantPackages(input.grants, input.hrefs, input.grantPackageCount), - relationships: toRelationships(input.grants, input.hrefs, input.now, clientNamesById(input.bearerClients)), + hero: computeHero(input), lately: toLately(input.traces, input.now), - advisoryOwnerActions: toAdvisoryOwnerActions(input.advisoryOwnerActions, input.hrefs), - attention: toAttention(input.attentionConnections, input.hrefs), overviewIssues, + relationships: toRelationships(input.grants, input.hrefs, input.now, clientNamesById(input.bearerClients)), sourceIssues: toSourceIssues(input.sourceIssues, input.hrefs), sourceWorkSections: toSourceWorkSections(sourceWork, overviewIssues, input.hrefs), }; diff --git a/apps/console/src/app/(console)/components/web-push-settings.tsx b/apps/console/src/app/(console)/components/web-push-settings.tsx index e70a77577..3cc54b36d 100644 --- a/apps/console/src/app/(console)/components/web-push-settings.tsx +++ b/apps/console/src/app/(console)/components/web-push-settings.tsx @@ -40,11 +40,11 @@ type DeviceAction = function deviceReducer(state: DeviceState, action: DeviceAction): DeviceState { switch (action.type) { case "supportDetected": - return { ...state, unavailable: action.unavailable, permission: action.permission, status: action.status }; + return { ...state, permission: action.permission, status: action.status, unavailable: action.unavailable }; case "swState": return action.status === undefined ? { ...state, swState: action.swState } - : { ...state, swState: action.swState, status: action.status }; + : { ...state, status: action.status, swState: action.swState }; case "subscribed": return { ...state, endpoint: action.endpoint, status: action.status }; case "disabled": @@ -177,91 +177,91 @@ function setupStepToneClass(state: DiagnosticState) { function secureContextRow(): DiagnosticRow { if (typeof window === "undefined") { - return { label: "Secure context (HTTPS/localhost)", state: "unknown", detail: "Server-rendered" }; + return { detail: "Server-rendered", label: "Secure context (HTTPS/localhost)", state: "unknown" }; } if (window.isSecureContext) { - return { label: "Secure context (HTTPS/localhost)", state: "ok", detail: window.location.origin }; + return { detail: window.location.origin, label: "Secure context (HTTPS/localhost)", state: "ok" }; } return { + detail: "Page is not served over a secure origin.", label: "Secure context (HTTPS/localhost)", state: "fail", - detail: "Page is not served over a secure origin.", }; } function featureRow(label: string, present: boolean, presentDetail: string, missingDetail: string): DiagnosticRow { - return present ? { label, state: "ok", detail: presentDetail } : { label, state: "fail", detail: missingDetail }; + return present ? { detail: presentDetail, label, state: "ok" } : { detail: missingDetail, label, state: "fail" }; } function vapidRow(config: WebPushConfig): DiagnosticRow { if (config.enabled && config.public_key) { return { + detail: "/_ref/web-push/config reports enabled", label: "Server VAPID keys configured", state: "ok", - detail: "/_ref/web-push/config reports enabled", }; } return { + detail: config.unavailable_reason || "Server VAPID keys are not configured.", label: "Server VAPID keys configured", state: "fail", - detail: config.unavailable_reason || "Server VAPID keys are not configured.", }; } function swRow(swState: "registered" | "absent" | "unknown" | "unsupported"): DiagnosticRow { const label = "Service worker registered"; if (swState === "registered") { - return { label, state: "ok", detail: "The PDPP service worker controls /." }; + return { detail: "The PDPP service worker controls /.", label, state: "ok" }; } if (swState === "absent") { - return { label, state: "warn", detail: "Not registered yet - use Enable this device." }; + return { detail: "Not registered yet - use Enable this device.", label, state: "warn" }; } if (swState === "unsupported") { - return { label, state: "fail", detail: "Browser lacks serviceWorker." }; + return { detail: "Browser lacks serviceWorker.", label, state: "fail" }; } - return { label, state: "unknown", detail: "Could not inspect registration." }; + return { detail: "Could not inspect registration.", label, state: "unknown" }; } function permissionRow(permission: NotificationPermission | "unknown"): DiagnosticRow { const label = "Notification permission granted"; if (permission === "granted") { - return { label, state: "ok", detail: 'Notification.permission === "granted"' }; + return { detail: 'Notification.permission === "granted"', label, state: "ok" }; } if (permission === "denied") { return { + detail: 'Notification.permission === "denied" - change browser/OS notification settings to opt in.', label, state: "fail", - detail: 'Notification.permission === "denied" - change browser/OS notification settings to opt in.', }; } if (permission === "default") { return { + detail: "Permission has not been requested on this device - use Enable this device.", label, state: "warn", - detail: "Permission has not been requested on this device - use Enable this device.", }; } - return { label, state: "unknown", detail: "Notification API not available." }; + return { detail: "Notification API not available.", label, state: "unknown" }; } function browserSubscriptionRow(endpoint: string | null, matchesThisBrowser: boolean): DiagnosticRow { const label = "Browser push subscription active"; if (!endpoint) { return { - label, - state: "warn", detail: "No active subscription on this device - use Enable this device to create one (installing the PWA alone does not subscribe).", + label, + state: "warn", }; } if (matchesThisBrowser) { - return { label, state: "ok", detail: "Browser endpoint is registered for this owner on the server." }; + return { detail: "Browser endpoint is registered for this owner on the server.", label, state: "ok" }; } return { - label, - state: "warn", detail: "Browser has a subscription but the server does not list it for this owner - use Enable this device to re-register.", + label, + state: "warn", }; } @@ -269,15 +269,15 @@ function deliveryHealthRow(lastSubscription: WebPushSubscriptionSummary | undefi const label = "Last delivery health"; if (lastSubscription?.last_failure_reason) { return { + detail: `Most recent failure: ${lastSubscription.last_failure_reason} (${lastSubscription.last_failure_at ?? "unknown time"}). Use Enable this device on the affected device to re-subscribe.`, label, state: "warn", - detail: `Most recent failure: ${lastSubscription.last_failure_reason} (${lastSubscription.last_failure_at ?? "unknown time"}). Use Enable this device on the affected device to re-subscribe.`, }; } if (lastSubscription?.last_success_at) { - return { label, state: "ok", detail: `Last success: ${lastSubscription.last_success_at}.` }; + return { detail: `Last success: ${lastSubscription.last_success_at}.`, label, state: "ok" }; } - return { label, state: "unknown", detail: "No delivery attempt recorded yet." }; + return { detail: "No delivery attempt recorded yet.", label, state: "unknown" }; } function deviceStatus({ @@ -295,43 +295,43 @@ function deviceStatus({ }): DeviceStatus { if (unavailable) { return { - title: "This browser cannot receive PDPP notifications.", detail: unavailable, + title: "This browser cannot receive PDPP notifications.", }; } if (permission === "unknown" && swState === "unknown") { return { - title: "Checking this device…", detail: "Inspecting browser permission and subscription state.", + title: "Checking this device…", }; } if (permission === "denied") { return { - title: "Notifications are blocked for this browser.", detail: "Change browser or OS notification settings, then return here and enable this device.", + title: "Notifications are blocked for this browser.", }; } if (matchesThisBrowser) { return { - title: "This device is subscribed.", detail: "Pending connector interactions can send browser notifications to this browser or installed app.", + title: "This device is subscribed.", }; } if (endpoint) { return { - title: "This browser has a local subscription, but the server does not recognize it.", detail: "Enable this device again to repair the server-side subscription.", + title: "This browser has a local subscription, but the server does not recognize it.", }; } if (permission === "granted") { return { - title: "Notifications are allowed, but this device is not subscribed.", detail: "Enable this device once so PDPP can send alerts here.", + title: "Notifications are allowed, but this device is not subscribed.", }; } return { - title: "This device is not subscribed yet.", detail: "Installing the PWA only adds the app icon. You still need to enable notifications from this device.", + title: "This device is not subscribed yet.", }; } @@ -352,26 +352,26 @@ function buildSetupSteps({ return [ { + detail: "This page configures notifications only for the browser or installed app you are using right now.", label: "Open the right device", state: "ok", - detail: "This page configures notifications only for the browser or installed app you are using right now.", }, { + detail: setupPermissionDetail(permission), label: "Allow notifications", state: setupPermissionState(permission, unavailable), - detail: setupPermissionDetail(permission), }, { + detail: setupSubscriptionDetail({ endpoint, matchesThisBrowser }), label: "Subscribe this device", state: setupSubscriptionState({ matchesThisBrowser, unavailable }), - detail: setupSubscriptionDetail({ endpoint, matchesThisBrowser }), }, { - label: "Send a test", - state: setupTestState({ matchesThisBrowser, testNotificationAccepted }), detail: testNotificationAccepted ? "The push provider accepted the test. Only the device can confirm whether it displayed." : "Use Send test notification after this device is subscribed.", + label: "Send a test", + state: setupTestState({ matchesThisBrowser, testNotificationAccepted }), }, ]; } @@ -493,12 +493,12 @@ function buildDiagnostics({ permissionRow(permission), browserSubscriptionRow(endpoint, matchesThisBrowser), { - label: "Server-tracked subscriptions for this owner", - state: subscriptions.length > 0 ? "ok" : "warn", detail: subscriptions.length > 0 ? `${subscriptions.length} saved across all of this owner's devices.` : "Server has no saved subscriptions yet for this owner.", + label: "Server-tracked subscriptions for this owner", + state: subscriptions.length > 0 ? "ok" : "warn", }, deliveryHealthRow(lastSubscription), ]; @@ -564,27 +564,27 @@ export function WebPushSettings({ async function refreshSubscriptionState() { if (!hasNavigatorFeature("serviceWorker")) { - dispatch({ type: "swState", swState: "unsupported" }); + dispatch({ swState: "unsupported", type: "swState" }); return; } try { const registration = await navigator.serviceWorker.getRegistration("/"); await registration?.update().catch(() => undefined); - dispatch({ type: "swState", swState: registration ? "registered" : "absent" }); + dispatch({ swState: registration ? "registered" : "absent", type: "swState" }); const existing = await registration?.pushManager.getSubscription(); - dispatch({ type: "endpoint", endpoint: existing?.endpoint ?? null }); + dispatch({ endpoint: existing?.endpoint ?? null, type: "endpoint" }); } catch { - dispatch({ type: "swState", swState: "unknown" }); + dispatch({ swState: "unknown", type: "swState" }); } } useEffect(() => { const reason = detectSupport(config); dispatch({ - type: "supportDetected", - unavailable: reason, permission: "Notification" in window ? Notification.permission : "unknown", status: reason ? "Unavailable in this browser" : `Permission: ${Notification.permission}`, + type: "supportDetected", + unavailable: reason, }); if (reason) { return; @@ -596,7 +596,7 @@ export function WebPushSettings({ if (cancelled) { return; } - dispatch({ type: "swState", swState: registration ? "registered" : "absent" }); + dispatch({ swState: registration ? "registered" : "absent", type: "swState" }); await registration?.update().catch(() => undefined); const existing = await registration?.pushManager.getSubscription(); if (cancelled) { @@ -604,18 +604,18 @@ export function WebPushSettings({ } if (existing) { dispatch({ - type: "subscribed", endpoint: existing.endpoint, status: "Web Push is enabled for this browser.", + type: "subscribed", }); } }) .catch(() => { if (!cancelled) { dispatch({ - type: "swState", - swState: "unknown", status: "Could not inspect this browser's Web Push subscription.", + swState: "unknown", + type: "swState", }); } }); @@ -632,45 +632,45 @@ export function WebPushSettings({ setTestStatus(null); try { const registration = await navigator.serviceWorker.register("/pdpp-dashboard-sw.js"); - dispatch({ type: "swState", swState: "registered" }); + dispatch({ swState: "registered", type: "swState" }); const result = await Notification.requestPermission(); - dispatch({ type: "permission", permission: result }); + dispatch({ permission: result, type: "permission" }); if (result !== "granted") { dispatch({ - type: "status", status: result === "denied" ? "Permission denied. Enable notifications in browser settings." : "Permission was not granted.", + type: "status", }); return; } const subscription = (await registration.pushManager.getSubscription()) ?? (await registration.pushManager.subscribe({ - userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(config.public_key), + userVisibleOnly: true, })); const response = await fetch("/_ref/web-push/subscriptions", { - method: "POST", - credentials: "same-origin", - headers: { "Content-Type": "application/json", Accept: "application/json" }, body: JSON.stringify({ - subscription: subscription.toJSON(), - platform: navigator.platform || null, device_label: "PDPP browser", + platform: navigator.platform || null, + subscription: subscription.toJSON(), }), + credentials: "same-origin", + headers: { Accept: "application/json", "Content-Type": "application/json" }, + method: "POST", }); if (!response.ok) { throw await webPushResponseError(response, "Subscription failed"); } dispatch({ - type: "subscribed", endpoint: subscription.endpoint, status: "Web Push is enabled for this browser.", + type: "subscribed", }); } catch (err) { - dispatch({ type: "status", status: err instanceof Error ? err.message : "Failed to enable Web Push." }); + dispatch({ status: err instanceof Error ? err.message : "Failed to enable Web Push.", type: "status" }); } finally { setBusy(false); } @@ -681,9 +681,9 @@ export function WebPushSettings({ setTestStatus("Sending test notification…"); try { const response = await fetch("/_ref/web-push/test", { - method: "POST", credentials: "same-origin", headers: { Accept: "application/json" }, + method: "POST", }); if (!response.ok) { throw await webPushResponseError(response, "Test notification failed"); @@ -721,18 +721,18 @@ export function WebPushSettings({ } if (targetEndpoint) { const response = await fetch("/_ref/web-push/subscriptions", { - method: "DELETE", - credentials: "same-origin", - headers: { "Content-Type": "application/json", Accept: "application/json" }, body: JSON.stringify({ endpoint: targetEndpoint }), + credentials: "same-origin", + headers: { Accept: "application/json", "Content-Type": "application/json" }, + method: "DELETE", }); if (!response.ok) { throw await webPushResponseError(response, "Unsubscribe failed"); } } - dispatch({ type: "disabled", status: "Web Push is disabled for this browser." }); + dispatch({ status: "Web Push is disabled for this browser.", type: "disabled" }); } catch (err) { - dispatch({ type: "status", status: err instanceof Error ? err.message : "Failed to disable Web Push." }); + dispatch({ status: err instanceof Error ? err.message : "Failed to disable Web Push.", type: "status" }); } finally { setBusy(false); } @@ -746,17 +746,17 @@ export function WebPushSettings({ const lastSubscription = subscriptions[0]; const matchesThisBrowser = endpoint ? subscriptions.some((s) => s.endpoint === endpoint && !s.revoked_at) : false; - const currentDeviceStatus = deviceStatus({ unavailable, permission, endpoint, matchesThisBrowser, swState }); - const setupSteps = buildSetupSteps({ unavailable, permission, endpoint, matchesThisBrowser, testStatus }); + const currentDeviceStatus = deviceStatus({ endpoint, matchesThisBrowser, permission, swState, unavailable }); + const setupSteps = buildSetupSteps({ endpoint, matchesThisBrowser, permission, testStatus, unavailable }); const diagnostics = buildDiagnostics({ config, - swState, - permission, endpoint, + lastSubscription, matchesThisBrowser, + permission, subscriptions, - lastSubscription, + swState, }); // Whether this browser is set up and healthy, so the demoted summary line can @@ -791,7 +791,7 @@ export function WebPushSettings({
{enabled || unavailable ? null : ( - + Open Syncs - + Back to Sources
diff --git a/apps/console/src/app/(console)/connect/browser-session/[connectorId]/launch/page.tsx b/apps/console/src/app/(console)/connect/browser-session/[connectorId]/launch/page.tsx index dd3f7f0aa..f93d947dc 100644 --- a/apps/console/src/app/(console)/connect/browser-session/[connectorId]/launch/page.tsx +++ b/apps/console/src/app/(console)/connect/browser-session/[connectorId]/launch/page.tsx @@ -48,7 +48,7 @@ export default async function BrowserSessionLaunchPage({ + Back to sources } diff --git a/apps/console/src/app/(console)/connect/browser-session/[connectorId]/page.tsx b/apps/console/src/app/(console)/connect/browser-session/[connectorId]/page.tsx index bf8f43737..07829fa38 100644 --- a/apps/console/src/app/(console)/connect/browser-session/[connectorId]/page.tsx +++ b/apps/console/src/app/(console)/connect/browser-session/[connectorId]/page.tsx @@ -65,10 +65,10 @@ function UnavailableSetupCard({ displayName }: { displayName: string }) { existing source to reconnect it, or return to Add source to see what this dashboard can add now.

- + Open sources - + Add source
@@ -119,7 +119,7 @@ export default async function BrowserSessionConnectPage({
{selected ? "Selected" : "Use"} @@ -192,27 +192,27 @@ function SelectedIdentityCommands({ const cimd = buildCimdCommands(mcpUrl, selected.client_id); const entries: SetupEntry[] = [ { - title: "Claude Code", body: "Default discovery; use this unless the client asks for an explicit client_id.", label: "Claude Code default command", + title: "Claude Code", value: targets.claudeCodeCommand, }, { - title: "Claude Code + CIMD", body: "Pins this local client to the selected stable metadata URL.", label: "Claude Code CIMD command", + title: "Claude Code + CIMD", value: cimd.claudeCodeCimdCommand, }, { - title: "Codex", body: "Default discovery for the hosted MCP endpoint.", label: "Codex default command", + title: "Codex", value: targets.codexCommand, }, { - title: "Codex + CIMD", body: "Pins Codex to the selected stable metadata URL.", label: "Codex CIMD command", + title: "Codex + CIMD", value: cimd.codexCimdCommand, }, ]; @@ -277,35 +277,35 @@ export default async function ConnectPage({ searchParams }: { searchParams: Prom const notice = noticeText(params.notice); const primaryEntries: SetupEntry[] = [ { - title: "MCP URL", body: "Use this for ChatGPT, Claude.ai, and remote MCP clients. Browser clients use PKCE; sandboxed clients can use the advertised device-code flow.", label: "MCP server URL", + title: "MCP URL", value: targets.mcpUrl, }, { - title: "Claude Code", body: "Adds the remote Streamable HTTP MCP server.", label: "Claude Code command", + title: "Claude Code", value: targets.claudeCodeCommand, }, { - title: "Codex", body: "Adds the same remote MCP endpoint without a bearer-token env var.", label: "Codex command", + title: "Codex", value: targets.codexCommand, }, ]; const secondaryEntries: SetupEntry[] = [ { - title: "PDPP CLI", body: "For a shell agent that will use scoped REST reads instead of hosted MCP.", label: "PDPP CLI connect command", + title: "PDPP CLI", value: targets.pdppCliCommand, }, { - title: "Agent skill", body: "For agents that discover instructions before choosing MCP or CLI.", label: "agent-readable entrypoint URL", + title: "Agent skill", value: targets.agentEntrypoint, }, ]; @@ -314,7 +314,7 @@ export default async function ConnectPage({ searchParams }: { searchParams: Prom + Deployment readiness } diff --git a/apps/console/src/app/(console)/connect/static-secret-payload.test.ts b/apps/console/src/app/(console)/connect/static-secret-payload.test.ts index 6150b656b..f8fddd0ed 100644 --- a/apps/console/src/app/(console)/connect/static-secret-payload.test.ts +++ b/apps/console/src/app/(console)/connect/static-secret-payload.test.ts @@ -249,5 +249,5 @@ test("required bundled fields fail before capture instead of storing incomplete form ); - assert.deepEqual(payload, { ok: false, error: "Password is required." }); + assert.deepEqual(payload, { error: "Password is required.", ok: false }); }); diff --git a/apps/console/src/app/(console)/connect/static-secret/[connectorId]/page.tsx b/apps/console/src/app/(console)/connect/static-secret/[connectorId]/page.tsx index 30b9a6547..e92f7fdd9 100644 --- a/apps/console/src/app/(console)/connect/static-secret/[connectorId]/page.tsx +++ b/apps/console/src/app/(console)/connect/static-secret/[connectorId]/page.tsx @@ -55,8 +55,8 @@ export default async function StaticSecretConnectPage({ }); const resolvedSearchParams = await searchParams; const pageParams: PageSearchParams = { - error: firstValue(resolvedSearchParams.error), connectionId: firstValue(resolvedSearchParams.connection_id), + error: firstValue(resolvedSearchParams.error), }; // Repair/update mode: a connection_id in the query means the owner is // replacing the credential on an existing connection, not creating a new one. @@ -86,7 +86,7 @@ export default async function StaticSecretConnectPage({ + {backLabel} } diff --git a/apps/console/src/app/(console)/connect/static-secret/[connectorId]/static-secret-payload.ts b/apps/console/src/app/(console)/connect/static-secret/[connectorId]/static-secret-payload.ts index e17a9b34a..29dc56126 100644 --- a/apps/console/src/app/(console)/connect/static-secret/[connectorId]/static-secret-payload.ts +++ b/apps/console/src/app/(console)/connect/static-secret/[connectorId]/static-secret-payload.ts @@ -52,7 +52,7 @@ export function buildStaticSecretPayload( ? bundledSecretPayload(setup, formData) : singleSecretPayload(setup, formData); if ("error" in result) { - return { ok: false, error: result.error }; + return { error: result.error, ok: false }; } return { ok: true, secret: result.secret }; } diff --git a/apps/console/src/app/(console)/connect/status/[connectionId]/page.tsx b/apps/console/src/app/(console)/connect/status/[connectionId]/page.tsx index 1bb9330a7..863b04586 100644 --- a/apps/console/src/app/(console)/connect/status/[connectionId]/page.tsx +++ b/apps/console/src/app/(console)/connect/status/[connectionId]/page.tsx @@ -60,32 +60,32 @@ function describeActiveConnectionState(status: ConnectionSetupStatus): StatusDes if (status.setup_kind === "static_secret" && runStartedAfterCredentialRotation(status)) { if (status.running) { return { - tone: "pending", - headline: "Credential saved", detail: "A sync is running now to verify the updated credential. Existing records remain available while it runs.", + headline: "Credential saved", + tone: "pending", }; } if (statusRunIsFailure(status)) { return { - tone: "failed", - headline: "Credential saved, sync failed", detail: "The updated credential was saved, but the verification sync failed. Re-enter it or open the run timeline for the exact failure.", + headline: "Credential saved, sync failed", + tone: "failed", }; } if (statusRunIsSuccess(status)) { return { - tone: "active", - headline: "Connection active", detail: "The updated credential was verified by a completed sync. Records are available.", + headline: "Connection active", + tone: "active", }; } } return { - tone: "active", - headline: "Connection active", detail: "Records are available for this account.", + headline: "Connection active", + tone: "active", }; } @@ -93,45 +93,45 @@ function describeImportState(status: ConnectionSetupStatus): StatusDescription { switch (status.setup_state) { case "active": return { - tone: "active", - headline: "Import complete", detail: status.import_receipt ? "Your import was validated and committed. Review the durable coverage receipt below." : "Your import was committed. This connector did not provide a validation preview for the setup receipt.", + headline: "Import complete", + tone: "active", }; case "first_sync_running": return { - tone: "pending", - headline: "Import running", detail: "The import file is captured and the import is in progress. This page updates as it finishes.", + headline: "Import running", + tone: "pending", }; case "first_sync_pending": return { - tone: "pending", - headline: "Import starting", detail: "The import file is captured and the import is queued. This page updates as it runs.", + headline: "Import starting", + tone: "pending", }; case "awaiting_credential": return { - tone: "pending", - headline: "File needed", detail: "This source is set up but no import file is captured yet.", + headline: "File needed", + tone: "pending", }; case "first_sync_failed": return { - tone: "failed", - headline: "Import failed", detail: status.last_error?.remediation ?? "Start the import again.", + headline: "Import failed", + tone: "failed", }; case "paused": - return { tone: "pending", headline: "Connection paused", detail: "This connection is paused." }; + return { detail: "This connection is paused.", headline: "Connection paused", tone: "pending" }; case "revoked": - return { tone: "failed", headline: "Connection revoked", detail: "This connection has been revoked." }; + return { detail: "This connection has been revoked.", headline: "Connection revoked", tone: "failed" }; default: return { - tone: "pending", - headline: "Setting up", detail: "This connection is being set up. This page updates as the setup progresses.", + headline: "Setting up", + tone: "pending", }; } } @@ -142,38 +142,38 @@ function describeConnectionState(status: ConnectionSetupStatus): StatusDescripti return describeActiveConnectionState(status); case "first_sync_running": return { - tone: "pending", - headline: "First sync running", detail: "The provider credential is captured and the first sync is in progress. This page updates as it finishes.", + headline: "First sync running", + tone: "pending", }; case "first_sync_pending": return { - tone: "pending", - headline: "First sync starting", detail: "The provider credential is captured and the first sync is queued. This page updates as it runs.", + headline: "First sync starting", + tone: "pending", }; case "awaiting_credential": return { - tone: "pending", - headline: "Setup material needed", detail: "This connection is set up but no provider credential is captured yet.", + headline: "Setup material needed", + tone: "pending", }; case "first_sync_failed": return { - tone: "failed", - headline: "First sync failed", detail: status.last_error?.remediation ?? "Start the first sync again.", + headline: "First sync failed", + tone: "failed", }; case "paused": - return { tone: "pending", headline: "Connection paused", detail: "This connection is paused." }; + return { detail: "This connection is paused.", headline: "Connection paused", tone: "pending" }; case "revoked": - return { tone: "failed", headline: "Connection revoked", detail: "This connection has been revoked." }; + return { detail: "This connection has been revoked.", headline: "Connection revoked", tone: "failed" }; default: return { - tone: "pending", - headline: "Setting up", detail: "This connection is being set up. This page updates as the setup progresses.", + headline: "Setting up", + tone: "pending", }; } } @@ -274,7 +274,7 @@ interface ReceiptRow { function receiptRows(receipt: ImportReceipt): readonly ReceiptRow[] { const baseRows: ReceiptRow[] = [ - { label: "Batch", value: receipt.batch_id, monospace: true }, + { label: "Batch", monospace: true, value: receipt.batch_id }, { label: "File", value: receipt.uploaded_file_name }, { label: "Receipt status", value: receipt.status }, { label: "Detected format", value: receipt.detected_format }, @@ -403,41 +403,41 @@ function importPhaseProgress(status: ConnectionSetupStatus): readonly ImportPhas const facts = importPhaseFacts(status); return [ { - label: "Received", detail: facts.fileReceived ? "PDPP captured the file for this import." : "Choose a file to start.", + label: "Received", state: facts.fileReceived ? "done" : "waiting", }, { - label: "Parsed", detail: facts.parsed ? "The connector parser produced safe validation facts." : "PDPP has not parsed this file yet.", + label: "Parsed", state: parsedPhaseState(facts), }, { - label: "Deduplicated", detail: facts.deduped ? "Duplicate and skipped counts are available." : "Duplicate checks run before records commit.", + label: "Deduplicated", state: dedupedPhaseState(facts), }, { - label: "Committed", detail: facts.committed ? "Accepted records or duplicate-only receipt facts are committed." : "Records are not committed yet.", + label: "Committed", state: committedPhaseState(facts), }, { - label: "Indexed", detail: facts.active ? "Committed records are available on owner surfaces." : "Indexing follows commit.", + label: "Indexed", state: indexedPhaseState(facts), }, { - label: "Health projected", detail: facts.active ? "Connection health and acquisition coverage include this batch." : "Coverage updates after commit.", + label: "Health projected", state: healthProjectedPhaseState(facts), }, ]; @@ -526,7 +526,7 @@ export default async function ConnectionSetupStatusPage({ + Back to Sources } @@ -582,26 +582,26 @@ export default async function ConnectionSetupStatusPage({
{described.tone === "active" ? ( <> - + Open in Explore - + Source details {status.setup_kind === "manual_upload" ? ( - + Import another file ) : null} ) : null} {described.tone === "failed" || status.setup_state === "awaiting_credential" ? ( - + {retryLabel(status)} ) : null} {described.tone === "pending" && status.setup_state !== "awaiting_credential" ? ( - + Refresh status ) : null} diff --git a/apps/console/src/app/(console)/deployment/page.tsx b/apps/console/src/app/(console)/deployment/page.tsx index 21e408889..bd50935f1 100644 --- a/apps/console/src/app/(console)/deployment/page.tsx +++ b/apps/console/src/app/(console)/deployment/page.tsx @@ -68,7 +68,7 @@ export default async function DeploymentPage() { + Tokens } diff --git a/apps/console/src/app/(console)/deployment/tokens/actions.ts b/apps/console/src/app/(console)/deployment/tokens/actions.ts index 20c74fd90..962d76685 100644 --- a/apps/console/src/app/(console)/deployment/tokens/actions.ts +++ b/apps/console/src/app/(console)/deployment/tokens/actions.ts @@ -187,8 +187,8 @@ export async function revokeOwnerTokenAction(formData: FormData) { const res = await fetch( url, await withOwnerSessionCookie({ - method: "DELETE", cache: "no-store", + method: "DELETE", }) ); if (res.status !== 204 && res.status !== 404) { diff --git a/apps/console/src/app/(console)/deployment/tokens/page.tsx b/apps/console/src/app/(console)/deployment/tokens/page.tsx index 4c094420b..dc49e0ec9 100644 --- a/apps/console/src/app/(console)/deployment/tokens/page.tsx +++ b/apps/console/src/app/(console)/deployment/tokens/page.tsx @@ -81,7 +81,7 @@ function OwnerAgentOnboardingCard({ entrypoint }: { entrypoint: string }) { pasted into chat or copied out of the dashboard.

- + Review deployment metadata
@@ -191,9 +191,9 @@ function Transcript({ flow }: { flow: FlowState }) { const approve = JSON.stringify({ user_code: flow.userCode }, null, 2); const exchange = JSON.stringify( { - grant_type: "urn:ietf:params:oauth:grant-type:device_code", - device_code: flow.deviceCode, client_id: flow.clientId, + device_code: flow.deviceCode, + grant_type: "urn:ietf:params:oauth:grant-type:device_code", }, null, 2 @@ -522,11 +522,11 @@ export default async function DeploymentTokensPage({ searchParams }: { searchPar + Deployment overview } - breadcrumbs={[{ label: "Deployment", href: "/deployment" }, { label: "Tokens" }]} + breadcrumbs={[{ href: "/deployment", label: "Deployment" }, { label: "Tokens" }]} description="Set up trusted local owner automation without pasting bearer material. Manual owner bearers stay available below for debugging." title="Owner-agent access" /> diff --git a/apps/console/src/app/(console)/device-exporters/actions.ts b/apps/console/src/app/(console)/device-exporters/actions.ts index 64b6d4efe..2bca4735c 100644 --- a/apps/console/src/app/(console)/device-exporters/actions.ts +++ b/apps/console/src/app/(console)/device-exporters/actions.ts @@ -22,19 +22,19 @@ export async function createEnrollmentCodeAction( const expiresRaw = String(formData.get("expires_in_seconds") ?? "").trim(); if (!(connectorId && localBindingName)) { - return { ok: false, message: "Connector id and local binding name are required." }; + return { message: "Connector id and local binding name are required.", ok: false }; } if (!isSupportedLocalCollectorConnector(connectorId)) { return { - ok: false, message: "This setup form only creates packaged local collector enrollments. Browser-based sources will use the dashboard browser setup flow when it ships.", + ok: false, }; } const expiresInSeconds = expiresRaw ? Number(expiresRaw) : undefined; if (expiresInSeconds !== undefined && !Number.isInteger(expiresInSeconds)) { - return { ok: false, message: "Expiration must be a whole number of seconds." }; + return { message: "Expiration must be a whole number of seconds.", ok: false }; } try { @@ -47,7 +47,7 @@ export async function createEnrollmentCodeAction( revalidatePath("/device-exporters"); return { code, deviceLabel: displayName || null, ok: true }; } catch (err) { - return { ok: false, message: err instanceof Error ? err.message : String(err) }; + return { message: err instanceof Error ? err.message : String(err), ok: false }; } } diff --git a/apps/console/src/app/(console)/device-exporters/page.tsx b/apps/console/src/app/(console)/device-exporters/page.tsx index 9030c5301..981835777 100644 --- a/apps/console/src/app/(console)/device-exporters/page.tsx +++ b/apps/console/src/app/(console)/device-exporters/page.tsx @@ -35,10 +35,10 @@ export const metadata = { const DEVICE_STATUS_VOCABULARY = { active: { label: "active", tone: "success" }, + fresh: { label: "fresh", tone: "success" }, + never: { label: "no heartbeat", tone: "warning" }, revoked: { label: "revoked", tone: "danger" }, stale: { label: "stale", tone: "warning" }, - never: { label: "no heartbeat", tone: "warning" }, - fresh: { label: "fresh", tone: "success" }, } as const; export default async function DeviceExportersPage({ @@ -69,7 +69,7 @@ export default async function DeviceExportersPage({ // already validated against the supported sets, so it never names an arbitrary // value. The bare page (no deep-link) keeps its existing diagnostics framing. const addConnectionBreadcrumbs = defaultConnectorId - ? [{ label: "Sources", href: "/sources" }, { label: `Add ${formatConnectorKeyForDisplay(defaultConnectorId)}` }] + ? [{ href: "/sources", label: "Sources" }, { label: `Add ${formatConnectorKeyForDisplay(defaultConnectorId)}` }] : undefined; try { diff --git a/apps/console/src/app/(console)/device-exporters/render.test.ts b/apps/console/src/app/(console)/device-exporters/render.test.ts index 12e9b2750..5638c7078 100644 --- a/apps/console/src/app/(console)/device-exporters/render.test.ts +++ b/apps/console/src/app/(console)/device-exporters/render.test.ts @@ -29,7 +29,7 @@ test("summarizeIngestCounts totals source-instance accepted and rejected counts" test("formatLastError prefers message, then code, then generic state", () => { assert.equal(formatLastError(null), "none"); assert.equal( - formatLastError({ message: "browser session expired", code: "session_expired" }), + formatLastError({ code: "session_expired", message: "browser session expired" }), "browser session expired" ); assert.equal(formatLastError({ code: "rate_limited" }), "rate_limited"); diff --git a/apps/console/src/app/(console)/error.tsx b/apps/console/src/app/(console)/error.tsx index 43612f23c..98f74083b 100644 --- a/apps/console/src/app/(console)/error.tsx +++ b/apps/console/src/app/(console)/error.tsx @@ -29,10 +29,10 @@ export default function DashboardError({ error, reset }: { error: Error & { dige sign back in if the problem persists.

diff --git a/apps/console/src/app/(console)/event-subscriptions/disable-action.ts b/apps/console/src/app/(console)/event-subscriptions/disable-action.ts index 152d18b23..c20c6ada6 100644 --- a/apps/console/src/app/(console)/event-subscriptions/disable-action.ts +++ b/apps/console/src/app/(console)/event-subscriptions/disable-action.ts @@ -43,7 +43,7 @@ function validateReason(raw: string): { ok: true; value: string | null } | { ok: // (an `email-loop_suspected_…` reason that becomes `email-l…` is worse // than no reason at all). if (Buffer.byteLength(raw, "utf8") > MAX_REASON_BYTES) { - return { ok: false, message: reasonOverflowMessage() }; + return { message: reasonOverflowMessage(), ok: false }; } return { ok: true, value: raw }; } diff --git a/apps/console/src/app/(console)/explore/actions.ts b/apps/console/src/app/(console)/explore/actions.ts index 1d1be8487..d5cc6d872 100644 --- a/apps/console/src/app/(console)/explore/actions.ts +++ b/apps/console/src/app/(console)/explore/actions.ts @@ -49,17 +49,17 @@ export async function loadExploreBuckets(req: ExploreBucketRequest): Promise
- - + Back to PDPP
diff --git a/apps/console/src/app/(console)/explore/explore-acceptance.test.ts b/apps/console/src/app/(console)/explore/explore-acceptance.test.ts index f04d9492b..292648c0d 100644 --- a/apps/console/src/app/(console)/explore/explore-acceptance.test.ts +++ b/apps/console/src/app/(console)/explore/explore-acceptance.test.ts @@ -107,13 +107,13 @@ function makeHit(over: { connection_id?: string; }): SearchResultHit { return { - connector_id: over.connector_id, connection_id: over.connection_id, - stream: over.stream ?? "records", - record_key: over.record_key ?? "rec-1", + connector_id: over.connector_id, emitted_at: "2026-01-01T00:00:00Z", matched_fields: [], object: "search_result", + record_key: over.record_key ?? "rec-1", + stream: over.stream ?? "records", }; } @@ -139,11 +139,11 @@ function makeLexicalPage( function makeRecordsPage(ids: string[], opts?: { has_more?: boolean; next_cursor?: string }): RecordsPage { return { data: ids.map((id) => ({ + data: {}, + emitted_at: "2026-01-01T00:00:00Z", id, object: "record" as const, stream: "transactions", - emitted_at: "2026-01-01T00:00:00Z", - data: {}, })), has_more: opts?.has_more ?? false, ...(opts?.next_cursor ? { next_cursor: opts.next_cursor } : {}), @@ -157,19 +157,19 @@ function makeTimelinePage( ): ExploreTimelinePage { return { data: Array.from({ length: count }, (_, i) => ({ - object: "timeline_record" as const, connector_id: "ynab", connector_instance_id: `cin_ynab_stub_${i}`, - stream: "transactions", - record_key: `rec-${i}`, - emitted_at: `2026-01-0${i + 1}T00:00:00Z`, data: {}, + emitted_at: `2026-01-0${i + 1}T00:00:00Z`, + object: "timeline_record" as const, + record_key: `rec-${i}`, + stream: "transactions", })), has_more: opts.has_more, + new_since_snapshot: 0, next_cursor: opts.next_cursor ?? null, object: "list", snapshot_at: opts.snapshot_at ?? "2026-06-19T00:00:00Z", - new_since_snapshot: 0, }; } @@ -177,40 +177,40 @@ const notStubbed = () => Promise.reject(new Error("not stubbed")); function makeDataSource(overrides: Partial): DashboardDataSource { return { - kind: "sandbox" as const, aggregateRecordsByTime: notStubbed, - listExploreRecordBuckets: notStubbed, - isHybridRetrievalAdvertised: () => Promise.resolve(false), - isSemanticRetrievalAdvertised: () => Promise.resolve(false), - listConnectorSummaries: notStubbed, - listConnectorManifests: notStubbed, - searchRecordsLexical: notStubbed, - searchRecordsHybrid: notStubbed, - searchRecordsSemantic: notStubbed, - queryRecords: notStubbed, - getRecord: notStubbed, getConnectorOverview: notStubbed, - getStreamMetadata: notStubbed, - getTraceTimeline: notStubbed, - getGrantTimeline: notStubbed, - getRunTimeline: notStubbed, getDatasetSummary: notStubbed, getDeploymentDiagnostics: notStubbed, - listGrants: notStubbed, - listPendingApprovals: notStubbed, - listRuns: notStubbed, - listStreams: notStubbed, - listTraces: notStubbed, - refSearch: notStubbed, + getGrantTimeline: notStubbed, + getRecord: notStubbed, + getRunTimeline: notStubbed, + getStreamMetadata: notStubbed, + getTraceTimeline: notStubbed, + isHybridRetrievalAdvertised: () => Promise.resolve(false), + isSemanticRetrievalAdvertised: () => Promise.resolve(false), + kind: "sandbox" as const, + listConnectorManifests: notStubbed, + listConnectorSummaries: notStubbed, + listExploreRecordBuckets: notStubbed, listExploreTimeline: () => Promise.resolve({ - object: "list" as const, data: [], has_more: false, + new_since_snapshot: 0, next_cursor: null, + object: "list" as const, snapshot_at: new Date(0).toISOString(), - new_since_snapshot: 0, }), + listGrants: notStubbed, + listPendingApprovals: notStubbed, + listRuns: notStubbed, + listStreams: notStubbed, + listTraces: notStubbed, + queryRecords: notStubbed, + refSearch: notStubbed, + searchRecordsHybrid: notStubbed, + searchRecordsLexical: notStubbed, + searchRecordsSemantic: notStubbed, ...overrides, }; } @@ -333,7 +333,6 @@ test("P1 assembler: streamSeeAllLinks is non-empty when the time-range fan-out h // produce the ramps. const summary = makeSummary({ connection_id: "ynab-1", connector_id: "ynab", streams: ["transactions"] }); const ds = makeDataSource({ - listConnectorSummaries: async () => summaryListResponse([summary]), // The time-range fan-out requires a consent_time_field on the stream so // timeRangeStreamTargets includes it. The base makeManifest helper does not // set this field; provide an explicit manifest here with it declared. @@ -341,22 +340,23 @@ test("P1 assembler: streamSeeAllLinks is non-empty when the time-range fan-out h [ { connector_id: "ynab", - streams: [{ name: "transactions", consent_time_field: "date" }], + streams: [{ consent_time_field: "date", name: "transactions" }], }, ] satisfies ConnectorManifest[], - // Time-range fan-out: queryRecords returns has_more=true so the see-all ramp fires. - queryRecords: async () => - makeRecordsPage(["rec-1", "rec-2", "rec-3", "rec-4", "rec-5", "rec-6"], { has_more: true }), + listConnectorSummaries: async () => summaryListResponse([summary]), // The recent lens would call this; it is irrelevant here (time-range lens), // but provide a valid empty page rather than throwing. listExploreTimeline: async () => ({ - object: "list" as const, data: [], has_more: false, + new_since_snapshot: 0, next_cursor: null, + object: "list" as const, snapshot_at: new Date(0).toISOString(), - new_since_snapshot: 0, }), + // Time-range fan-out: queryRecords returns has_more=true so the see-all ramp fires. + queryRecords: async () => + makeRecordsPage(["rec-1", "rec-2", "rec-3", "rec-4", "rec-5", "rec-6"], { has_more: true }), }); const result = await assembleExplorerData( @@ -387,8 +387,8 @@ test("P3 invariant: day-group (rr-x-day) and burst-collapse (rr-x-burst) class n test("P3 Load-more cursor: assembler returns non-null nextCursor when real endpoint has_more=true", async () => { const summary = makeSummary({ connection_id: "ynab-1", connector_id: "ynab", streams: ["transactions"] }); const ds = makeDataSource({ - listConnectorSummaries: async () => summaryListResponse([summary]), listConnectorManifests: async () => [makeManifest("ynab", ["transactions"])], + listConnectorSummaries: async () => summaryListResponse([summary]), // Real endpoint: returns has_more=true with a real composite cursor listExploreTimeline: async () => makeTimelinePage(32, { has_more: true, next_cursor: "composite-cursor-p2" }), }); @@ -404,8 +404,8 @@ test("P3 Load-more cursor: assembler returns non-null nextCursor when real endpo test("P3 Load-more cursor: assembler returns null nextCursor when real endpoint has_more=false (end of feed)", async () => { const summary = makeSummary({ connection_id: "ynab-1", connector_id: "ynab", streams: ["transactions"] }); const ds = makeDataSource({ - listConnectorSummaries: async () => summaryListResponse([summary]), listConnectorManifests: async () => [makeManifest("ynab", ["transactions"])], + listConnectorSummaries: async () => summaryListResponse([summary]), listExploreTimeline: async () => makeTimelinePage(5, { has_more: false, snapshot_at: "2026-06-19T00:00:00Z" }), }); @@ -430,39 +430,39 @@ test("P3 Load-more trail: second page ACCUMULATES (page 1 stays, page 2 appended // Snapshot newer than every record so the page-1 filter keeps the whole snapshot. const PAGE1_SNAPSHOT = "2026-12-31T00:00:00Z"; const mkRecord = (page: string, i: number, dayOffset: number) => ({ - object: "timeline_record" as const, connector_id: "ynab", connector_instance_id: "ynab-1", - stream: "transactions", - record_key: `${page}-${i}`, - emitted_at: new Date(baseMs - dayOffset * dayMs).toISOString(), data: {}, + emitted_at: new Date(baseMs - dayOffset * dayMs).toISOString(), + object: "timeline_record" as const, + record_key: `${page}-${i}`, + stream: "transactions", }); const page1Records = Array.from({ length: 32 }, (_, i) => mkRecord("p1", i, i)); const page2Records = Array.from({ length: 3 }, (_, i) => mkRecord("p2", i, 100 + i)); const page1: ExploreTimelinePage = { - object: "list", data: page1Records, has_more: true, + new_since_snapshot: 0, next_cursor: "composite-cursor-p2", + object: "list", snapshot_at: PAGE1_SNAPSHOT, - new_since_snapshot: 0, }; const page2: ExploreTimelinePage = { - object: "list", data: page2Records, has_more: false, + new_since_snapshot: 0, next_cursor: null, + object: "list", snapshot_at: PAGE1_SNAPSHOT, - new_since_snapshot: 0, }; // Capture each fetch as `${rewind ? "rewind:" : "cursor:"}${cursor}` so we can // assert the fetch plan distinguishes a REWIND of the trail head (page 1) from a // plain fetch of the same cursor (page 2). const capturedFetches: string[] = []; const ds = makeDataSource({ - listConnectorSummaries: async () => summaryListResponse([summary]), listConnectorManifests: async () => [makeManifest("ynab", ["transactions"])], + listConnectorSummaries: async () => summaryListResponse([summary]), listExploreTimeline: (opts) => { const cursor = opts?.cursor ?? null; const rewind = Boolean(opts?.rewindToFirstPage); @@ -485,7 +485,7 @@ test("P3 Load-more trail: second page ACCUMULATES (page 1 stays, page 2 appended // Page 2: append the page-1 cursor to the trail (Load more), forwarding the anchor. const accumulated = await assembleExplorerData( - { cursors: "composite-cursor-p2", anchor: firstSnapshot ?? undefined }, + { anchor: firstSnapshot ?? undefined, cursors: "composite-cursor-p2" }, ds, "https://rs.test" ); @@ -549,28 +549,28 @@ test("P2 Option D invariant: escape links to search_sort=recent in the URL build test("P2 sort toggle: lexical Most-relevant and Most-recent return same hit count (same pool)", async () => { const summary = makeSummary({ connection_id: "ynab-1", connector_id: "ynab", streams: ["transactions"] }); - const hit1 = makeHit({ connector_id: "ynab", stream: "transactions", record_key: "r1", connection_id: "ynab-1" }); - const hit2 = makeHit({ connector_id: "ynab", stream: "transactions", record_key: "r2", connection_id: "ynab-1" }); + const hit1 = makeHit({ connection_id: "ynab-1", connector_id: "ynab", record_key: "r1", stream: "transactions" }); + const hit2 = makeHit({ connection_id: "ynab-1", connector_id: "ynab", record_key: "r2", stream: "transactions" }); // Both orderings use the same assembler data source with the same hits. // Most-relevant: lexical call returns the hits ranked by relevance. // Most-recent (single-stream): switches to queryRecords for the same stream. // The feed count must be the same (same pool, different ordering). const dsRelevance = makeDataSource({ - listConnectorSummaries: async () => summaryListResponse([summary]), - listConnectorManifests: async () => [makeManifest("ynab", ["transactions"])], isHybridRetrievalAdvertised: async () => false, + listConnectorManifests: async () => [makeManifest("ynab", ["transactions"])], + listConnectorSummaries: async () => summaryListResponse([summary]), searchRecordsLexical: async () => makeLexicalPage([hit1, hit2], { has_more: false }), }); const dsRecent = makeDataSource({ - listConnectorSummaries: async () => summaryListResponse([summary]), - listConnectorManifests: async () => [makeManifest("ynab", ["transactions"])], isHybridRetrievalAdvertised: async () => false, - // Most-recent detection path: lexical to find stream door - searchRecordsLexical: async () => makeLexicalPage([hit1, hit2], { has_more: false }), + listConnectorManifests: async () => [makeManifest("ynab", ["transactions"])], + listConnectorSummaries: async () => summaryListResponse([summary]), // Then queryRecords for the same 2 records in time order queryRecords: async () => makeRecordsPage(["r1", "r2"], { has_more: false }), + // Most-recent detection path: lexical to find stream door + searchRecordsLexical: async () => makeLexicalPage([hit1, hit2], { has_more: false }), }); const relevanceResult = await assembleExplorerData({ q: "rent" }, dsRelevance, "https://rs.test"); @@ -586,14 +586,14 @@ test("P2 sort toggle: lexical Most-relevant and Most-recent return same hit coun test("P2 lexical Load-more forwards cursor (assembler advances, does not re-fetch from top)", async () => { const summary = makeSummary({ connection_id: "ynab-1", connector_id: "ynab", streams: ["transactions"] }); - const hit1 = makeHit({ connector_id: "ynab", stream: "transactions", record_key: "p1", connection_id: "ynab-1" }); - const hit2 = makeHit({ connector_id: "ynab", stream: "transactions", record_key: "p2", connection_id: "ynab-1" }); + const hit1 = makeHit({ connection_id: "ynab-1", connector_id: "ynab", record_key: "p1", stream: "transactions" }); + const hit2 = makeHit({ connection_id: "ynab-1", connector_id: "ynab", record_key: "p2", stream: "transactions" }); const capturedCursors: (string | undefined)[] = []; const ds = makeDataSource({ - listConnectorSummaries: async () => summaryListResponse([summary]), - listConnectorManifests: async () => [makeManifest("ynab", ["transactions"])], isHybridRetrievalAdvertised: () => Promise.resolve(false), + listConnectorManifests: async () => [makeManifest("ynab", ["transactions"])], + listConnectorSummaries: async () => summaryListResponse([summary]), searchRecordsLexical: (_q, opts) => { capturedCursors.push(opts?.cursor); // Recall proven complete: keyword_pageable, so the cursor pages exhaustively. @@ -608,7 +608,7 @@ test("P2 lexical Load-more forwards cursor (assembler advances, does not re-fetc const page1 = await assembleExplorerData({ q: "coffee" }, ds, "https://rs.test"); assert.equal(page1.searchNextCursor, "lex-p2"); - const page2 = await assembleExplorerData({ q: "coffee", cursor: "lex-p2" }, ds, "https://rs.test"); + const page2 = await assembleExplorerData({ cursor: "lex-p2", q: "coffee" }, ds, "https://rs.test"); assert.equal(page2.searchNextCursor, null, "page 2 must be exhausted"); assert.equal(capturedCursors[0], undefined, "page 1: no cursor"); diff --git a/apps/console/src/app/(console)/explore/explore-canvas.tsx b/apps/console/src/app/(console)/explore/explore-canvas.tsx index 337c8cbcc..add040e18 100644 --- a/apps/console/src/app/(console)/explore/explore-canvas.tsx +++ b/apps/console/src/app/(console)/explore/explore-canvas.tsx @@ -248,18 +248,18 @@ function dateNormalizedHref(explorePath: string, state: DateNormalizeState): str const dateNav = dateNavFromLift(dateLift.after, dateLift.before); return withOrderSuffix( buildHref(explorePath, { - query: dateLift.rest, connectionIds: state.selectedConnectionIds, excludeConnectionIds: state.excludeConnectionIds, - streams: state.selectedStreams, excludeStreams: state.excludeStreams, + // Preserve a peeked record from a shared `?q=after:X&peek=Y` link. + peek: state.peek, + query: dateLift.rest, + searchSort: state.searchSort === "recent" ? "recent" : undefined, // Only the endpoint the URL actually typed overrides the canonical window; the // other side is carried forward from the existing window (never stacks/clears). since: dateNav.since ?? state.since, + streams: state.selectedStreams, until: dateNav.until ?? state.until, - searchSort: state.searchSort === "recent" ? "recent" : undefined, - // Preserve a peeked record from a shared `?q=after:X&peek=Y` link. - peek: state.peek, }), state.order ); @@ -299,9 +299,9 @@ function composeFacetSelection( const nextStreams = unionIds(cur.streams, lift.includeStreams); return { connectionIds: nextInclude, - streams: nextStreams, excludeConnectionIds: unionIds(cur.excludeConnectionIds, liftedExclude).filter((id) => !nextInclude.includes(id)), excludeStreams: unionIds(cur.excludeStreams, lift.excludeStreams).filter((s) => !nextStreams.includes(s)), + streams: nextStreams, }; } @@ -541,19 +541,19 @@ function buildFilterChips(args: { for (const id of args.selectedConnectionIds) { const name = args.connections.find((c) => c.connectionId === id)?.displayName ?? id; out.push({ + canNegate: true, + clear: () => args.toggleConnection(id), id: `con:${id}`, label: name, - property: "source", - operator: "is", - value: name, - negated: false, - canNegate: true, negate: () => { // Toggle include → exclude: remove from include, add to exclude args.toggleConnection(id); args.toggleExcludeConnection(id); }, - clear: () => args.toggleConnection(id), + negated: false, + operator: "is", + property: "source", + value: name, }); } // EXCLUDE chips render the facet/operator exclusion as a visible chip in the SAME @@ -562,51 +562,51 @@ function buildFilterChips(args: { for (const id of args.excludeConnectionIds) { const name = args.connections.find((c) => c.connectionId === id)?.displayName ?? id; out.push({ + canNegate: true, + clear: () => args.toggleExcludeConnection(id), id: `xcon:${id}`, label: `not ${name}`, - property: "source", - operator: "is not", - value: name, - negated: true, - canNegate: true, negate: () => { // Toggle exclude → include: remove from exclude, add to include args.toggleExcludeConnection(id); args.toggleConnection(id); }, - clear: () => args.toggleExcludeConnection(id), + negated: true, + operator: "is not", + property: "source", + value: name, }); } for (const s of args.selectedStreams) { out.push({ + canNegate: true, + clear: () => args.toggleStream(s), id: `stream:${s}`, label: `stream: ${s}`, - property: "stream", - operator: "is", - value: s, - negated: false, - canNegate: true, negate: () => { args.toggleStream(s); args.toggleExcludeStream(s); }, - clear: () => args.toggleStream(s), + negated: false, + operator: "is", + property: "stream", + value: s, }); } for (const s of args.excludeStreams) { out.push({ + canNegate: true, + clear: () => args.toggleExcludeStream(s), id: `xstream:${s}`, label: `not stream: ${s}`, - property: "stream", - operator: "is not", - value: s, - negated: true, - canNegate: true, negate: () => { args.toggleExcludeStream(s); args.toggleStream(s); }, - clear: () => args.toggleExcludeStream(s), + negated: true, + operator: "is not", + property: "stream", + value: s, }); } // NB: the active DATE window is NOT pushed here. It is rendered ONCE, by the @@ -627,17 +627,17 @@ function buildFilterChips(args: { const property = colon > 0 ? body.slice(0, colon) : "search"; const value = colon > 0 ? body.slice(colon + 1) : body; out.push({ + canNegate: false, + clear: () => args.navigate({ query: removeToken(args.query, tk.raw) }), id: `tok:${i}`, label: tk.label, - property, - operator: negated ? "is not" : "is", - value, - negated, - canNegate: false, negate: () => { // token chips do not support negation toggling (use -operator: in the query) }, - clear: () => args.navigate({ query: removeToken(args.query, tk.raw) }), + negated, + operator: negated ? "is not" : "is", + property, + value, }); }); return out; @@ -1564,8 +1564,8 @@ function StreamDoorLink({ recordsBasePath: string; }) { const streamHref = buildStreamRecordsHref(recordsBasePath, { - connectorId: door.connectorId, connectionId: door.connectionId, + connectorId: door.connectorId, stream: door.stream, }); return ( @@ -1577,8 +1577,8 @@ function StreamDoorLink({ function StreamSeeAllLink({ link, recordsBasePath }: { link: ExplorerStreamSeeAllLink; recordsBasePath: string }) { const streamHref = buildStreamRecordsHref(recordsBasePath, { - connectorId: link.connectorId, connectionId: link.connectionId, + connectorId: link.connectorId, stream: link.stream, }); const totalLabel = typeof link.total === "number" ? ` - ${link.total.toLocaleString()} records` : ""; @@ -2737,17 +2737,17 @@ function FeedBody({ recordsBasePath={recordsBasePath} selectedPeekParam={selectedPeekParam} zeroResultsProps={{ - loadedCount: data.feed.length, explorePath, exploreState: { - query: data.query, connectionIds: data.selectedConnectionIds, excludeConnectionIds: data.excludeConnectionIds, - streams: data.selectedStreams, excludeStreams: data.excludeStreams, + query: data.query, since: data.since, + streams: data.selectedStreams, until: data.until, }, + loadedCount: data.feed.length, }} /> @@ -2936,7 +2936,7 @@ function SavedViewTabs({ const onSave = useCallback( (name: string) => { // A stable id; crypto.randomUUID is available in every browser the console targets. - persist(addSavedView(views, { id: crypto.randomUUID(), name, href: currentHref })); + persist(addSavedView(views, { href: currentHref, id: crypto.randomUUID(), name })); }, [persist, views, currentHref] ); @@ -3081,17 +3081,17 @@ export function ExploreCanvas({ data, explorePath, order = "newest", peekRelatio const [optimisticSelection, setOptimisticSelectionState] = useState({ connectionIds: data.selectedConnectionIds, - streams: data.selectedStreams, excludeConnectionIds: data.excludeConnectionIds, excludeStreams: data.excludeStreams, + streams: data.selectedStreams, }); const optimisticSelectionRef = useRef(optimisticSelection); useEffect(() => { const next = { connectionIds: data.selectedConnectionIds, - streams: data.selectedStreams, excludeConnectionIds: data.excludeConnectionIds, excludeStreams: data.excludeStreams, + streams: data.selectedStreams, }; optimisticSelectionRef.current = next; setOptimisticSelectionState(next); @@ -3129,16 +3129,16 @@ export function ExploreCanvas({ data, explorePath, order = "newest", peekRelatio // date operators via `chipTokens`, so the single render before this settles is honest.) useEffect(() => { const href = dateNormalizedHref(explorePath, { + excludeConnectionIds: data.excludeConnectionIds, + excludeStreams: data.excludeStreams, + order, + peek: data.peek ? explorerPeekParam(data.peek) : undefined, query: data.query, + searchSort: data.searchSort === "recent" ? "recent" : "relevance", selectedConnectionIds: data.selectedConnectionIds, - excludeConnectionIds: data.excludeConnectionIds, selectedStreams: data.selectedStreams, - excludeStreams: data.excludeStreams, since: data.since, until: data.until, - searchSort: data.searchSort === "recent" ? "recent" : "relevance", - peek: data.peek ? explorerPeekParam(data.peek) : undefined, - order, }); if (href !== null) { // Replace (not push) so the un-normalized URL never traps the back button. @@ -3213,12 +3213,12 @@ export function ExploreCanvas({ data, explorePath, order = "newest", peekRelatio const streamGroups = useMemo( () => computeSourceGroupedStreamFacets({ - feed: data.feed, connections: data.connections, + excludeStreams, + feed: data.feed, passes: (e) => passesClientFilter(e, parsed, serverFilterableFields), selectedConnectionIds, selectedStreams, - excludeStreams, }), [ data.feed, @@ -3278,18 +3278,18 @@ export function ExploreCanvas({ data, explorePath, order = "newest", peekRelatio const href = buildNavigateHref( explorePath, { - query: data.query, connectionIds: optimisticSelectionRef.current.connectionIds, - streams: optimisticSelectionRef.current.streams, + cursorTrail: data.cursorTrail, excludeConnectionIds: optimisticSelectionRef.current.excludeConnectionIds, excludeStreams: optimisticSelectionRef.current.excludeStreams, - since: data.since, - until: data.until, + order, + query: data.query, searchSort: data.searchSort === "recent" ? "recent" : "relevance", + since: data.since, snapshotAnchor: data.snapshotAnchor, - cursorTrail: data.cursorTrail, + streams: optimisticSelectionRef.current.streams, + until: data.until, upcomingTrail: data.upcomingTrail, - order, }, opts ); @@ -3317,19 +3317,19 @@ export function ExploreCanvas({ data, explorePath, order = "newest", peekRelatio const cur = optimisticSelectionRef.current; const next = { connectionIds: toggleIdSelection(cur.connectionIds, connectionId), - streams: [], // Including a connection clears any exclusion of the same id (a connection // can't be both "only this" and "not this") and clears stream exclusions // since the stream facet rescopes when the connection set changes. excludeConnectionIds: cur.excludeConnectionIds.filter((id) => id !== connectionId), excludeStreams: [], + streams: [], }; setOptimisticSelection(next); navigate({ connectionIds: next.connectionIds, - streams: next.streams, excludeConnectionIds: next.excludeConnectionIds, excludeStreams: next.excludeStreams, + streams: next.streams, }); }, [navigate, setOptimisticSelection] @@ -3341,9 +3341,9 @@ export function ExploreCanvas({ data, explorePath, order = "newest", peekRelatio const next = { // Excluding a connection clears its inclusion (mutually exclusive per id). connectionIds: cur.connectionIds.filter((id) => id !== connectionId), - streams: cur.streams, excludeConnectionIds: toggleIdSelection(cur.excludeConnectionIds, connectionId), excludeStreams: cur.excludeStreams, + streams: cur.streams, }; setOptimisticSelection(next); navigate({ @@ -3359,12 +3359,12 @@ export function ExploreCanvas({ data, explorePath, order = "newest", peekRelatio const cur = optimisticSelectionRef.current; const next = { connectionIds: cur.connectionIds, - streams: toggleIdSelection(cur.streams, stream), excludeConnectionIds: cur.excludeConnectionIds, excludeStreams: cur.excludeStreams.filter((s) => s !== stream), + streams: toggleIdSelection(cur.streams, stream), }; setOptimisticSelection(next); - navigate({ streams: next.streams, excludeStreams: next.excludeStreams }); + navigate({ excludeStreams: next.excludeStreams, streams: next.streams }); }, [navigate, setOptimisticSelection] ); @@ -3374,12 +3374,12 @@ export function ExploreCanvas({ data, explorePath, order = "newest", peekRelatio const cur = optimisticSelectionRef.current; const next = { connectionIds: cur.connectionIds, - streams: cur.streams.filter((s) => s !== stream), excludeConnectionIds: cur.excludeConnectionIds, excludeStreams: toggleIdSelection(cur.excludeStreams, stream), + streams: cur.streams.filter((s) => s !== stream), }; setOptimisticSelection(next); - navigate({ streams: next.streams, excludeStreams: next.excludeStreams }); + navigate({ excludeStreams: next.excludeStreams, streams: next.streams }); }, [navigate, setOptimisticSelection] ); @@ -3407,11 +3407,11 @@ export function ExploreCanvas({ data, explorePath, order = "newest", peekRelatio setOptimisticSelection(next); setDraft(lift.rest); navigate({ - query: lift.rest, connectionIds: next.connectionIds, - streams: next.streams, excludeConnectionIds: next.excludeConnectionIds, excludeStreams: next.excludeStreams, + query: lift.rest, + streams: next.streams, ...dateNav, }); }, [draft, navigate, data.connections, setOptimisticSelection]); @@ -3434,7 +3434,7 @@ export function ExploreCanvas({ data, explorePath, order = "newest", peekRelatio const setOrder = useCallback((next: SortOrder) => navigate({ order: next }), [navigate]); const setSearchSort = useCallback( - (next: "relevance" | "recent") => navigate({ searchSort: next, cursor: undefined }), + (next: "relevance" | "recent") => navigate({ cursor: undefined, searchSort: next }), [navigate] ); @@ -3470,7 +3470,7 @@ export function ExploreCanvas({ data, explorePath, order = "newest", peekRelatio (recordId: string) => { setLastRecordIdSearch(recordId); setDraft(recordId); - navigate({ query: recordId, peek: undefined }); + navigate({ peek: undefined, query: recordId }); }, [navigate] ); @@ -3498,18 +3498,18 @@ export function ExploreCanvas({ data, explorePath, order = "newest", peekRelatio const chips = useMemo( () => buildFilterChips({ - selectedConnectionIds, - selectedStreams, + connections: data.connections, excludeConnectionIds, excludeStreams, - connections: data.connections, - tokens: chipTokens(committedParsed.tokens), + navigate, query: data.query, + selectedConnectionIds, + selectedStreams, toggleConnection, - toggleStream, toggleExcludeConnection, toggleExcludeStream, - navigate, + toggleStream, + tokens: chipTokens(committedParsed.tokens), }), [ selectedConnectionIds, @@ -3576,15 +3576,15 @@ export function ExploreCanvas({ data, explorePath, order = "newest", peekRelatio const currentViewHref = useMemo( () => buildCurrentViewHref(explorePath, order, { - query: data.query, connectionIds: selectedConnectionIds, - streams: selectedStreams, excludeConnectionIds, excludeStreams, - since: data.since, - until: data.until, peek: data.peek ? explorerPeekParam(data.peek) : undefined, + query: data.query, searchSort: data.searchSort === "recent" ? "recent" : undefined, + since: data.since, + streams: selectedStreams, + until: data.until, }), [ data.peek, @@ -3658,12 +3658,12 @@ export function ExploreCanvas({ data, explorePath, order = "newest", peekRelatio const buildSearchSortHref = useCallback( (searchSort: "relevance" | "recent") => buildHref(explorePath, { - query: data.query, connectionIds: selectedConnectionIds, - streams: selectedStreams, + query: data.query, + searchSort, since: data.since, + streams: selectedStreams, until: data.until, - searchSort, }), [data.query, data.since, data.until, explorePath, selectedConnectionIds, selectedStreams] ); diff --git a/apps/console/src/app/(console)/explore/explore-control-state.test.ts b/apps/console/src/app/(console)/explore/explore-control-state.test.ts index 8a25d0a72..da9d2571c 100644 --- a/apps/console/src/app/(console)/explore/explore-control-state.test.ts +++ b/apps/console/src/app/(console)/explore/explore-control-state.test.ts @@ -98,8 +98,8 @@ test("buildRecordDetailHref builds the record-detail route from clean path segme const href = buildRecordDetailHref("/sources", { connectionId: "cin_bc1efca69a1c386d610f0924", connectorId: "usaa", - stream: "transactions", recordId: "d495b98f5bfe6ce1ae10c465aeb607b5", + stream: "transactions", }); assert.equal(href, "/sources/cin_bc1efca69a1c386d610f0924/transactions/d495b98f5bfe6ce1ae10c465aeb607b5"); }); @@ -114,8 +114,8 @@ test("buildRecordDetailHref reproduce-the-bug: no query string, record key is th const href = buildRecordDetailHref("/sources", { connectionId: null, connectorId: "gmail", - stream: "messages", recordId, + stream: "messages", }); assert.ok(!href.includes("?"), `record detail href must not contain a query string: ${href}`); assert.ok(!href.includes("order="), `record detail href must not carry a sort order: ${href}`); @@ -128,8 +128,8 @@ test("buildRecordDetailHref falls back to connectorId when connection identity i const href = buildRecordDetailHref("/sources", { connectionId: null, connectorId: "github", - stream: "issues", recordId: "42", + stream: "issues", }); assert.equal(href, "/sources/github/issues/42"); }); @@ -152,7 +152,7 @@ test("resolveRowKeyAction: Cmd/Ctrl-Enter ESCALATES to the full record route (di action: "open-full", preventDefault: true, }); - assert.deepEqual(resolveRowKeyAction({ key: "Enter", ctrlKey: true }), { + assert.deepEqual(resolveRowKeyAction({ ctrlKey: true, key: "Enter" }), { action: "open-full", preventDefault: true, }); diff --git a/apps/console/src/app/(console)/explore/explore-control-state.ts b/apps/console/src/app/(console)/explore/explore-control-state.ts index b86fbd172..c69825f6f 100644 --- a/apps/console/src/app/(console)/explore/explore-control-state.ts +++ b/apps/console/src/app/(console)/explore/explore-control-state.ts @@ -3,7 +3,7 @@ export type ExploreRange = "today" | "7d" | "30d" | "all"; -const RANGE_DAYS: Record, number> = { today: 0, "7d": 6, "30d": 29 }; +const RANGE_DAYS: Record, number> = { "7d": 6, "30d": 29, today: 0 }; const DAY_MS = 86_400_000; /** ISO `since` for a relative range, or "" for "all". */ diff --git a/apps/console/src/app/(console)/explore/explore-date.ts b/apps/console/src/app/(console)/explore/explore-date.ts index 26c795cda..ff3d85414 100644 --- a/apps/console/src/app/(console)/explore/explore-date.ts +++ b/apps/console/src/app/(console)/explore/explore-date.ts @@ -64,7 +64,7 @@ function parseYmd(value: string): CalendarDay | null { if (!m) { return null; } - return { year: Number(m[1]), month: Number(m[2]), day: Number(m[3]) }; + return { day: Number(m[3]), month: Number(m[2]), year: Number(m[1]) }; } /** @@ -74,13 +74,13 @@ function parseYmd(value: string): CalendarDay | null { */ function calendarDayInZone(ms: number, timeZone: string): CalendarDay { const parts = new Intl.DateTimeFormat("en-CA", { + day: "2-digit", + month: "2-digit", timeZone, year: "numeric", - month: "2-digit", - day: "2-digit", }).formatToParts(new Date(ms)); const get = (type: string) => Number(parts.find((p) => p.type === type)?.value ?? "0"); - return { year: get("year"), month: get("month"), day: get("day") }; + return { day: get("day"), month: get("month"), year: get("year") }; } /** @@ -101,14 +101,14 @@ function startOfDayMs(day: CalendarDay, timeZone: string): number { /** The wall-clock instant (as if-UTC ms) that `timeZone` displays for epoch `ms`. */ function wallClockMsInZone(ms: number, timeZone: string): number { const parts = new Intl.DateTimeFormat("en-US", { - timeZone, - year: "numeric", - month: "2-digit", day: "2-digit", hour: "2-digit", + hourCycle: "h23", minute: "2-digit", + month: "2-digit", second: "2-digit", - hourCycle: "h23", + timeZone, + year: "numeric", }).formatToParts(new Date(ms)); const get = (type: string) => Number(parts.find((p) => p.type === type)?.value ?? "0"); return Date.UTC(get("year"), get("month") - 1, get("day"), get("hour"), get("minute"), get("second")); @@ -207,7 +207,7 @@ const MONTH_DAY_FMT_CACHE = new Map(); function monthDayFormatter(timeZone: string): Intl.DateTimeFormat { let fmt = MONTH_DAY_FMT_CACHE.get(timeZone); if (!fmt) { - fmt = new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric", timeZone }); + fmt = new Intl.DateTimeFormat("en-US", { day: "numeric", month: "short", timeZone }); MONTH_DAY_FMT_CACHE.set(timeZone, fmt); } return fmt; diff --git a/apps/console/src/app/(console)/explore/explore-facet-groups.test.ts b/apps/console/src/app/(console)/explore/explore-facet-groups.test.ts index 8c097ae2d..dd5297405 100644 --- a/apps/console/src/app/(console)/explore/explore-facet-groups.test.ts +++ b/apps/console/src/app/(console)/explore/explore-facet-groups.test.ts @@ -51,12 +51,12 @@ test("per-source loaded count = loaded feed rows for THAT (connection, stream), entry("conn_slack", "channels"), ]; const groups = computeSourceGroupedStreamFacets({ - feed, connections: [SLACK, IMESSAGE], + excludeStreams: [], + feed, passes: ALL, selectedConnectionIds: [], selectedStreams: [], - excludeStreams: [], }); const slack = groups.find((g) => g.connectionId === "conn_slack"); @@ -81,12 +81,12 @@ test("per-source loaded count = loaded feed rows for THAT (connection, stream), test("source loadedTotal is the sum of its visible streams' loaded counts (same KIND, not a lifetime total)", () => { const feed = [entry("conn_slack", "messages"), entry("conn_slack", "messages"), entry("conn_slack", "channels")]; const groups = computeSourceGroupedStreamFacets({ - feed, connections: [SLACK], + excludeStreams: [], + feed, passes: ALL, selectedConnectionIds: [], selectedStreams: [], - excludeStreams: [], }); assert.equal(groups[0]?.loadedTotal, 3); }); @@ -96,12 +96,12 @@ test("0-in-window stream is hidden (no dead-end) UNLESS it is an active filter", const feed = [entry("conn_slack", "channels")]; const hidden = computeSourceGroupedStreamFacets({ - feed, connections: [SLACK], + excludeStreams: [], + feed, passes: ALL, selectedConnectionIds: [], selectedStreams: [], - excludeStreams: [], }); // messages (0 in window, not selected) must NOT appear — never a "0" dead-end. assert.equal( @@ -112,12 +112,12 @@ test("0-in-window stream is hidden (no dead-end) UNLESS it is an active filter", // But a SELECTED messages stays visible even at 0 — an active filter is never hidden. const withSelected = computeSourceGroupedStreamFacets({ - feed, connections: [SLACK], + excludeStreams: [], + feed, passes: ALL, selectedConnectionIds: [], selectedStreams: ["messages"], - excludeStreams: [], }); const sel = withSelected[0]?.streams.find((s) => s.stream === "messages"); assert.ok(sel, "a selected stream with 0 in-window rows is kept (no dropped filter)"); @@ -126,12 +126,12 @@ test("0-in-window stream is hidden (no dead-end) UNLESS it is an active filter", // Likewise an EXCLUDED stream at 0 stays visible. const withExcluded = computeSourceGroupedStreamFacets({ - feed, connections: [SLACK], + excludeStreams: ["messages"], + feed, passes: ALL, selectedConnectionIds: [], selectedStreams: [], - excludeStreams: ["messages"], }); const ex = withExcluded[0]?.streams.find((s) => s.stream === "messages"); assert.ok(ex); @@ -142,12 +142,12 @@ test("a source with no visible streams is dropped entirely (no empty group)", () // iMessage owns messages/attachments but the loaded window has neither. const feed = [entry("conn_slack", "channels")]; const groups = computeSourceGroupedStreamFacets({ - feed, connections: [SLACK, IMESSAGE], + excludeStreams: [], + feed, passes: ALL, selectedConnectionIds: [], selectedStreams: [], - excludeStreams: [], }); assert.equal( groups.find((g) => g.connectionId === "conn_imessage"), @@ -158,12 +158,12 @@ test("a source with no visible streams is dropped entirely (no empty group)", () test("when connections are selected, the rail scopes to those sources only", () => { const feed = [entry("conn_slack", "messages"), entry("conn_imessage", "messages")]; const groups = computeSourceGroupedStreamFacets({ - feed, connections: [SLACK, IMESSAGE], + excludeStreams: [], + feed, passes: ALL, selectedConnectionIds: ["conn_slack"], selectedStreams: [], - excludeStreams: [], }); assert.equal(groups.length, 1); assert.equal(groups[0]?.connectionId, "conn_slack"); @@ -179,12 +179,12 @@ test("the client predicate is honored: counts only loaded rows that pass (count= return keep; }; const groups = computeSourceGroupedStreamFacets({ - feed, connections: [SLACK], + excludeStreams: [], + feed, passes: everyOther, selectedConnectionIds: [], selectedStreams: [], - excludeStreams: [], }); assert.equal( groups[0]?.streams.find((s) => s.stream === "messages")?.loadedCount, @@ -200,12 +200,12 @@ test("rows with a null connectionId never contribute to a per-source count", () { ...entry("conn_slack", "messages"), connectionId: null }, ]; const groups = computeSourceGroupedStreamFacets({ - feed, connections: [SLACK], + excludeStreams: [], + feed, passes: ALL, selectedConnectionIds: [], selectedStreams: [], - excludeStreams: [], }); // Only the row with a concrete connectionId counts (1, not 2). assert.equal(groups[0]?.streams.find((s) => s.stream === "messages")?.loadedCount, 1); @@ -219,12 +219,12 @@ test("active filters pin to the top of a source group; the rest rank by loaded c entry("conn_slack", "messages"), ]; const groups = computeSourceGroupedStreamFacets({ - feed, connections: [SLACK], + excludeStreams: [], + feed, passes: ALL, selectedConnectionIds: [], selectedStreams: ["messages"], // selected, but fewer loaded rows - excludeStreams: [], }); // Selected `messages` pins first despite channels having more rows. assert.equal(groups[0]?.streams[0]?.stream, "messages"); @@ -235,12 +235,12 @@ test("active filters pin to the top of a source group; the rest rank by loaded c test("totalVisibleStreamFacets sums the rendered stream rows across groups", () => { const feed = [entry("conn_slack", "messages"), entry("conn_slack", "channels"), entry("conn_imessage", "messages")]; const groups = computeSourceGroupedStreamFacets({ - feed, connections: [SLACK, IMESSAGE], + excludeStreams: [], + feed, passes: ALL, selectedConnectionIds: [], selectedStreams: [], - excludeStreams: [], }); // Slack: messages + channels (2); iMessage: messages (1) → 3 total. assert.equal(totalVisibleStreamFacets(groups), 3); @@ -254,12 +254,12 @@ test("filterSourceGroups matches source name (keep whole group) or stream names entry("conn_imessage", "attachments"), ]; const groups = computeSourceGroupedStreamFacets({ - feed, connections: [SLACK, IMESSAGE], + excludeStreams: [], + feed, passes: ALL, selectedConnectionIds: [], selectedStreams: [], - excludeStreams: [], }); // Matching the SOURCE name keeps the whole group with all its streams. @@ -284,12 +284,12 @@ test("filterSourceGroups matches source name (keep whole group) or stream names test("filterSourceGroups recomputes the group total over the surviving streams", () => { const feed = [entry("conn_slack", "messages"), entry("conn_slack", "messages"), entry("conn_slack", "channels")]; const groups = computeSourceGroupedStreamFacets({ - feed, connections: [SLACK], + excludeStreams: [], + feed, passes: ALL, selectedConnectionIds: [], selectedStreams: [], - excludeStreams: [], }); // Filtering to just `channels` makes the source total 1 (not the full 3). const filtered = filterSourceGroups(groups, "channels"); diff --git a/apps/console/src/app/(console)/explore/explore-facet-groups.ts b/apps/console/src/app/(console)/explore/explore-facet-groups.ts index 88575d8ef7f3431eef07a4f74af6aedcf4abeac0..c93f35f466410a3ee7d64343c81f95a20bbf83d8 100644 GIT binary patch delta 91 zcmX?Lf53i&inw}eMRHDQN@|LZLQZ}nkao^5%`4GSC{E2uO)dcm7nc;JCgyJT7GK50 i4N(%3Uy_(J`K_e#W@RZe*2zio_M9+f#giAx%L4!mH6X(P delta 61 zcmX?Lf53i&ia1+wNl|KI?qnTtNsiQt { // Rail click → selectedConnectionIds: ["gmail"]. const viaFacet = buildCompiledQuery({ + limit: 50, + order: "newest", parsed: parseQuery(""), selectedConnectionIds: ["gmail"], selectedStreams: [], serverFilterableFields: new Set(), since: "", until: "", - order: "newest", - limit: 50, }); // Typed operator → con:gmail. const viaOperator = buildCompiledQuery({ + limit: 50, + order: "newest", parsed: parseQuery("con:gmail"), selectedConnectionIds: [], selectedStreams: [], serverFilterableFields: new Set(), since: "", until: "", - order: "newest", - limit: 50, }); assert.match(viaFacet, CONNECTION_GMAIL_RE); assert.match(viaOperator, CONNECTION_GMAIL_RE); @@ -59,24 +59,24 @@ test("clicking a SOURCE facet and typing con: compile to the IDENTICAL query (ch test("clicking a STREAM facet and typing stream: compile to the IDENTICAL query (chip == operator)", () => { const viaFacet = buildCompiledQuery({ + limit: 50, + order: "newest", parsed: parseQuery(""), selectedConnectionIds: [], selectedStreams: ["messages"], serverFilterableFields: new Set(), since: "", until: "", - order: "newest", - limit: 50, }); const viaOperator = buildCompiledQuery({ + limit: 50, + order: "newest", parsed: parseQuery("stream:messages"), selectedConnectionIds: [], selectedStreams: [], serverFilterableFields: new Set(), since: "", until: "", - order: "newest", - limit: 50, }); assert.match(viaFacet, STREAM_MESSAGES_RE); assert.match(viaOperator, STREAM_MESSAGES_RE); @@ -107,26 +107,26 @@ test("removing a con:/stream: token from the query deselects the rail (no lifted test("the 'is not' SOURCE toggle and -con: compile to the same exclusion (inversion == flipped operator)", () => { const viaFacet = buildCompiledQuery({ + excludedConnectionIds: ["gmail"], + excludedStreams: [], + limit: 50, + order: "newest", parsed: parseQuery(""), selectedConnectionIds: [], selectedStreams: [], - excludedConnectionIds: ["gmail"], - excludedStreams: [], serverFilterableFields: new Set(), since: "", until: "", - order: "newest", - limit: 50, }); const viaOperator = buildCompiledQuery({ + limit: 50, + order: "newest", parsed: parseQuery("-con:gmail"), selectedConnectionIds: [], selectedStreams: [], serverFilterableFields: new Set(), since: "", until: "", - order: "newest", - limit: 50, }); assert.match(viaFacet, CONNECTION_NOT_GMAIL_RE); assert.match(viaOperator, CONNECTION_NOT_GMAIL_RE); @@ -135,25 +135,25 @@ test("the 'is not' SOURCE toggle and -con: compile to the same exclusion (invers test("the 'is not' STREAM toggle and -stream: compile to the same exclusion", () => { const viaFacet = buildCompiledQuery({ + excludedStreams: ["messages"], + limit: 50, + order: "newest", parsed: parseQuery(""), selectedConnectionIds: [], selectedStreams: [], - excludedStreams: ["messages"], serverFilterableFields: new Set(), since: "", until: "", - order: "newest", - limit: 50, }); const viaOperator = buildCompiledQuery({ + limit: 50, + order: "newest", parsed: parseQuery("-stream:messages"), selectedConnectionIds: [], selectedStreams: [], serverFilterableFields: new Set(), since: "", until: "", - order: "newest", - limit: 50, }); assert.match(viaFacet, STREAM_NOT_MESSAGES_RE); assert.match(viaOperator, STREAM_NOT_MESSAGES_RE); @@ -207,12 +207,12 @@ test("a stream owned by two sources shows source A's loaded count under A and so entry("conn_b", "messages", "b2"), ]; const groups = computeSourceGroupedStreamFacets({ - feed, connections: [A, B], + excludeStreams: [], + feed, passes: ALL, selectedConnectionIds: [], selectedStreams: [], - excludeStreams: [], }); const a = groups.find((g) => g.connectionId === "conn_a"); const b = groups.find((g) => g.connectionId === "conn_b"); diff --git a/apps/console/src/app/(console)/explore/explore-feed-grouping.test.ts b/apps/console/src/app/(console)/explore/explore-feed-grouping.test.ts index 5d6a5a615..cbccfcca6 100644 --- a/apps/console/src/app/(console)/explore/explore-feed-grouping.test.ts +++ b/apps/console/src/app/(console)/explore/explore-feed-grouping.test.ts @@ -140,9 +140,9 @@ test("groupFeedWithBursts: burst key uses connectionId when present", () => { const n = BURST_THRESHOLD; const entries = Array.from({ length: n }, (_, i) => entry({ + connectionId: "cin_abc", displayAt: `${TODAY}T10:00:${String(i).padStart(2, "0")}Z`, recordId: `r${i}`, - connectionId: "cin_abc", stream: "messages", }) ); @@ -154,10 +154,10 @@ test("groupFeedWithBursts: burst key falls back to connectorId when connectionId const n = BURST_THRESHOLD; const entries = Array.from({ length: n }, (_, i) => entry({ - displayAt: `${TODAY}T10:00:${String(i).padStart(2, "0")}Z`, - recordId: `r${i}`, connectionId: null, connectorId: "whatsapp", + displayAt: `${TODAY}T10:00:${String(i).padStart(2, "0")}Z`, + recordId: `r${i}`, stream: "chats", }) ); @@ -170,16 +170,16 @@ test("groupFeedWithBursts: burst key falls back to connectorId when connectionId test("groupFeedWithBursts: burst partition is separated from singles in same day", () => { const burstEntries = Array.from({ length: BURST_THRESHOLD }, (_, i) => entry({ + connectionId: "cin_wa", displayAt: `${TODAY}T10:00:${String(i).padStart(2, "0")}Z`, recordId: `burst_${i}`, - connectionId: "cin_wa", stream: "messages", }) ); // Two singles from a different (connection, stream) partition const singleEntries = [ - entry({ displayAt: `${TODAY}T11:00:00Z`, recordId: "s1", connectionId: "cin_gmail", stream: "emails" }), - entry({ displayAt: `${TODAY}T11:01:00Z`, recordId: "s2", connectionId: "cin_gmail", stream: "emails" }), + entry({ connectionId: "cin_gmail", displayAt: `${TODAY}T11:00:00Z`, recordId: "s1", stream: "emails" }), + entry({ connectionId: "cin_gmail", displayAt: `${TODAY}T11:01:00Z`, recordId: "s2", stream: "emails" }), ]; const groups = groupFeedWithBursts([...burstEntries, ...singleEntries], NOW_MS); assert.equal(groups.length, 1); @@ -192,9 +192,9 @@ test("groupFeedWithBursts: two burst partitions in same day produce two burst gr const mkBurst = (stream: string, connId: string) => Array.from({ length: BURST_THRESHOLD }, (_, i) => entry({ + connectionId: connId, displayAt: `${TODAY}T10:00:${String(i).padStart(2, "0")}Z`, recordId: `${stream}_${i}`, - connectionId: connId, stream, }) ); @@ -207,14 +207,14 @@ test("groupFeedWithBursts: two burst partitions in same day produce two burst gr test("groupFeedWithBursts: bursts in one day, singles in another day", () => { const burstEntries = Array.from({ length: BURST_THRESHOLD }, (_, i) => entry({ + connectionId: "cin_wa", displayAt: `${TODAY}T10:00:${String(i).padStart(2, "0")}Z`, recordId: `b${i}`, - connectionId: "cin_wa", stream: "messages", }) ); const singles = [ - entry({ displayAt: `${YESTERDAY}T09:00:00Z`, recordId: "y1", connectionId: "cin_gmail", stream: "emails" }), + entry({ connectionId: "cin_gmail", displayAt: `${YESTERDAY}T09:00:00Z`, recordId: "y1", stream: "emails" }), ]; const groups = groupFeedWithBursts([...burstEntries, ...singles], NOW_MS); assert.equal(groups.length, 2); @@ -256,11 +256,11 @@ test("isFutureDay is true strictly after today, false for today/past/empty", () test("partitionFeedByTime splits future records into Upcoming, leaving today/past in the main feed", () => { const feed = [ // Future (YNAB-shaped budget months), arriving newest-first (farthest-out first) - entry({ displayAt: "2026-08-01T00:00:00Z", recordId: "aug", connectorId: "ynab", stream: "months" }), - entry({ displayAt: "2026-07-01T00:00:00Z", recordId: "jul", connectorId: "ynab", stream: "months" }), + entry({ connectorId: "ynab", displayAt: "2026-08-01T00:00:00Z", recordId: "aug", stream: "months" }), + entry({ connectorId: "ynab", displayAt: "2026-07-01T00:00:00Z", recordId: "jul", stream: "months" }), // Today + past - entry({ displayAt: `${TODAY}T14:00:00Z`, recordId: "t1", connectorId: "claude-code" }), - entry({ displayAt: `${YESTERDAY}T09:00:00Z`, recordId: "y1", connectorId: "gmail", stream: "emails" }), + entry({ connectorId: "claude-code", displayAt: `${TODAY}T14:00:00Z`, recordId: "t1" }), + entry({ connectorId: "gmail", displayAt: `${YESTERDAY}T09:00:00Z`, recordId: "y1", stream: "emails" }), ]; const { upcoming, upcomingCount, past } = partitionFeedByTime(feed, NOW_MS); @@ -311,7 +311,7 @@ test("groupFeedDaysNoBursts: a same-partition day over the burst threshold stays // (it is already a collapsed disclosure — a second "expand" inside it is the clunk). const day = `${TODAY}T10:00:00Z`; const feed = Array.from({ length: BURST_THRESHOLD + 54 }, (_, i) => - entry({ displayAt: day, recordId: `m${i}`, stream: "month_categories", connectionId: "cin_ynab" }) + entry({ connectionId: "cin_ynab", displayAt: day, recordId: `m${i}`, stream: "month_categories" }) ); // Control: the normal grouping DOES burst (proving the input is burst-eligible). @@ -384,7 +384,7 @@ test("8.2: a day-group that crosses BURST_THRESHOLD still yields visible content // load-more scenario that previously produced a zero-row collapsed burst. const day = `${TODAY}T10:00:00Z`; const feed = Array.from({ length: BURST_THRESHOLD + 86 }, (_, i) => - entry({ displayAt: day, recordId: `m${i}`, stream: "messages", connectionId: "cin_wa" }) + entry({ connectionId: "cin_wa", displayAt: day, recordId: `m${i}`, stream: "messages" }) ); const days = groupFeedWithBursts(feed, NOW_MS); assert.equal(days.length, 1, "one day group"); @@ -403,7 +403,7 @@ test("8.3: preview-reachability — the burst count label number == burst.entrie const day = `${TODAY}T10:00:00Z`; const loaded = BURST_THRESHOLD + 33; const feed = Array.from({ length: loaded }, (_, i) => - entry({ displayAt: day, recordId: `m${i}`, stream: "messages", connectionId: "cin_wa" }) + entry({ connectionId: "cin_wa", displayAt: day, recordId: `m${i}`, stream: "messages" }) ); const burst = groupFeedWithBursts(feed, NOW_MS)[0]?.bursts[0]; assert.ok(burst, "burst exists"); @@ -435,9 +435,9 @@ test("8.3: preview-reachability — the burst count label number == burst.entrie function burstAt(opts: { stream: string; connectionId: string; isos: string[] }): ExplorerFeedEntry[] { return opts.isos.map((iso, i) => entry({ + connectionId: opts.connectionId, displayAt: iso, recordId: `${opts.stream}-${i}`, - connectionId: opts.connectionId, stream: opts.stream, }) ); @@ -447,7 +447,7 @@ function burstAt(opts: { stream: string; connectionId: string; isos: string[] }) * burst's latest member matters for ordering. */ function burstNewestAt(stream: string, connectionId: string, newestIso: string): ExplorerFeedEntry[] { // 10 members at the same instant => latestAt = newestIso, crosses BURST_THRESHOLD. - return burstAt({ stream, connectionId, isos: Array.from({ length: BURST_THRESHOLD }, () => newestIso) }); + return burstAt({ connectionId, isos: Array.from({ length: BURST_THRESHOLD }, () => newestIso), stream }); } /** The `latestAt` of each unit, in render order. */ @@ -497,7 +497,7 @@ test("burst latestAt = NEWEST member even when burst members arrive shuffled (no "2026-06-19T14:33:00Z", "2026-06-19T14:40:00Z", ]; - const feed = burstAt({ stream: "messages", connectionId: "cin_shuffled", isos }); + const feed = burstAt({ connectionId: "cin_shuffled", isos, stream: "messages" }); const groups = groupFeedWithBursts(feed, NOW_MS); const units = groups[0]?.units ?? []; assert.equal(units.length, 1, "one burst unit"); @@ -520,7 +520,7 @@ test("burst order: a single interleaves with bursts by time (single at 25m betwe const feed = [ // Feed order deliberately not display order: the single arrives first, the older // burst before the newer one — the fix must still produce strict DESC. - entry({ displayAt: ago25, recordId: "single-25", connectionId: "cin_gmail", stream: "emails" }), + entry({ connectionId: "cin_gmail", displayAt: ago25, recordId: "single-25", stream: "emails" }), ...burstNewestAt("cc_messages", "cin_cc", ago31), // older burst ...burstNewestAt("codex_messages", "cin_codex", ago23), // newer burst ]; @@ -548,7 +548,7 @@ test("burst order: members within a burst stay newest-first (descending input pr "2026-06-19T14:33:00Z", "2026-06-19T14:32:00Z", ]; - const feed = burstAt({ stream: "messages", connectionId: "cin_wa", isos: isosDesc }); + const feed = burstAt({ connectionId: "cin_wa", isos: isosDesc, stream: "messages" }); const burst = groupFeedWithBursts(feed, NOW_MS)[0]?.units[0]; assert.ok(burst, "the over-threshold partition produced a unit"); if (burst.kind !== "burst") { @@ -568,7 +568,7 @@ test("burst order: equal-latestAt units keep deterministic ORIGINAL feed order ( // order they first appeared in the feed: burstX, single, burstY. const feed = [ ...burstNewestAt("x", "cin_x", sameIso), - entry({ displayAt: sameIso, recordId: "tie-single", connectionId: "cin_s", stream: "single" }), + entry({ connectionId: "cin_s", displayAt: sameIso, recordId: "tie-single", stream: "single" }), ...burstNewestAt("y", "cin_y", sameIso), ]; const units = groupFeedWithBursts(feed, NOW_MS)[0]?.units ?? []; @@ -589,9 +589,9 @@ test("burst order: an undated unit sorts AFTER all dated units within the day (n const ago20 = "2026-06-19T14:40:00Z"; const feed = [ // Undated single arrives FIRST in the feed but must sink to the bottom of the day. - { ...entry({ recordId: "undated", connectionId: "cin_u", stream: "misc" }), displayAt: "" } as ExplorerFeedEntry, + { ...entry({ connectionId: "cin_u", recordId: "undated", stream: "misc" }), displayAt: "" } as ExplorerFeedEntry, ...burstNewestAt("dated", "cin_d", ago20), - entry({ displayAt: ago20, recordId: "dated-single", connectionId: "cin_ds", stream: "emails" }), + entry({ connectionId: "cin_ds", displayAt: ago20, recordId: "dated-single", stream: "emails" }), ]; const groups = groupFeedWithBursts(feed, NOW_MS); // The undated "" day buckets separately from the 2026-06-19 day. Within the DATED @@ -606,12 +606,12 @@ test("burst order: an undated unit sorts AFTER all dated units within the day (n // Now force an undated unit to share a day with dated units (unparseable displayAt // that still slices to a same-day prefix is impossible, so construct directly). const mixed = [ - entry({ displayAt: ago20, recordId: "d1", connectionId: "cin_a", stream: "a" }), + entry({ connectionId: "cin_a", displayAt: ago20, recordId: "d1", stream: "a" }), { - ...entry({ recordId: "u1", connectionId: "cin_b", stream: "b" }), + ...entry({ connectionId: "cin_b", recordId: "u1", stream: "b" }), displayAt: "2026-06-19TBROKEN", } as ExplorerFeedEntry, - entry({ displayAt: "2026-06-19T14:41:00Z", recordId: "d2", connectionId: "cin_c", stream: "c" }), + entry({ connectionId: "cin_c", displayAt: "2026-06-19T14:41:00Z", recordId: "d2", stream: "c" }), ]; const mixedDay = groupFeedWithBursts(mixed, NOW_MS).find((g) => g.day === "2026-06-19"); assert.ok(mixedDay, "mixed same-day group exists"); @@ -628,9 +628,9 @@ test("burst order SCOPE GUARD: groupFeedDaysNoBursts (Upcoming path) stays FLAT // Upcoming arrives soonest-first (forward-chron). The units must MIRROR that input // order verbatim — never get newest-first re-sorted like the past feed. const feed = [ - entry({ displayAt: "2026-06-20T09:00:00Z", recordId: "soon", connectorId: "ynab", stream: "months" }), - entry({ displayAt: "2026-06-20T11:00:00Z", recordId: "later-same-day", connectorId: "ynab", stream: "months" }), - entry({ displayAt: "2026-07-01T09:00:00Z", recordId: "next-month", connectorId: "ynab", stream: "months" }), + entry({ connectorId: "ynab", displayAt: "2026-06-20T09:00:00Z", recordId: "soon", stream: "months" }), + entry({ connectorId: "ynab", displayAt: "2026-06-20T11:00:00Z", recordId: "later-same-day", stream: "months" }), + entry({ connectorId: "ynab", displayAt: "2026-07-01T09:00:00Z", recordId: "next-month", stream: "months" }), ]; const days = groupFeedDaysNoBursts(feed, NOW_MS); // Days stay in input (forward-chron) order, every unit a single, no bursts. @@ -660,17 +660,17 @@ test("burst order SCOPE GUARD: groupFeedDaysNoBursts (Upcoming path) stays FLAT test("burst order INVARIANT: scanning units top-to-bottom, latestAt is monotonic non-increasing (dated), undated last", () => { // A realistic mixed day: bursts + singles at scattered times, fed in non-display order. const feed = [ - entry({ displayAt: "2026-06-19T10:00:00Z", recordId: "s-10", connectionId: "cin_1a", stream: "a" }), + entry({ connectionId: "cin_1a", displayAt: "2026-06-19T10:00:00Z", recordId: "s-10", stream: "a" }), ...burstNewestAt("b1", "cin_b1", "2026-06-19T14:00:00Z"), - entry({ displayAt: "2026-06-19T12:30:00Z", recordId: "s-1230", connectionId: "cin_2a", stream: "b" }), + entry({ connectionId: "cin_2a", displayAt: "2026-06-19T12:30:00Z", recordId: "s-1230", stream: "b" }), ...burstNewestAt("b2", "cin_b2", "2026-06-19T08:15:00Z"), - { ...entry({ recordId: "u", connectionId: "cin_u", stream: "u" }), displayAt: "" } as ExplorerFeedEntry, + { ...entry({ connectionId: "cin_u", recordId: "u", stream: "u" }), displayAt: "" } as ExplorerFeedEntry, ...burstNewestAt("b3", "cin_b3", "2026-06-19T13:45:00Z"), ]; // Put the undated entry into the SAME day as the dated ones via a broken-but-prefixed // displayAt so it shares the 2026-06-19 bucket. feed[4] = { - ...entry({ recordId: "u", connectionId: "cin_u", stream: "u" }), + ...entry({ connectionId: "cin_u", recordId: "u", stream: "u" }), displayAt: "2026-06-19Tnonsense", } as ExplorerFeedEntry; const day = groupFeedWithBursts(feed, NOW_MS).find((g) => g.day === "2026-06-19"); diff --git a/apps/console/src/app/(console)/explore/explore-feed-grouping.ts b/apps/console/src/app/(console)/explore/explore-feed-grouping.ts index f70cfab19..77e8a6a3d 100644 --- a/apps/console/src/app/(console)/explore/explore-feed-grouping.ts +++ b/apps/console/src/app/(console)/explore/explore-feed-grouping.ts @@ -16,11 +16,11 @@ import type { ExplorerFeedEntry } from "@pdpp/operator-ui/components/views/explo // ─── Day label ─────────────────────────────────────────────────────────────── const DAY_FMT = new Intl.DateTimeFormat("en-US", { - weekday: "long", - year: "numeric", - month: "long", day: "numeric", + month: "long", timeZone: "UTC", + weekday: "long", + year: "numeric", }); export const MS_PER_DAY = 86_400_000; @@ -167,7 +167,7 @@ function entryLatestAt(e: ExplorerFeedEntry): string | null { */ function sortEntriesNewestFirst(entries: ExplorerFeedEntry[]): ExplorerFeedEntry[] { return entries - .map((entry, index) => ({ entry, index, at: entryLatestAt(entry) })) + .map((entry, index) => ({ at: entryLatestAt(entry), entry, index })) .sort((a, b) => { if (a.at === b.at) { return a.index - b.index; @@ -193,7 +193,7 @@ function sortEntriesNewestFirst(entries: ExplorerFeedEntry[]): ExplorerFeedEntry */ function orderDayUnits(units: DayRenderUnit[]): DayRenderUnit[] { return units - .map((unit, index) => ({ unit, index })) + .map((unit, index) => ({ index, unit })) .sort((a, b) => { const aAt = a.unit.latestAt; const bAt = b.unit.latestAt; @@ -246,14 +246,14 @@ function splitDayBursts(day: string, entries: ExplorerFeedEntry[], nowMs: number existing.entries.push(e); } else { // First member of this burst — reserve the burst's slot in feed order. - const burst: BurstGroup = { key: pk, entries: [e], preview: [], expanded: false }; + const burst: BurstGroup = { entries: [e], expanded: false, key: pk, preview: [] }; burstByKey.set(pk, burst); bursts.push(burst); - orderedUnits.push({ kind: "burst", burst, latestAt: null }); + orderedUnits.push({ burst, kind: "burst", latestAt: null }); } } else { singles.push(e); - orderedUnits.push({ kind: "single", entry: e, latestAt: entryLatestAt(e) }); + orderedUnits.push({ entry: e, kind: "single", latestAt: entryLatestAt(e) }); } } @@ -271,7 +271,7 @@ function splitDayBursts(day: string, entries: ExplorerFeedEntry[], nowMs: number } } - return { day, label: dayLabel(day, nowMs), singles, bursts, units: orderDayUnits(orderedUnits) }; + return { bursts, day, label: dayLabel(day, nowMs), singles, units: orderDayUnits(orderedUnits) }; } /** Group entries into ordered day buckets (insertion order preserved → already-sorted feed). */ @@ -325,11 +325,11 @@ export function groupFeedDaysNoBursts( // design — do NOT newest-first re-sort it. Every entry is a flat single, and // `units` preserves the incoming order verbatim (no orderDayUnits call). const units: DayRenderUnit[] = entries.map((entry) => ({ - kind: "single", entry, + kind: "single", latestAt: entryLatestAt(entry), })); - days.push({ day, label: dayLabel(day, nowMs), singles: entries, bursts: [], units }); + days.push({ bursts: [], day, label: dayLabel(day, nowMs), singles: entries, units }); } return days; } @@ -376,5 +376,5 @@ export function partitionFeedByTime(feed: readonly ExplorerFeedEntry[], nowMs: n const upcoming = groupDays(upcomingSorted, nowMs); const past = groupDays(pastEntries, nowMs); - return { upcoming, upcomingCount: futureEntries.length, past }; + return { past, upcoming, upcomingCount: futureEntries.length }; } diff --git a/apps/console/src/app/(console)/explore/explore-frontend-review-hold-fixes.test.ts b/apps/console/src/app/(console)/explore/explore-frontend-review-hold-fixes.test.ts index aea04a616..180c2f538 100644 --- a/apps/console/src/app/(console)/explore/explore-frontend-review-hold-fixes.test.ts +++ b/apps/console/src/app/(console)/explore/explore-frontend-review-hold-fixes.test.ts @@ -58,13 +58,13 @@ function makeTimelineRecord(over: { emitted_at?: string; }): ExploreTimelineRecord { return { - object: "timeline_record", connector_id: over.connector_id, connector_instance_id: over.connector_instance_id, - stream: over.stream ?? "records", - record_key: over.record_key ?? "rec-1", - emitted_at: over.emitted_at ?? "2026-01-01T00:00:00Z", data: { title: "test" }, + emitted_at: over.emitted_at ?? "2026-01-01T00:00:00Z", + object: "timeline_record", + record_key: over.record_key ?? "rec-1", + stream: over.stream ?? "records", }; } @@ -76,12 +76,12 @@ function makeTimelinePage( } ): ExploreTimelinePage { return { - object: "list", data: records, has_more: opts?.has_more ?? false, + new_since_snapshot: 0, next_cursor: opts?.next_cursor ?? null, + object: "list", snapshot_at: "2026-06-19T00:00:00Z", - new_since_snapshot: 0, }; } @@ -93,13 +93,13 @@ function makeHit(over: { emitted_at?: string; }): SearchResultHit { return { - connector_id: over.connector_id, connection_id: over.connection_id, - stream: over.stream ?? "records", - record_key: over.record_key ?? "rec-1", + connector_id: over.connector_id, emitted_at: over.emitted_at ?? "2026-01-01T00:00:00Z", matched_fields: [], object: "search_result", + record_key: over.record_key ?? "rec-1", + stream: over.stream ?? "records", }; } @@ -132,32 +132,32 @@ function notStubbed(name: string): Promise { function makeDataSource(overrides: Partial): DashboardDataSource { const stub: DashboardDataSource = { - kind: "sandbox" as const, aggregateRecordsByTime: () => notStubbed("aggregateRecordsByTime"), - listExploreRecordBuckets: () => notStubbed("listExploreRecordBuckets"), - isHybridRetrievalAdvertised: () => Promise.resolve(false), - isSemanticRetrievalAdvertised: () => Promise.resolve(false), - listConnectorSummaries: () => notStubbed("listConnectorSummaries"), - listConnectorManifests: () => notStubbed("listConnectorManifests"), - searchRecordsLexical: () => notStubbed("searchRecordsLexical"), - searchRecordsHybrid: () => notStubbed("searchRecordsHybrid"), - searchRecordsSemantic: () => notStubbed("searchRecordsSemantic"), - queryRecords: () => notStubbed("queryRecords"), - getRecord: () => notStubbed("getRecord"), getConnectorOverview: () => notStubbed("getConnectorOverview"), + getDatasetSummary: () => notStubbed("getDatasetSummary"), + getDeploymentDiagnostics: () => notStubbed("getDeploymentDiagnostics"), + getGrantTimeline: () => notStubbed("getGrantTimeline"), + getRecord: () => notStubbed("getRecord"), + getRunTimeline: () => notStubbed("getRunTimeline"), getStreamMetadata: () => notStubbed("getStreamMetadata"), getTraceTimeline: () => notStubbed("getTraceTimeline"), + isHybridRetrievalAdvertised: () => Promise.resolve(false), + isSemanticRetrievalAdvertised: () => Promise.resolve(false), + kind: "sandbox" as const, + listConnectorManifests: () => notStubbed("listConnectorManifests"), + listConnectorSummaries: () => notStubbed("listConnectorSummaries"), + listExploreRecordBuckets: () => notStubbed("listExploreRecordBuckets"), + listExploreTimeline: () => Promise.resolve(makeTimelinePage([])), listGrants: () => notStubbed("listGrants"), listPendingApprovals: () => notStubbed("listPendingApprovals"), listRuns: () => notStubbed("listRuns"), listStreams: () => notStubbed("listStreams"), listTraces: () => notStubbed("listTraces"), + queryRecords: () => notStubbed("queryRecords"), refSearch: () => notStubbed("refSearch"), - listExploreTimeline: () => Promise.resolve(makeTimelinePage([])), - getDatasetSummary: () => notStubbed("getDatasetSummary"), - getDeploymentDiagnostics: () => notStubbed("getDeploymentDiagnostics"), - getGrantTimeline: () => notStubbed("getGrantTimeline"), - getRunTimeline: () => notStubbed("getRunTimeline"), + searchRecordsHybrid: () => notStubbed("searchRecordsHybrid"), + searchRecordsLexical: () => notStubbed("searchRecordsLexical"), + searchRecordsSemantic: () => notStubbed("searchRecordsSemantic"), ...overrides, }; return stub; @@ -182,35 +182,35 @@ test("F1 (P1): recent lens filters by selected connection — YNAB records do no connection_id: "cin_amazon_1", connector_id: "amazon", connector_instance_id: "cin_amazon_1", - streams: ["orders"], display_name: "Amazon", + streams: ["orders"], }); const ynabSummary = makeSummary({ connection_id: "cin_ynab_1", connector_id: "ynab", connector_instance_id: "cin_ynab_1", - streams: ["transactions"], display_name: "YNAB", + streams: ["transactions"], }); // The endpoint returns records from BOTH connections (cross-source leak scenario). const amazonRecord = makeTimelineRecord({ connector_id: "amazon", connector_instance_id: "cin_amazon_1", - stream: "orders", record_key: "order-1", + stream: "orders", }); const ynabRecord = makeTimelineRecord({ connector_id: "ynab", connector_instance_id: "cin_ynab_1", - stream: "transactions", record_key: "txn-1", + stream: "transactions", }); const ds = makeDataSource({ - listConnectorSummaries: () => Promise.resolve(summaryListResponse([amazonSummary, ynabSummary])), listConnectorManifests: () => Promise.resolve([makeManifest("amazon", ["orders"]), makeManifest("ynab", ["transactions"])]), + listConnectorSummaries: () => Promise.resolve(summaryListResponse([amazonSummary, ynabSummary])), listExploreTimeline: (opts) => { assert.deepEqual( opts?.connectionIds, @@ -257,30 +257,23 @@ test("F2 (P0): single-stream Most-recent calls lexical (not queryRecords) — on connection_id: "cin_amazon_1", connector_id: "amazon", connector_instance_id: "cin_amazon_1", - streams: ["orders"], display_name: "Amazon", + streams: ["orders"], }); // Only one matching hit (connector+stream all same so streamDoor fires). const matchingHit = makeHit({ + connection_id: "cin_amazon_1", connector_id: "amazon", - stream: "orders", record_key: "matching-order", - connection_id: "cin_amazon_1", + stream: "orders", }); let queryRecordsCalled = false; let lexicalCalledWithStream = false; const ds = makeDataSource({ - listConnectorSummaries: () => Promise.resolve(summaryListResponse([summary])), listConnectorManifests: () => Promise.resolve([makeManifest("amazon", ["orders"])]), - // Lexical must be called for BOTH stream-door probe AND the display results. - searchRecordsLexical: (_q, opts) => { - if (opts?.streams?.includes("orders")) { - lexicalCalledWithStream = true; - } - return Promise.resolve(makeLexicalPage([matchingHit], { has_more: false })); - }, + listConnectorSummaries: () => Promise.resolve(summaryListResponse([summary])), // PRE-FIX: assembler called queryRecords here. If we stub it to NOT throw, // all records (matching or not) would be returned. We stub it to throw so // the pre-fix behavior is caught as a test failure. @@ -290,6 +283,13 @@ test("F2 (P0): single-stream Most-recent calls lexical (not queryRecords) — on new Error("queryRecords must not be called for Most-recent single-stream (F2 reproduce bug)") ); }, + // Lexical must be called for BOTH stream-door probe AND the display results. + searchRecordsLexical: (_q, opts) => { + if (opts?.streams?.includes("orders")) { + lexicalCalledWithStream = true; + } + return Promise.resolve(makeLexicalPage([matchingHit], { has_more: false })); + }, }); const result = await assembleExplorerData({ q: "hdmi cable", search_sort: "recent" }, ds, "https://rs.test"); @@ -327,26 +327,26 @@ test("F2 (P0): multi-stream Most-recent wires lexical cursor — searchHasMore r const summaryA = makeSummary({ connection_id: "cin_ynab_1", connector_id: "ynab", streams: ["transactions"] }); const summaryB = makeSummary({ connection_id: "cin_amazon_1", connector_id: "amazon", streams: ["orders"] }); const hitA = makeHit({ - connector_id: "ynab", - stream: "transactions", connection_id: "cin_ynab_1", + connector_id: "ynab", record_key: "ynab-rec", + stream: "transactions", }); const hitB = makeHit({ - connector_id: "amazon", - stream: "orders", connection_id: "cin_amazon_1", + connector_id: "amazon", record_key: "amz-rec", + stream: "orders", }); const ds = makeDataSource({ - listConnectorSummaries: () => Promise.resolve(summaryListResponse([summaryA, summaryB])), listConnectorManifests: () => Promise.resolve([makeManifest("ynab", ["transactions"]), makeManifest("amazon", ["orders"])]), + listConnectorSummaries: () => Promise.resolve(summaryListResponse([summaryA, summaryB])), + queryRecords: () => Promise.reject(new Error("queryRecords must not be called for multi-stream Most-recent")), // Lexical returns has_more=true with a cursor to simulate a deep result set. searchRecordsLexical: () => Promise.resolve(makeLexicalPage([hitA, hitB], { has_more: true, next_cursor: "lex-cursor-99" })), - queryRecords: () => Promise.reject(new Error("queryRecords must not be called for multi-stream Most-recent")), }); const result = await assembleExplorerData({ q: "payment", search_sort: "recent" }, ds, "https://rs.test"); @@ -391,28 +391,28 @@ test("F3 (P1): timelineRecordToEntry uses connector_instance_id to resolve corre connection_id: "cin_ynab_personal", connector_id: "ynab", connector_instance_id: "cin_ynab_personal", - streams: ["transactions"], display_name: "YNAB Personal", + streams: ["transactions"], }); const workSummary = makeSummary({ connection_id: "cin_ynab_work", connector_id: "ynab", connector_instance_id: "cin_ynab_work", - streams: ["transactions"], display_name: "YNAB Work", + streams: ["transactions"], }); // Record belongs to the WORK instance. const workRecord = makeTimelineRecord({ connector_id: "ynab", // type: "ynab" connector_instance_id: "cin_ynab_work", // instance: work - stream: "transactions", record_key: "work-txn-1", + stream: "transactions", }); const ds = makeDataSource({ - listConnectorSummaries: () => Promise.resolve(summaryListResponse([personalSummary, workSummary])), listConnectorManifests: () => Promise.resolve([makeManifest("ynab", ["transactions"])]), + listConnectorSummaries: () => Promise.resolve(summaryListResponse([personalSummary, workSummary])), // The endpoint returns a record whose connector_instance_id = "cin_ynab_work". listExploreTimeline: () => Promise.resolve(makeTimelinePage([workRecord])), }); @@ -465,21 +465,21 @@ test("F3 (P1): connectionId falls back to connector_instance_id (not connector_i connection_id: "cin_amazon_1", connector_id: "amazon", connector_instance_id: "cin_amazon_1", - streams: ["orders"], display_name: "Amazon", + streams: ["orders"], }); // Record from "twitter" — no twitter summary exists in the owner's connections. const twitterRecord = makeTimelineRecord({ connector_id: "twitter", connector_instance_id: "cin_twitter_unknown", - stream: "tweets", record_key: "tweet-999", + stream: "tweets", }); const ds = makeDataSource({ - listConnectorSummaries: () => Promise.resolve(summaryListResponse([amazonSummary])), listConnectorManifests: () => Promise.resolve([makeManifest("amazon", ["orders"])]), + listConnectorSummaries: () => Promise.resolve(summaryListResponse([amazonSummary])), listExploreTimeline: () => Promise.resolve(makeTimelinePage([twitterRecord])), }); diff --git a/apps/console/src/app/(console)/explore/explore-grammar.test.ts b/apps/console/src/app/(console)/explore/explore-grammar.test.ts index 83e1b2ed1..5c35e12bc 100644 --- a/apps/console/src/app/(console)/explore/explore-grammar.test.ts +++ b/apps/console/src/app/(console)/explore/explore-grammar.test.ts @@ -84,27 +84,27 @@ const STREAM_NOT_BUDGET_RE = /stream!=budget_months/; test("CHIP == OPERATOR for exclusion: the facet 'is not' and -con: compile to the SAME query", () => { // Typing the operator -con:ynab: const viaOperator = buildCompiledQuery({ + limit: 50, + order: "newest", parsed: parseQuery("-con:ynab"), selectedConnectionIds: [], selectedStreams: [], serverFilterableFields: new Set(), since: "", until: "", - order: "newest", - limit: 50, }); // Clicking the "is not" chip for the same connection (excludedConnectionIds): const viaChip = buildCompiledQuery({ + excludedConnectionIds: ["ynab"], + excludedStreams: [], + limit: 50, + order: "newest", parsed: parseQuery(""), selectedConnectionIds: [], selectedStreams: [], - excludedConnectionIds: ["ynab"], - excludedStreams: [], serverFilterableFields: new Set(), since: "", until: "", - order: "newest", - limit: 50, }); // Both produce the identical exclusion param (chip == operator). assert.match(viaOperator, CONNECTION_NOT_YNAB_RE); @@ -157,25 +157,25 @@ test("liftDateTokens: last-write-wins on a repeated operator (no stacking)", () test("buildCompiledQuery renders excluded streams as stream!= (chip == -stream: operator)", () => { const viaOperator = buildCompiledQuery({ + limit: 50, + order: "newest", parsed: parseQuery("-stream:budget_months"), selectedConnectionIds: [], selectedStreams: [], serverFilterableFields: new Set(), since: "", until: "", - order: "newest", - limit: 50, }); const viaChip = buildCompiledQuery({ + excludedStreams: ["budget_months"], + limit: 50, + order: "newest", parsed: parseQuery(""), selectedConnectionIds: [], selectedStreams: [], - excludedStreams: ["budget_months"], serverFilterableFields: new Set(), since: "", until: "", - order: "newest", - limit: 50, }); assert.match(viaOperator, STREAM_NOT_BUDGET_RE); assert.match(viaChip, STREAM_NOT_BUDGET_RE); @@ -202,6 +202,8 @@ test("hasClientSideTokens treats a declared exact-filterable field as server-sid test("buildCompiledQuery renders server params plainly and undeclared field tokens behind a comment", () => { const line = buildCompiledQuery({ + limit: 50, + order: "newest", parsed: parseQuery("con:gmail stream:messages has:image merchant:coffee coffee"), selectedConnectionIds: [], selectedStreams: [], @@ -209,8 +211,6 @@ test("buildCompiledQuery renders server params plainly and undeclared field toke serverFilterableFields: new Set(), since: "", until: "", - order: "newest", - limit: 50, }); assert.match(line, GET_RECORDS_RE); assert.match(line, CONNECTION_GMAIL_RE); @@ -228,6 +228,8 @@ test("buildCompiledQuery renders server params plainly and undeclared field toke test("buildCompiledQuery promotes a declared exact-filterable field:value to a server filter[] param", () => { const line = buildCompiledQuery({ + limit: 50, + order: "newest", parsed: parseQuery("merchant:coffee has:image"), selectedConnectionIds: [], selectedStreams: [], @@ -235,8 +237,6 @@ test("buildCompiledQuery promotes a declared exact-filterable field:value to a s serverFilterableFields: new Set(["merchant"]), since: "", until: "", - order: "newest", - limit: 50, }); // merchant:coffee renders as a real server param in the server section. const [serverPart] = line.split("# client-side:"); @@ -251,14 +251,14 @@ test("buildCompiledQuery promotes a declared exact-filterable field:value to a s test("buildCompiledQuery prefers facet-selected ids and maps before/after to since/until", () => { const line = buildCompiledQuery({ + limit: 50, + order: "oldest", parsed: parseQuery("before:2026-06-11 after:2026-06-10"), selectedConnectionIds: ["conn_abc"], selectedStreams: ["transactions"], serverFilterableFields: new Set(), since: "", until: "", - order: "oldest", - limit: 50, }); assert.match(line, CONNECTION_ABC_RE); assert.match(line, STREAM_TX_RE); diff --git a/apps/console/src/app/(console)/explore/explore-grammar.ts b/apps/console/src/app/(console)/explore/explore-grammar.ts index b71f767a1..afff4f3e3 100644 --- a/apps/console/src/app/(console)/explore/explore-grammar.ts +++ b/apps/console/src/app/(console)/explore/explore-grammar.ts @@ -105,84 +105,84 @@ function applyKvToken(out: ParsedQuery, tok: string, negated: boolean, key: stri const lower = value.toLowerCase(); if (negated && key === "con") { out.conNot = lower; - out.tokens.push({ raw: tok, label: `not in ${value}` }); + out.tokens.push({ label: `not in ${value}`, raw: tok }); return; } if (negated && key === "stream") { out.streamNot = lower; - out.tokens.push({ raw: tok, label: `not stream: ${value}` }); + out.tokens.push({ label: `not stream: ${value}`, raw: tok }); return; } switch (key) { case "con": out.con = lower; - out.tokens.push({ raw: tok, label: `in ${value}` }); + out.tokens.push({ label: `in ${value}`, raw: tok }); break; case "stream": out.stream = lower; - out.tokens.push({ raw: tok, label: `stream: ${value}` }); + out.tokens.push({ label: `stream: ${value}`, raw: tok }); break; case "role": out.role = lower; - out.tokens.push({ raw: tok, label: `role: ${value}` }); + out.tokens.push({ label: `role: ${value}`, raw: tok }); break; case "has": if (lower === "image") { out.hasImage = true; - out.tokens.push({ raw: tok, label: "has image" }); + out.tokens.push({ label: "has image", raw: tok }); } else if (lower === "link") { out.hasLink = true; - out.tokens.push({ raw: tok, label: "has link" }); + out.tokens.push({ label: "has link", raw: tok }); } else { out.fields.push({ key, value: lower }); - out.tokens.push({ raw: tok, label: `${key}: ${value}` }); + out.tokens.push({ label: `${key}: ${value}`, raw: tok }); } break; case "is": if (lower === "folded") { out.folded = true; - out.tokens.push({ raw: tok, label: "folded" }); + out.tokens.push({ label: "folded", raw: tok }); } else { out.fields.push({ key, value: lower }); - out.tokens.push({ raw: tok, label: `${key}: ${value}` }); + out.tokens.push({ label: `${key}: ${value}`, raw: tok }); } break; case "before": out.before = value; - out.tokens.push({ raw: tok, label: `before ${value}` }); + out.tokens.push({ label: `before ${value}`, raw: tok }); break; case "after": out.after = value; - out.tokens.push({ raw: tok, label: `after ${value}` }); + out.tokens.push({ label: `after ${value}`, raw: tok }); break; default: out.fields.push({ key, value: lower }); - out.tokens.push({ raw: tok, label: `${key}: ${value}` }); + out.tokens.push({ label: `${key}: ${value}`, raw: tok }); } } /** Parse a raw query string into structured tokens. Pure; never throws. */ export function parseQuery(input: string): ParsedQuery { const out: ParsedQuery = { - text: [], + after: null, + before: null, con: null, conNot: null, - stream: null, - streamNot: null, - role: null, + fields: [], + folded: false, hasImage: false, hasLink: false, - folded: false, - before: null, - after: null, - fields: [], + role: null, + stream: null, + streamNot: null, + text: [], tokens: [], }; for (const tok of input.trim().split(WHITESPACE_RE).filter(Boolean)) { const m = tok.match(KV_RE); if (!m) { out.text.push(tok.toLowerCase()); - out.tokens.push({ raw: tok, label: tok }); + out.tokens.push({ label: tok, raw: tok }); continue; } applyKvToken(out, tok, m[1] === "-", (m[2] ?? "").toLowerCase(), m[3] ?? ""); @@ -263,10 +263,10 @@ export interface QueryFacetLift { */ export function liftFacetTokens(query: string): QueryFacetLift { const lift: QueryFacetLift = { - includeConnections: [], excludeConnections: [], - includeStreams: [], excludeStreams: [], + includeConnections: [], + includeStreams: [], rest: "", }; const kept: string[] = []; @@ -405,7 +405,7 @@ function splitFieldFilters( clientFields.push(f); } } - return { serverFilters, clientFields }; + return { clientFields, serverFilters }; } /** @@ -457,18 +457,18 @@ export function buildCompiledQuery(input: CompiledQueryInput): string { // Connection/stream include + exclude scope (the chip toggle and the operator // render identically; exclusion is a first-class `!=` scope param). ...renderScopeParams({ - include: selectedConnectionIds, exclude: input.excludedConnectionIds ?? [], - tokenInclude: parsed.con, - tokenExclude: parsed.conNot, + include: selectedConnectionIds, param: "connection", + tokenExclude: parsed.conNot, + tokenInclude: parsed.con, }), ...renderScopeParams({ - include: selectedStreams, exclude: input.excludedStreams ?? [], - tokenInclude: parsed.stream, - tokenExclude: parsed.streamNot, + include: selectedStreams, param: "stream", + tokenExclude: parsed.streamNot, + tokenInclude: parsed.stream, }), ]; diff --git a/apps/console/src/app/(console)/explore/explore-navigation.test.ts b/apps/console/src/app/(console)/explore/explore-navigation.test.ts index bb5af4460..388a949d0 100644 --- a/apps/console/src/app/(console)/explore/explore-navigation.test.ts +++ b/apps/console/src/app/(console)/explore/explore-navigation.test.ts @@ -41,7 +41,6 @@ function accumulatingState(overrides: Partial = {}): NavigateStat cursorTrail: ["cursor-p2"], excludeConnectionIds: [], excludeStreams: [], - upcomingTrail: ["ucursor-u1"], order: "newest", query: "", searchSort: "relevance", @@ -49,6 +48,7 @@ function accumulatingState(overrides: Partial = {}): NavigateStat snapshotAnchor: "2026-06-20T00:00:00Z", streams: ["orders"], until: "", + upcomingTrail: ["ucursor-u1"], ...overrides, }; } diff --git a/apps/console/src/app/(console)/explore/explore-navigation.ts b/apps/console/src/app/(console)/explore/explore-navigation.ts index 73681a6df..283716d7d 100644 --- a/apps/console/src/app/(console)/explore/explore-navigation.ts +++ b/apps/console/src/app/(console)/explore/explore-navigation.ts @@ -264,23 +264,23 @@ export function buildNavigateHref(explorePath: string, state: NavigateState, opt // clearCursor resets it to the head. const nextUpcomingTrail = nextAccumulatingTrail(resetFeed, state.upcomingTrail, opts.appendUpcomingCursor); const href = buildHref(explorePath, { - query: opts.query ?? state.query, + // Anchor survives ONLY for non-feed-defining moves (Load-more, peek, order); + // forwardAnchor is already undefined when the feed is reset. + anchor: forwardAnchor, connectionIds: opts.connectionIds ?? state.connectionIds, - excludeConnectionIds: opts.excludeConnectionIds ?? state.excludeConnectionIds, - streams: opts.streams ?? state.streams, - excludeStreams: opts.excludeStreams ?? state.excludeStreams, - since: opts.since ?? state.since, - until: opts.until ?? state.until, - peek: opts.peek, - searchSort: opts.searchSort ?? (state.searchSort === "recent" ? "recent" : undefined), cursor: opts.clearCursor ? undefined : opts.cursor, // Trail is preserved ONLY for Load-more (appendCursor); every other navigation // leaves nextTrail undefined → the `cursors` param is dropped (reset to page 1). cursors: nextTrail, + excludeConnectionIds: opts.excludeConnectionIds ?? state.excludeConnectionIds, + excludeStreams: opts.excludeStreams ?? state.excludeStreams, + peek: opts.peek, + query: opts.query ?? state.query, + searchSort: opts.searchSort ?? (state.searchSort === "recent" ? "recent" : undefined), + since: opts.since ?? state.since, + streams: opts.streams ?? state.streams, + until: opts.until ?? state.until, upcomingCursors: nextUpcomingTrail, - // Anchor survives ONLY for non-feed-defining moves (Load-more, peek, order); - // forwardAnchor is already undefined when the feed is reset. - anchor: forwardAnchor, }); const nextOrder = opts.order ?? state.order; return nextOrder === "oldest" ? `${href}${href.includes("?") ? "&" : "?"}order=oldest` : href; diff --git a/apps/console/src/app/(console)/explore/explore-p2-search-sort.test.ts b/apps/console/src/app/(console)/explore/explore-p2-search-sort.test.ts index 8ed473758..813022e3f 100644 --- a/apps/console/src/app/(console)/explore/explore-p2-search-sort.test.ts +++ b/apps/console/src/app/(console)/explore/explore-p2-search-sort.test.ts @@ -66,13 +66,13 @@ function makeHit(over: { emitted_at?: string; }): SearchResultHit { return { - connector_id: over.connector_id, connection_id: over.connection_id, - stream: over.stream ?? "records", - record_key: over.record_key ?? "rec-1", + connector_id: over.connector_id, emitted_at: over.emitted_at ?? "2026-01-01T00:00:00Z", matched_fields: [], object: "search_result", + record_key: over.record_key ?? "rec-1", + stream: over.stream ?? "records", }; } @@ -99,11 +99,11 @@ function makeLexicalPage( function makeRecordsPage(ids: string[], opts?: { has_more?: boolean; next_cursor?: string }): RecordsPage { return { data: ids.map((id) => ({ + data: {}, + emitted_at: "2026-01-01T00:00:00Z", id, object: "record" as const, stream: "transactions", - emitted_at: "2026-01-01T00:00:00Z", - data: {}, })), has_more: opts?.has_more ?? false, ...(opts?.next_cursor ? { next_cursor: opts.next_cursor } : {}), @@ -132,42 +132,42 @@ const notStubbed = (name: string) => () => Promise.reject(new Error(`${name} not function makeDataSource(overrides: Partial): DashboardDataSource { const stub: DashboardDataSource = { - kind: "sandbox" as const, aggregateRecordsByTime: notStubbed("aggregateRecordsByTime"), - listExploreRecordBuckets: notStubbed("listExploreRecordBuckets"), - isHybridRetrievalAdvertised: () => Promise.resolve(false), - isSemanticRetrievalAdvertised: () => Promise.resolve(false), - listConnectorSummaries: notStubbed("listConnectorSummaries"), - listConnectorManifests: notStubbed("listConnectorManifests"), - searchRecordsLexical: notStubbed("searchRecordsLexical"), - searchRecordsHybrid: notStubbed("searchRecordsHybrid"), - searchRecordsSemantic: notStubbed("searchRecordsSemantic"), - queryRecords: notStubbed("queryRecords"), - getRecord: notStubbed("getRecord"), getConnectorOverview: notStubbed("getConnectorOverview"), + getDatasetSummary: notStubbed("getDatasetSummary"), + getDeploymentDiagnostics: notStubbed("getDeploymentDiagnostics"), + getGrantTimeline: notStubbed("getGrantTimeline"), + getRecord: notStubbed("getRecord"), + getRunTimeline: notStubbed("getRunTimeline"), getStreamMetadata: notStubbed("getStreamMetadata"), getTraceTimeline: notStubbed("getTraceTimeline"), - listGrants: notStubbed("listGrants"), - listPendingApprovals: notStubbed("listPendingApprovals"), - listRuns: notStubbed("listRuns"), - listStreams: notStubbed("listStreams"), - listTraces: notStubbed("listTraces"), - refSearch: notStubbed("refSearch"), + isHybridRetrievalAdvertised: () => Promise.resolve(false), + isSemanticRetrievalAdvertised: () => Promise.resolve(false), + kind: "sandbox" as const, + listConnectorManifests: notStubbed("listConnectorManifests"), + listConnectorSummaries: notStubbed("listConnectorSummaries"), + listExploreRecordBuckets: notStubbed("listExploreRecordBuckets"), // The recent lens is the merged-timeline endpoint (single path; no fan-out // fallback). Return an empty page so the empty-query feed assembles cleanly. listExploreTimeline: () => Promise.resolve({ - object: "list" as const, data: [], has_more: false, + new_since_snapshot: 0, next_cursor: null, + object: "list" as const, snapshot_at: new Date(0).toISOString(), - new_since_snapshot: 0, }), - getDatasetSummary: notStubbed("getDatasetSummary"), - getDeploymentDiagnostics: notStubbed("getDeploymentDiagnostics"), - getGrantTimeline: notStubbed("getGrantTimeline"), - getRunTimeline: notStubbed("getRunTimeline"), + listGrants: notStubbed("listGrants"), + listPendingApprovals: notStubbed("listPendingApprovals"), + listRuns: notStubbed("listRuns"), + listStreams: notStubbed("listStreams"), + listTraces: notStubbed("listTraces"), + queryRecords: notStubbed("queryRecords"), + refSearch: notStubbed("refSearch"), + searchRecordsHybrid: notStubbed("searchRecordsHybrid"), + searchRecordsLexical: notStubbed("searchRecordsLexical"), + searchRecordsSemantic: notStubbed("searchRecordsSemantic"), ...overrides, }; return stub; @@ -177,11 +177,11 @@ function makeDataSource(overrides: Partial): DashboardDataS test("P2 lexical Most-relevant: has_more and next_cursor wired into searchHasMore + searchNextCursor", async () => { const summary = makeSummary({ connection_id: "ynab-1", connector_id: "ynab", streams: ["transactions"] }); - const hit = makeHit({ connector_id: "ynab", stream: "transactions", connection_id: "ynab-1" }); + const hit = makeHit({ connection_id: "ynab-1", connector_id: "ynab", stream: "transactions" }); const ds = makeDataSource({ - listConnectorSummaries: () => Promise.resolve(summaryListResponse([summary])), - listConnectorManifests: () => Promise.resolve([makeManifest("ynab", ["transactions"])]), isHybridRetrievalAdvertised: () => Promise.resolve(false), + listConnectorManifests: () => Promise.resolve([makeManifest("ynab", ["transactions"])]), + listConnectorSummaries: () => Promise.resolve(summaryListResponse([summary])), // Recall proven complete: the lexical page is exhaustively pageable, so the // descriptor is keyword_pageable and the cursor trail is honest. searchRecordsLexical: async () => @@ -202,23 +202,23 @@ test("P2 lexical Most-relevant: has_more and next_cursor wired into searchHasMor test("P2 lexical Load-more: passing cursor= calls searchRecordsLexical with that cursor", async () => { const summary = makeSummary({ connection_id: "ynab-1", connector_id: "ynab", streams: ["transactions"] }); const hitP1 = makeHit({ + connection_id: "ynab-1", connector_id: "ynab", - stream: "transactions", record_key: "p1-rec", - connection_id: "ynab-1", + stream: "transactions", }); const hitP2 = makeHit({ + connection_id: "ynab-1", connector_id: "ynab", - stream: "transactions", record_key: "p2-rec", - connection_id: "ynab-1", + stream: "transactions", }); const capturedCursors: (string | undefined)[] = []; const ds = makeDataSource({ - listConnectorSummaries: () => Promise.resolve(summaryListResponse([summary])), - listConnectorManifests: () => Promise.resolve([makeManifest("ynab", ["transactions"])]), isHybridRetrievalAdvertised: () => Promise.resolve(false), + listConnectorManifests: () => Promise.resolve([makeManifest("ynab", ["transactions"])]), + listConnectorSummaries: () => Promise.resolve(summaryListResponse([summary])), searchRecordsLexical: (_q, opts) => { capturedCursors.push(opts?.cursor); if (opts?.cursor === "cursor-page2") { @@ -237,7 +237,7 @@ test("P2 lexical Load-more: passing cursor= calls searchRecordsLexical with that assert.equal(page1.feed[0]?.recordId, "p1-rec"); // Page 2: forward the cursor from page 1 - const page2 = await assembleExplorerData({ q: "coffee", cursor: "cursor-page2" }, ds, "https://rs.test"); + const page2 = await assembleExplorerData({ cursor: "cursor-page2", q: "coffee" }, ds, "https://rs.test"); assert.equal(page2.searchHasMore, false); assert.equal(page2.searchNextCursor, null); assert.equal(page2.feed[0]?.recordId, "p2-rec"); @@ -256,29 +256,29 @@ test("P2 Most-recent single-stream: lexical is called (not queryRecords); return const summary = makeSummary({ connection_id: "ynab-1", connector_id: "ynab", streams: ["transactions"] }); // All hits from the same connector+stream so detectSingleStreamDoor fires. const hit = makeHit({ - connector_id: "ynab", - stream: "transactions", connection_id: "ynab-1", + connector_id: "ynab", record_key: "rec-last", + stream: "transactions", }); let queryRecordsCalled = false; const lexicalCalls: Array<{ streams?: string[]; cursor?: string }> = []; const ds = makeDataSource({ - listConnectorSummaries: () => Promise.resolve(summaryListResponse([summary])), - listConnectorManifests: () => Promise.resolve([makeManifest("ynab", ["transactions"])]), isHybridRetrievalAdvertised: () => Promise.resolve(false), - // All lexical calls (stream-door probe + display fetch) go here. - searchRecordsLexical: (_q, opts) => { - lexicalCalls.push({ streams: opts?.streams, cursor: opts?.cursor }); - return Promise.resolve(makeLexicalPage([hit], { has_more: false })); - }, + listConnectorManifests: () => Promise.resolve([makeManifest("ynab", ["transactions"])]), + listConnectorSummaries: () => Promise.resolve(summaryListResponse([summary])), // queryRecords must NOT be called (F2 fix: was the display path, returned ALL records). queryRecords: () => { queryRecordsCalled = true; return Promise.reject(new Error("queryRecords must not be called for Most-recent single-stream (F2 fix)")); }, + // All lexical calls (stream-door probe + display fetch) go here. + searchRecordsLexical: (_q, opts) => { + lexicalCalls.push({ cursor: opts?.cursor, streams: opts?.streams }); + return Promise.resolve(makeLexicalPage([hit], { has_more: false })); + }, }); const result = await assembleExplorerData({ q: "rent", search_sort: "recent" }, ds, "https://rs.test"); @@ -305,19 +305,21 @@ test("P2 Most-recent single-stream: lexical is called (not queryRecords); return test("P2 Most-recent single-stream: second page (cursor forwarded to lexical) reaches last matching record", async () => { const summary = makeSummary({ connection_id: "ynab-1", connector_id: "ynab", streams: ["transactions"] }); - const hit = makeHit({ connector_id: "ynab", stream: "transactions", connection_id: "ynab-1", record_key: "rec-p1" }); + const hit = makeHit({ connection_id: "ynab-1", connector_id: "ynab", record_key: "rec-p1", stream: "transactions" }); const hitEnd = makeHit({ - connector_id: "ynab", - stream: "transactions", connection_id: "ynab-1", + connector_id: "ynab", record_key: "rec-end", + stream: "transactions", }); const lexicalCursors: (string | undefined)[] = []; const ds = makeDataSource({ - listConnectorSummaries: () => Promise.resolve(summaryListResponse([summary])), - listConnectorManifests: () => Promise.resolve([makeManifest("ynab", ["transactions"])]), isHybridRetrievalAdvertised: () => Promise.resolve(false), + listConnectorManifests: () => Promise.resolve([makeManifest("ynab", ["transactions"])]), + listConnectorSummaries: () => Promise.resolve(summaryListResponse([summary])), + // queryRecords must NOT be called (F2 fix). + queryRecords: () => Promise.reject(new Error("queryRecords must not be called (F2 fix)")), searchRecordsLexical: (_q, opts) => { // Track the cursor arg passed to lexical (skip the probe call which has no cursor). if (opts?.streams) { @@ -329,8 +331,6 @@ test("P2 Most-recent single-stream: second page (cursor forwarded to lexical) re // Probe call (no streams) or page 1 display call returns hit with cursor. return Promise.resolve(makeLexicalPage([hit], { has_more: true, next_cursor: "lex-p2" })); }, - // queryRecords must NOT be called (F2 fix). - queryRecords: () => Promise.reject(new Error("queryRecords must not be called (F2 fix)")), }); // Page 1 of Most-recent @@ -340,7 +340,7 @@ test("P2 Most-recent single-stream: second page (cursor forwarded to lexical) re // Page 2: forward the lexical cursor (F2 fix: was a keyset cursor to queryRecords) const page2 = await assembleExplorerData( - { q: "rent", search_sort: "recent", cursor: "lex-p2" }, + { cursor: "lex-p2", q: "rent", search_sort: "recent" }, ds, "https://rs.test" ); @@ -360,13 +360,13 @@ test("P2 Most-recent single-stream: second page (cursor forwarded to lexical) re test("P2 hybrid Most-relevant: searchHasMore false and searchNextCursor null (no fake Load-more)", async () => { const summary = makeSummary({ connection_id: "gmail-1", connector_id: "gmail", streams: ["messages"] }); - const hit1 = makeHit({ connector_id: "gmail", stream: "messages", record_key: "msg-1", connection_id: "gmail-1" }); - const hit2 = makeHit({ connector_id: "gmail", stream: "messages", record_key: "msg-2", connection_id: "gmail-1" }); + const hit1 = makeHit({ connection_id: "gmail-1", connector_id: "gmail", record_key: "msg-1", stream: "messages" }); + const hit2 = makeHit({ connection_id: "gmail-1", connector_id: "gmail", record_key: "msg-2", stream: "messages" }); const ds = makeDataSource({ - listConnectorSummaries: () => Promise.resolve(summaryListResponse([summary])), - listConnectorManifests: () => Promise.resolve([makeManifest("gmail", ["messages"])]), isHybridRetrievalAdvertised: () => Promise.resolve(true), + listConnectorManifests: () => Promise.resolve([makeManifest("gmail", ["messages"])]), + listConnectorSummaries: () => Promise.resolve(summaryListResponse([summary])), searchRecordsHybrid: () => Promise.resolve(makeLexicalPage([hit1, hit2], { has_more: false })), // Hybrid mode should NOT call lexical searchRecordsLexical: () => Promise.reject(new Error("lexical must not be called when hybrid is used")), @@ -390,7 +390,7 @@ test("P2 hybrid Most-relevant: searchHasMore false and searchNextCursor null (no test("P2 hybrid + search_sort=recent: uses lexical hit to detect stream door, then lexical (not queryRecords)", async () => { const summary = makeSummary({ connection_id: "gmail-1", connector_id: "gmail", streams: ["messages"] }); - const hit = makeHit({ connector_id: "gmail", stream: "messages", connection_id: "gmail-1" }); + const hit = makeHit({ connection_id: "gmail-1", connector_id: "gmail", stream: "messages" }); let lexicalCallCount = 0; let lexicalStreamsArg: string[] | undefined; @@ -398,9 +398,11 @@ test("P2 hybrid + search_sort=recent: uses lexical hit to detect stream door, th // detect the stream door, then uses lexical again (with stream scope) for the // actual display results (F2 fix: was queryRecords without query). const ds = makeDataSource({ - listConnectorSummaries: () => Promise.resolve(summaryListResponse([summary])), - listConnectorManifests: () => Promise.resolve([makeManifest("gmail", ["messages"])]), isHybridRetrievalAdvertised: () => Promise.resolve(true), + listConnectorManifests: () => Promise.resolve([makeManifest("gmail", ["messages"])]), + listConnectorSummaries: () => Promise.resolve(summaryListResponse([summary])), + // queryRecords must NOT be called (F2 fix: it returned ALL records ignoring query) + queryRecords: () => Promise.reject(new Error("queryRecords must not be called in Most-recent mode (F2 fix)")), // Hybrid must NOT be called when search_sort=recent searchRecordsHybrid: () => Promise.reject(new Error("hybrid must not be called in Most-recent mode")), searchRecordsLexical: (_q, opts) => { @@ -410,8 +412,6 @@ test("P2 hybrid + search_sort=recent: uses lexical hit to detect stream door, th } return Promise.resolve(makeLexicalPage([hit], { has_more: false })); }, - // queryRecords must NOT be called (F2 fix: it returned ALL records ignoring query) - queryRecords: () => Promise.reject(new Error("queryRecords must not be called in Most-recent mode (F2 fix)")), }); const result = await assembleExplorerData({ q: "invoice", search_sort: "recent" }, ds, "https://rs.test"); @@ -440,29 +440,29 @@ test("P2 multi-stream Most-recent: returns matching records via lexical with wir const summaryA = makeSummary({ connection_id: "ynab-1", connector_id: "ynab", streams: ["transactions"] }); const summaryB = makeSummary({ connection_id: "gmail-1", connector_id: "gmail", streams: ["messages"] }); const hitA = makeHit({ - connector_id: "ynab", - stream: "transactions", connection_id: "ynab-1", + connector_id: "ynab", record_key: "ynab-rec", + stream: "transactions", }); const hitB = makeHit({ - connector_id: "gmail", - stream: "messages", connection_id: "gmail-1", + connector_id: "gmail", record_key: "gmail-rec", + stream: "messages", }); const ds = makeDataSource({ - listConnectorSummaries: () => Promise.resolve(summaryListResponse([summaryA, summaryB])), + isHybridRetrievalAdvertised: () => Promise.resolve(false), listConnectorManifests: () => Promise.resolve([makeManifest("ynab", ["transactions"]), makeManifest("gmail", ["messages"])]), - isHybridRetrievalAdvertised: () => Promise.resolve(false), + listConnectorSummaries: () => Promise.resolve(summaryListResponse([summaryA, summaryB])), + // queryRecords must NOT be called (multi-stream has no single-stream path) + queryRecords: () => Promise.reject(new Error("queryRecords must not be called for multi-stream Most-recent")), // Both the stream-door probe and the display fetch are lexical. // The display fetch carries has_more=true to verify the cursor is wired. searchRecordsLexical: () => Promise.resolve(makeLexicalPage([hitA, hitB], { has_more: true, next_cursor: "lex-cursor-42" })), - // queryRecords must NOT be called (multi-stream has no single-stream path) - queryRecords: () => Promise.reject(new Error("queryRecords must not be called for multi-stream Most-recent")), }); const result = await assembleExplorerData({ q: "payment", search_sort: "recent" }, ds, "https://rs.test"); @@ -493,16 +493,16 @@ test("P2 stream door: populated when all hits share one connector+stream", async const summary = makeSummary({ connection_id: "ynab-1", connector_id: "ynab", - streams: ["transactions"], display_name: "My YNAB", + streams: ["transactions"], }); - const hit1 = makeHit({ connector_id: "ynab", stream: "transactions", record_key: "r1", connection_id: "ynab-1" }); - const hit2 = makeHit({ connector_id: "ynab", stream: "transactions", record_key: "r2", connection_id: "ynab-1" }); + const hit1 = makeHit({ connection_id: "ynab-1", connector_id: "ynab", record_key: "r1", stream: "transactions" }); + const hit2 = makeHit({ connection_id: "ynab-1", connector_id: "ynab", record_key: "r2", stream: "transactions" }); const ds = makeDataSource({ - listConnectorSummaries: () => Promise.resolve(summaryListResponse([summary])), - listConnectorManifests: () => Promise.resolve([makeManifest("ynab", ["transactions"])]), isHybridRetrievalAdvertised: () => Promise.resolve(false), + listConnectorManifests: () => Promise.resolve([makeManifest("ynab", ["transactions"])]), + listConnectorSummaries: () => Promise.resolve(summaryListResponse([summary])), searchRecordsLexical: () => Promise.resolve(makeLexicalPage([hit1, hit2], { has_more: false })), }); @@ -521,14 +521,14 @@ test("P2 stream door: populated when all hits share one connector+stream", async test("P2 stream door: null when hits span multiple connectors", async () => { const summaryA = makeSummary({ connection_id: "ynab-1", connector_id: "ynab", streams: ["transactions"] }); const summaryB = makeSummary({ connection_id: "gmail-1", connector_id: "gmail", streams: ["messages"] }); - const hitA = makeHit({ connector_id: "ynab", stream: "transactions", connection_id: "ynab-1" }); - const hitB = makeHit({ connector_id: "gmail", stream: "messages", connection_id: "gmail-1" }); + const hitA = makeHit({ connection_id: "ynab-1", connector_id: "ynab", stream: "transactions" }); + const hitB = makeHit({ connection_id: "gmail-1", connector_id: "gmail", stream: "messages" }); const ds = makeDataSource({ - listConnectorSummaries: () => Promise.resolve(summaryListResponse([summaryA, summaryB])), + isHybridRetrievalAdvertised: () => Promise.resolve(false), listConnectorManifests: () => Promise.resolve([makeManifest("ynab", ["transactions"]), makeManifest("gmail", ["messages"])]), - isHybridRetrievalAdvertised: () => Promise.resolve(false), + listConnectorSummaries: () => Promise.resolve(summaryListResponse([summaryA, summaryB])), searchRecordsLexical: () => Promise.resolve(makeLexicalPage([hitA, hitB], { has_more: false })), }); @@ -573,9 +573,9 @@ test("P2 searchSort defaults to 'relevance' on non-search (empty-query) feeds", const summary = makeSummary({ connection_id: "ynab-1", connector_id: "ynab", streams: ["transactions"] }); const ds = makeDataSource({ - listConnectorSummaries: () => Promise.resolve(summaryListResponse([summary])), - listConnectorManifests: () => Promise.resolve([makeManifest("ynab", ["transactions"])]), isHybridRetrievalAdvertised: () => Promise.resolve(false), + listConnectorManifests: () => Promise.resolve([makeManifest("ynab", ["transactions"])]), + listConnectorSummaries: () => Promise.resolve(summaryListResponse([summary])), queryRecords: () => Promise.resolve(makeRecordsPage(["rec-1"], { has_more: false })), }); diff --git a/apps/console/src/app/(console)/explore/explore-peek-relationships.test.ts b/apps/console/src/app/(console)/explore/explore-peek-relationships.test.ts index 1bb1cec69..0f11be033 100644 --- a/apps/console/src/app/(console)/explore/explore-peek-relationships.test.ts +++ b/apps/console/src/app/(console)/explore/explore-peek-relationships.test.ts @@ -15,8 +15,6 @@ function stubDataSource(over: { }): DashboardDataSource { const summaries = over.summaries ?? [{ connection_id: "conn_bank_chk", connector_id: "chase" }]; return { - listConnectorSummaries: () => Promise.resolve({ object: "list", data: summaries, has_more: false } as never), - listConnectorManifests: () => Promise.resolve(over.manifests ?? []), getStreamMetadata: (_connectorId: string, stream: string) => { const meta = over.streamMetadata?.[stream]; if (!meta) { @@ -24,6 +22,8 @@ function stubDataSource(over: { } return Promise.resolve(meta); }, + listConnectorManifests: () => Promise.resolve(over.manifests ?? []), + listConnectorSummaries: () => Promise.resolve({ data: summaries, has_more: false, object: "list" } as never), } as unknown as DashboardDataSource; } @@ -35,7 +35,7 @@ test("buildPeekRelationships resolves a child → parent back-link from a child- streams: [ { name: "transactions", - relationships: [{ name: "account", stream: "accounts", foreign_key: "account_id", cardinality: "has_one" }], + relationships: [{ cardinality: "has_one", foreign_key: "account_id", name: "account", stream: "accounts" }], }, { name: "accounts" }, ], @@ -43,11 +43,11 @@ test("buildPeekRelationships resolves a child → parent back-link from a child- ]; const rels = await buildPeekRelationships( { - connectorId: "chase", connectionId: "conn_bank_chk", - stream: "transactions", + connectorId: "chase", + data: { account_id: "acct_checking_4417", amount: -640 }, recordId: "rec_tx_41203", - data: { amount: -640, account_id: "acct_checking_4417" }, + stream: "transactions", }, stubDataSource({ manifests }) ); @@ -65,25 +65,25 @@ test("buildPeekRelationships resolves a parent → child has_many link from expa // Inspecting a PARENT (accounts) record whose metadata declares has_many → transactions. const streamMetadata: Record = { accounts: { - name: "accounts", expand_capabilities: [ { + cardinality: "has_many", + child_parent_key_field: "account_id", name: "transactions", target_stream: "transactions", - child_parent_key_field: "account_id", - cardinality: "has_many", usable: true, }, ], + name: "accounts", }, }; const rels = await buildPeekRelationships( { - connectorId: "chase", connectionId: "conn_bank_chk", - stream: "accounts", - recordId: "acct_checking_4417", + connectorId: "chase", data: { name: "checking ····4417" }, + recordId: "acct_checking_4417", + stream: "accounts", }, stubDataSource({ streamMetadata }) ); @@ -99,14 +99,14 @@ test("buildPeekRelationships resolves a parent → child has_many link from expa test("buildPeekRelationships returns empty (never throws) when no declared edge exists", async () => { const rels = await buildPeekRelationships( { - connectorId: "gmail", connectionId: "conn_mail_work", - stream: "messages", + connectorId: "gmail", + data: { from: "dana@studio.example", subject: "hi" }, recordId: "rec_msg_8841", - data: { subject: "hi", from: "dana@studio.example" }, + stream: "messages", }, stubDataSource({ summaries: [{ connection_id: "conn_mail_work", connector_id: "gmail" }] }) ); - assert.deepEqual(rels, { relatedLinks: [], reverseChildListLinks: [], parentBackLinks: [] }); + assert.deepEqual(rels, { parentBackLinks: [], relatedLinks: [], reverseChildListLinks: [] }); assert.equal(hasPeekRelationships(rels), false); }); diff --git a/apps/console/src/app/(console)/explore/explore-peek-relationships.ts b/apps/console/src/app/(console)/explore/explore-peek-relationships.ts index 05ca2d45e..702c17977 100644 --- a/apps/console/src/app/(console)/explore/explore-peek-relationships.ts +++ b/apps/console/src/app/(console)/explore/explore-peek-relationships.ts @@ -88,7 +88,7 @@ export async function buildPeekRelationships( input: PeekRelationshipInput, dataSource: DashboardDataSource ): Promise { - const empty: PeekRelationships = { relatedLinks: [], reverseChildListLinks: [], parentBackLinks: [] }; + const empty: PeekRelationships = { parentBackLinks: [], relatedLinks: [], reverseChildListLinks: [] }; const { connectionId, connectorId, stream, recordId, data } = input; // Resolve the connection's instance id so metadata reads scope to the right @@ -121,8 +121,8 @@ export async function buildPeekRelationships( .getStreamMetadata(connectorId, parentStream, { connectorInstanceId }) .catch(() => null); return { - parentStream, expandCapabilities: Array.isArray(parentMeta?.expand_capabilities) ? parentMeta.expand_capabilities : [], + parentStream, }; }) ); @@ -142,7 +142,7 @@ export async function buildPeekRelationships( ); const reverseChildListLinks = reverseChildListLinksFromManifest( connectorStreams, - { connectionId, parentStream: stream, parentRecordKey: recordId }, + { connectionId, parentRecordKey: recordId, parentStream: stream }, forwardChildListKeys ); // Child → parent back-links from parent metadata + child's own declared has_one. @@ -150,5 +150,5 @@ export async function buildPeekRelationships( const childHasOneLinks = childHasOneBackLinksFromManifest(childManifestStream, data, { connectionId }); const parentBackLinks = mergeParentBackLinks(parentBackLinkFromMeta, childHasOneLinks); - return { relatedLinks, reverseChildListLinks, parentBackLinks }; + return { parentBackLinks, relatedLinks, reverseChildListLinks }; } diff --git a/apps/console/src/app/(console)/explore/explore-query-input.test.ts b/apps/console/src/app/(console)/explore/explore-query-input.test.ts index 75c01f054..d40d035b7 100644 --- a/apps/console/src/app/(console)/explore/explore-query-input.test.ts +++ b/apps/console/src/app/(console)/explore/explore-query-input.test.ts @@ -51,13 +51,13 @@ const SEARCH_FALLBACK_LABEL_RE = /Search: yn/; test("typeahead surfaces source/stream/has-image/has-link/date chips, each carrying its operator", () => { const suggestions = buildTypeaheadSuggestions({ - fragment: "", connections: CONNECTIONS, - streams: ["transactions"], - selectedConnectionIds: new Set(), - selectedStreams: new Set(), + fragment: "", hasImageActive: false, hasLinkActive: false, + selectedConnectionIds: new Set(), + selectedStreams: new Set(), + streams: ["transactions"], }); // A source chip is EQUIVALENT to the con: operator (recognition over recall). const ynab = suggestions.find((s) => s.value === "cin_ynab"); @@ -73,13 +73,13 @@ test("typeahead surfaces source/stream/has-image/has-link/date chips, each carry test("typeahead narrows by the trailing fragment (recognition, fuzzy)", () => { const suggestions = buildTypeaheadSuggestions({ - fragment: "yn", connections: CONNECTIONS, - streams: ["transactions"], - selectedConnectionIds: new Set(), - selectedStreams: new Set(), + fragment: "yn", hasImageActive: false, hasLinkActive: false, + selectedConnectionIds: new Set(), + selectedStreams: new Set(), + streams: ["transactions"], }); const sources = suggestions.filter((s) => s.kind === "source"); assert.equal(sources.length, 1, "only YNAB matches 'yn'"); @@ -88,13 +88,13 @@ test("typeahead narrows by the trailing fragment (recognition, fuzzy)", () => { test("typeahead excludes already-selected facets and already-active has:image (no noise)", () => { const suggestions = buildTypeaheadSuggestions({ - fragment: "", connections: CONNECTIONS, - streams: ["transactions"], - selectedConnectionIds: new Set(["cin_ynab"]), - selectedStreams: new Set(["transactions"]), + fragment: "", hasImageActive: true, hasLinkActive: false, + selectedConnectionIds: new Set(["cin_ynab"]), + selectedStreams: new Set(["transactions"]), + streams: ["transactions"], }); assert.ok(!suggestions.some((s) => s.value === "cin_ynab"), "selected source is not re-suggested"); assert.ok(!suggestions.some((s) => s.value === "transactions"), "selected stream is not re-suggested"); @@ -104,14 +104,14 @@ test("typeahead excludes already-selected facets and already-active has:image (n test("typeahead is bounded by `limit` (popover never unbounded)", () => { const many = Array.from({ length: 50 }, (_, i) => ({ connectionId: `c${i}`, displayName: `Conn ${i}` })); const suggestions = buildTypeaheadSuggestions({ - fragment: "", connections: many, - streams: [], - selectedConnectionIds: new Set(), - selectedStreams: new Set(), + fragment: "", hasImageActive: false, hasLinkActive: false, limit: 5, + selectedConnectionIds: new Set(), + selectedStreams: new Set(), + streams: [], }); assert.ok(suggestions.length <= 5, "the suggestion menu is capped"); }); @@ -129,13 +129,13 @@ test("appendOperatorToken replaces the trailing fragment with the operator token test("typeahead emits SOURCES section label on first source suggestion only", () => { const suggestions = buildTypeaheadSuggestions({ - fragment: "", connections: CONNECTIONS, - streams: ["transactions"], - selectedConnectionIds: new Set(), - selectedStreams: new Set(), + fragment: "", hasImageActive: false, hasLinkActive: false, + selectedConnectionIds: new Set(), + selectedStreams: new Set(), + streams: ["transactions"], }); const sources = suggestions.filter((s) => s.kind === "source"); assert.ok(sources.length >= 2, "both connections should appear"); @@ -145,13 +145,13 @@ test("typeahead emits SOURCES section label on first source suggestion only", () test("typeahead emits STREAMS section label on first stream suggestion", () => { const suggestions = buildTypeaheadSuggestions({ - fragment: "", connections: [], - streams: ["transactions", "budgets"], - selectedConnectionIds: new Set(), - selectedStreams: new Set(), + fragment: "", hasImageActive: false, hasLinkActive: false, + selectedConnectionIds: new Set(), + selectedStreams: new Set(), + streams: ["transactions", "budgets"], }); const streams = suggestions.filter((s) => s.kind === "stream"); assert.equal(streams[0]?.sectionLabel, "STREAMS"); @@ -160,18 +160,18 @@ test("typeahead emits STREAMS section label on first stream suggestion", () => { test("typeahead attaches honest record counts when connectionCounts / streamCounts are provided", () => { const suggestions = buildTypeaheadSuggestions({ - fragment: "", - connections: CONNECTIONS, connectionCounts: new Map([ ["cin_ynab", 42], ["cin_chase", 7], ]), - streams: ["transactions"], - streamCounts: new Map([["transactions", 100]]), - selectedConnectionIds: new Set(), - selectedStreams: new Set(), + connections: CONNECTIONS, + fragment: "", hasImageActive: false, hasLinkActive: false, + selectedConnectionIds: new Set(), + selectedStreams: new Set(), + streamCounts: new Map([["transactions", 100]]), + streams: ["transactions"], }); const ynab = suggestions.find((s) => s.value === "cin_ynab"); assert.equal(ynab?.count, 42, "source carries its loaded count"); @@ -181,13 +181,13 @@ test("typeahead attaches honest record counts when connectionCounts / streamCoun test("typeahead omits counts when not provided (undefined, not 0)", () => { const suggestions = buildTypeaheadSuggestions({ - fragment: "", connections: CONNECTIONS, - streams: ["transactions"], - selectedConnectionIds: new Set(), - selectedStreams: new Set(), + fragment: "", hasImageActive: false, hasLinkActive: false, + selectedConnectionIds: new Set(), + selectedStreams: new Set(), + streams: ["transactions"], }); const ynab = suggestions.find((s) => s.value === "cin_ynab"); assert.equal(ynab?.count, undefined, "no count when connectionCounts not provided"); @@ -195,13 +195,13 @@ test("typeahead omits counts when not provided (undefined, not 0)", () => { test("typeahead always appends SEARCH-fallback last when fragment is non-empty", () => { const suggestions = buildTypeaheadSuggestions({ - fragment: "yn", connections: CONNECTIONS, - streams: ["transactions"], - selectedConnectionIds: new Set(), - selectedStreams: new Set(), + fragment: "yn", hasImageActive: false, hasLinkActive: false, + selectedConnectionIds: new Set(), + selectedStreams: new Set(), + streams: ["transactions"], }); const last = suggestions.at(-1); assert.equal(last?.kind, "search", "last item is always SEARCH fallback"); @@ -211,13 +211,13 @@ test("typeahead always appends SEARCH-fallback last when fragment is non-empty", test("typeahead SEARCH-fallback is absent when fragment is empty", () => { const suggestions = buildTypeaheadSuggestions({ - fragment: "", connections: CONNECTIONS, - streams: ["transactions"], - selectedConnectionIds: new Set(), - selectedStreams: new Set(), + fragment: "", hasImageActive: false, hasLinkActive: false, + selectedConnectionIds: new Set(), + selectedStreams: new Set(), + streams: ["transactions"], }); assert.ok(!suggestions.some((s) => s.kind === "search"), "no SEARCH fallback when fragment is empty"); }); @@ -225,13 +225,13 @@ test("typeahead SEARCH-fallback is absent when fragment is empty", () => { test("typeahead SEARCH-fallback carries SEARCH section label when it is the only suggestion", () => { // Fragment that matches nothing — only the SEARCH-fallback survives. const suggestions = buildTypeaheadSuggestions({ - fragment: "xyzzy_no_match_ever", connections: [], - streams: [], - selectedConnectionIds: new Set(), - selectedStreams: new Set(), + fragment: "xyzzy_no_match_ever", hasImageActive: true, hasLinkActive: true, + selectedConnectionIds: new Set(), + selectedStreams: new Set(), + streams: [], }); assert.equal(suggestions.length, 1); assert.equal(suggestions[0]?.kind, "search"); diff --git a/apps/console/src/app/(console)/explore/explore-saved-views.test.ts b/apps/console/src/app/(console)/explore/explore-saved-views.test.ts index 24543f18a..37c267cdf 100644 --- a/apps/console/src/app/(console)/explore/explore-saved-views.test.ts +++ b/apps/console/src/app/(console)/explore/explore-saved-views.test.ts @@ -20,7 +20,7 @@ import { sameView, } from "./explore-saved-views.ts"; -const view = (id: string, name: string, href: string): SavedView => ({ id, name, href }); +const view = (id: string, name: string, href: string): SavedView => ({ href, id, name }); test("canonicalViewIdentity ignores volatile params (peek/cursor/anchor) and param order", () => { const a = "/explore?q=deploy&connection=con_1&peek=p1&cursor=c1&anchor=a1"; @@ -60,13 +60,13 @@ test("parseSavedViews drops malformed entries, never throws", () => { assert.deepEqual(parseSavedViews("not json"), []); assert.deepEqual(parseSavedViews(JSON.stringify({ not: "an array" })), []); const raw = JSON.stringify([ - { id: "1", name: "Finance", href: "/explore?q=x" }, - { id: "2", name: "", href: "/explore?q=y" }, // empty name → dropped - { id: "3", href: "/explore?q=z" }, // missing name → dropped - { name: "no id", href: "/x" }, // missing id → dropped + { href: "/explore?q=x", id: "1", name: "Finance" }, + { href: "/explore?q=y", id: "2", name: "" }, // empty name → dropped + { href: "/explore?q=z", id: "3" }, // missing name → dropped + { href: "/x", name: "no id" }, // missing id → dropped "garbage", ]); - assert.deepEqual(parseSavedViews(raw), [{ id: "1", name: "Finance", href: "/explore?q=x" }]); + assert.deepEqual(parseSavedViews(raw), [{ href: "/explore?q=x", id: "1", name: "Finance" }]); }); test("addSavedView never saves the All (no-filter) view as a tab", () => { diff --git a/apps/console/src/app/(console)/explore/explore-saved-views.ts b/apps/console/src/app/(console)/explore/explore-saved-views.ts index 369fecc51..6f5a51bb2 100644 --- a/apps/console/src/app/(console)/explore/explore-saved-views.ts +++ b/apps/console/src/app/(console)/explore/explore-saved-views.ts @@ -108,7 +108,7 @@ export function parseSavedViews(raw: string | null): SavedView[] { typeof (item as SavedView).href === "string" && (item as SavedView).name.trim().length > 0 ) { - out.push({ id: (item as SavedView).id, name: (item as SavedView).name, href: (item as SavedView).href }); + out.push({ href: (item as SavedView).href, id: (item as SavedView).id, name: (item as SavedView).name }); } } return out; diff --git a/apps/console/src/app/(console)/explore/explorer-url.test.ts b/apps/console/src/app/(console)/explore/explorer-url.test.ts index e331fa491..d4f071daf 100644 --- a/apps/console/src/app/(console)/explore/explorer-url.test.ts +++ b/apps/console/src/app/(console)/explore/explorer-url.test.ts @@ -22,8 +22,8 @@ const NO_CONNECTION_TOKEN = "~"; test("buildExplorerHref preserves repeated connection params (no collapse)", () => { const href = buildExplorerHref(dashboardRoutes, { - query: "payroll", connectionIds: ["gmail-personal", "gmail-work"], + query: "payroll", streams: ["messages"], }); const url = new URL(href, "https://example.test"); @@ -61,10 +61,10 @@ test("buildExplorerHref carries the date window when set", () => { test("buildExplorerHref preserves date window alongside chip + query state", () => { const href = buildExplorerHref(dashboardRoutes, { - query: "invoice", connectionIds: ["gmail-personal"], - streams: ["messages"], + query: "invoice", since: "2026-05-21", + streams: ["messages"], until: "2026-05-28", }); const url = new URL(href, "https://example.test"); @@ -82,10 +82,10 @@ test("buildExplorerHref omits empty date params", () => { test("explorerPeekParam round-trips a concrete connection_id when known", () => { const entry = { - connectorId: "gmail", connectionId: "conn-personal", - stream: "messages", + connectorId: "gmail", recordId: "ABC123", + stream: "messages", }; const raw = explorerPeekParam(entry); assert.equal(raw, "gmail::conn-personal::messages::ABC123"); @@ -94,7 +94,7 @@ test("explorerPeekParam round-trips a concrete connection_id when known", () => }); test("explorerPeekParam encodes the no-connection sentinel when unknown", () => { - const entry = { connectorId: "gmail", connectionId: null, stream: "messages", recordId: "ABC123" }; + const entry = { connectionId: null, connectorId: "gmail", recordId: "ABC123", stream: "messages" }; const raw = explorerPeekParam(entry); assert.equal(raw, `gmail::${NO_CONNECTION_TOKEN}::messages::ABC123`); const parsed = parseExplorerPeekParam(raw); @@ -105,16 +105,16 @@ test("explorerPeekParam keeps two same-connector connections distinct in the URL // Regression: previously the peek param was `connectorId::stream::recordId`, // so two Gmail connections viewing the same logical record id collided. const a = explorerPeekParam({ - connectorId: "gmail", connectionId: "conn-personal", - stream: "messages", + connectorId: "gmail", recordId: "ABC123", + stream: "messages", }); const b = explorerPeekParam({ - connectorId: "gmail", connectionId: "conn-work", - stream: "messages", + connectorId: "gmail", recordId: "ABC123", + stream: "messages", }); assert.notEqual(a, b); }); @@ -133,10 +133,10 @@ test("explorerPeekParam round-trips record ids containing the ':: ' separator", // contains `::`, so a record id like `thread::42` would parse as five // parts and be rejected, or worse, silently split mid-id. const entry = { - connectorId: "imap", connectionId: "conn-personal", - stream: "threads", + connectorId: "imap", recordId: "thread::42", + stream: "threads", }; const raw = explorerPeekParam(entry); assert.deepEqual(parseExplorerPeekParam(raw), entry); @@ -144,10 +144,10 @@ test("explorerPeekParam round-trips record ids containing the ':: ' separator", test("explorerPeekParam round-trips ids containing /, #, and spaces", () => { const entry = { - connectorId: "github", connectionId: "owner/repo#42", - stream: "issues/comments", + connectorId: "github", recordId: "comment id with spaces", + stream: "issues/comments", }; const raw = explorerPeekParam(entry); assert.deepEqual(parseExplorerPeekParam(raw), entry); @@ -155,10 +155,10 @@ test("explorerPeekParam round-trips ids containing /, #, and spaces", () => { test("explorerPeekParam round-trips a stream containing the separator", () => { const entry = { - connectorId: "custom", connectionId: "conn-a", - stream: "ns::events", + connectorId: "custom", recordId: "rec-1", + stream: "ns::events", }; const raw = explorerPeekParam(entry); assert.deepEqual(parseExplorerPeekParam(raw), entry); @@ -166,10 +166,10 @@ test("explorerPeekParam round-trips a stream containing the separator", () => { test("explorerPeekParam round-trips a connection id containing the separator", () => { const entry = { - connectorId: "gmail", connectionId: "tenant::user", - stream: "messages", + connectorId: "gmail", recordId: "ABC123", + stream: "messages", }; const raw = explorerPeekParam(entry); assert.deepEqual(parseExplorerPeekParam(raw), entry); @@ -177,14 +177,14 @@ test("explorerPeekParam round-trips a connection id containing the separator", ( function fakeEntry(displayAt: string, recordId: string): ExplorerFeedEntry { return { - connectorId: "gmail", - connectionId: "conn-personal", connectionDisplayName: "Personal Gmail", - stream: "messages", - recordId, - emittedAt: displayAt, + connectionId: "conn-personal", + connectorId: "gmail", displayAt, displayIsSemantic: false, + emittedAt: displayAt, + recordId, + stream: "messages", }; } diff --git a/apps/console/src/app/(console)/explore/over-time-chart.tsx b/apps/console/src/app/(console)/explore/over-time-chart.tsx index 988c7f671..8ae1242d8 100644 --- a/apps/console/src/app/(console)/explore/over-time-chart.tsx +++ b/apps/console/src/app/(console)/explore/over-time-chart.tsx @@ -133,7 +133,7 @@ export function OverTimeChart({ series, since, until, descriptorKind, onSelectRa return; } draggingRef.current = true; - setDrag({ start: i, end: i }); + setDrag({ end: i, start: i }); }, [brushable] ); @@ -141,7 +141,7 @@ export function OverTimeChart({ series, since, until, descriptorKind, onSelectRa const onPointerEnterBar = useCallback((i: number) => { setHovered(i); if (draggingRef.current) { - setDrag((d) => (d ? { ...d, end: i } : { start: i, end: i })); + setDrag((d) => (d ? { ...d, end: i } : { end: i, start: i })); } }, []); diff --git a/apps/console/src/app/(console)/explore/page.tsx b/apps/console/src/app/(console)/explore/page.tsx index 08099ef84..055085892 100644 --- a/apps/console/src/app/(console)/explore/page.tsx +++ b/apps/console/src/app/(console)/explore/page.tsx @@ -131,11 +131,11 @@ export default async function RecordsExplorerPage({ if (data.peek && !data.peek.error && data.peek.connectionId) { peekRelationships = await buildPeekRelationships( { - connectorId: data.peek.connectorId, connectionId: data.peek.connectionId, - stream: data.peek.stream, - recordId: data.peek.recordId, + connectorId: data.peek.connectorId, data: parsePeekData(data.peek.bodyJson), + recordId: data.peek.recordId, + stream: data.peek.stream, }, liveDashboardDataSource ); diff --git a/apps/console/src/app/(console)/explore/peek-url.test.ts b/apps/console/src/app/(console)/explore/peek-url.test.ts index 314a861c1..a8e8a1e87 100644 --- a/apps/console/src/app/(console)/explore/peek-url.test.ts +++ b/apps/console/src/app/(console)/explore/peek-url.test.ts @@ -7,11 +7,11 @@ import { buildPeekReadUrl } from "@pdpp/operator-ui/explore/peek-read-url"; test("buildPeekReadUrl matches the canonical record-read URL shape", () => { const url = buildPeekReadUrl({ - rsBaseUrl: "http://rs.test", connectorId: "gmail", - stream: "messages", - recordId: "rec-1", connectorInstanceId: "conn-abc", + recordId: "rec-1", + rsBaseUrl: "http://rs.test", + stream: "messages", }); const parsed = new URL(url); assert.equal(parsed.pathname, "/v1/streams/messages/records/rec-1"); @@ -21,11 +21,11 @@ test("buildPeekReadUrl matches the canonical record-read URL shape", () => { test("buildPeekReadUrl encodes path-unsafe stream and record ids", () => { const url = buildPeekReadUrl({ - rsBaseUrl: "http://rs.test", connectorId: "github", - stream: "issues/comments", - recordId: "owner/repo#42", connectorInstanceId: null, + recordId: "owner/repo#42", + rsBaseUrl: "http://rs.test", + stream: "issues/comments", }); const parsed = new URL(url); assert.equal(parsed.pathname, "/v1/streams/issues%2Fcomments/records/owner%2Frepo%2342"); diff --git a/apps/console/src/app/(console)/explore/search-hit-attribution.test.ts b/apps/console/src/app/(console)/explore/search-hit-attribution.test.ts index e4c621cf7..64d9bca22 100644 --- a/apps/console/src/app/(console)/explore/search-hit-attribution.test.ts +++ b/apps/console/src/app/(console)/explore/search-hit-attribution.test.ts @@ -34,8 +34,8 @@ test("attributeSearchHit honors hit.connection_id when present", () => { ]; const result = attributeSearchHit( { - connector_id: "gmail", connection_id: "conn-work", + connector_id: "gmail", }, visible ); @@ -94,13 +94,13 @@ test("shouldIncludeSearchHit drops a hit whose concrete connection identity is n // is filtered out at the summaries layer and so is not in this set. const allowedConnectionIds = new Set(["conn-personal"]); const hit = { - connector_id: "gmail", connection_id: "conn-work", + connector_id: "gmail", }; assert.equal( shouldIncludeSearchHit(hit, { - allowedConnectors, allowedConnectionIds, + allowedConnectors, enforceConnectionFilter: true, }), false @@ -111,13 +111,13 @@ test("shouldIncludeSearchHit keeps a hit whose concrete connection identity matc const allowedConnectors = new Set(["gmail"]); const allowedConnectionIds = new Set(["conn-personal"]); const hit = { - connector_id: "gmail", connection_id: "conn-personal", + connector_id: "gmail", }; assert.equal( shouldIncludeSearchHit(hit, { - allowedConnectors, allowedConnectionIds, + allowedConnectors, enforceConnectionFilter: true, }), true @@ -133,8 +133,8 @@ test("shouldIncludeSearchHit honors the deprecated connector_instance_id alias w }; assert.equal( shouldIncludeSearchHit(hit, { - allowedConnectors, allowedConnectionIds, + allowedConnectors, enforceConnectionFilter: true, }), false @@ -150,8 +150,8 @@ test("shouldIncludeSearchHit falls through to connector-scope when the hit carri const hit = { connector_id: "gmail" }; assert.equal( shouldIncludeSearchHit(hit, { - allowedConnectors, allowedConnectionIds, + allowedConnectors, enforceConnectionFilter: true, }), true @@ -161,11 +161,11 @@ test("shouldIncludeSearchHit falls through to connector-scope when the hit carri test("shouldIncludeSearchHit drops hits whose connector type is not in the visible set", () => { const allowedConnectors = new Set(["gmail"]); const allowedConnectionIds = new Set(["conn-personal"]); - const hit = { connector_id: "github", connection_id: "conn-personal" }; + const hit = { connection_id: "conn-personal", connector_id: "github" }; assert.equal( shouldIncludeSearchHit(hit, { - allowedConnectors, allowedConnectionIds, + allowedConnectors, enforceConnectionFilter: true, }), false @@ -178,11 +178,11 @@ test("shouldIncludeSearchHit does not enforce connection-scope when no chips are // carry an unrecognized connection_id. const allowedConnectors = new Set(["gmail"]); const allowedConnectionIds = new Set(); - const hit = { connector_id: "gmail", connection_id: "conn-unknown" }; + const hit = { connection_id: "conn-unknown", connector_id: "gmail" }; assert.equal( shouldIncludeSearchHit(hit, { - allowedConnectors, allowedConnectionIds, + allowedConnectors, enforceConnectionFilter: false, }), true @@ -195,8 +195,8 @@ test("attributeSearchHit prefers server-provided display_name over summary fallb ]; const result = attributeSearchHit( { - connector_id: "gmail", connection_id: "conn-personal", + connector_id: "gmail", display_name: "Server Label", }, visible diff --git a/apps/console/src/app/(console)/grants/[grantId]/page.tsx b/apps/console/src/app/(console)/grants/[grantId]/page.tsx index c2b324c83..fc79fb873 100644 --- a/apps/console/src/app/(console)/grants/[grantId]/page.tsx +++ b/apps/console/src/app/(console)/grants/[grantId]/page.tsx @@ -77,7 +77,7 @@ export default async function GrantDetailPage({
} - breadcrumbs={[{ label: "Grants", href: "/grants" }, { label: "Grant" }]} + breadcrumbs={[{ href: "/grants", label: "Grants" }, { label: "Grant" }]} cliCommand={`pdpp ref grant timeline ${grantId}`} count={`${envelope.events.length} events${revoked ? " · revoked" : ""}`} envelope={envelope} diff --git a/apps/console/src/app/(console)/grants/page.tsx b/apps/console/src/app/(console)/grants/page.tsx index 386813d76..24f407296 100644 --- a/apps/console/src/app/(console)/grants/page.tsx +++ b/apps/console/src/app/(console)/grants/page.tsx @@ -80,13 +80,13 @@ function grantClientCaption(grant: GrantSummary): string | null { export default async function GrantsPage({ searchParams }: { searchParams: Promise }) { const params = await searchParams; const filters = { - cursor: params.cursor, - status: params.status, client_id: params.client_id, + cursor: params.cursor, + limit: 50, + q: params.q, source_id: params.source_id, source_kind: params.source_kind, - q: params.q, - limit: 50, + status: params.status, }; let result: ListResponse; @@ -166,21 +166,21 @@ export default async function GrantsPage({ searchParams }: { searchParams: Promi emptyHint: "Grant artifacts appear after client/provider-connect consent flows issue or reject grants.", emptyTitle: "No grants yet", filters: { - query: { name: "q", placeholder: "id contains…", defaultValue: params.q ?? "" }, + query: { defaultValue: params.q ?? "", name: "q", placeholder: "id contains…" }, status: { - name: "status", defaultValue: params.status ?? "", + name: "status", options: [ - { value: "issued", label: "issued" }, - { value: "revoked", label: "revoked" }, - { value: "denied", label: "denied" }, - { value: "failed", label: "failed" }, - { value: "pending", label: "pending" }, + { label: "issued", value: "issued" }, + { label: "revoked", value: "revoked" }, + { label: "denied", value: "denied" }, + { label: "failed", value: "failed" }, + { label: "pending", value: "pending" }, ], }, }, headerActions: ( - + Grant request workspace ), diff --git a/apps/console/src/app/(console)/grants/pending-actions.ts b/apps/console/src/app/(console)/grants/pending-actions.ts index c0c694b6c..94508ca2b 100644 --- a/apps/console/src/app/(console)/grants/pending-actions.ts +++ b/apps/console/src/app/(console)/grants/pending-actions.ts @@ -32,8 +32,8 @@ export async function approvePendingApprovalAction(formData: FormData) { try { await approvePendingApproval({ - kind, approvalId, + kind, subjectId, }); } catch (err) { @@ -52,8 +52,8 @@ export async function denyPendingApprovalAction(formData: FormData) { try { await denyPendingApproval({ - kind, approvalId, + kind, subjectId, }); } catch (err) { diff --git a/apps/console/src/app/(console)/grants/request/actions.ts b/apps/console/src/app/(console)/grants/request/actions.ts index d43b4b1dc..f96fb109a 100644 --- a/apps/console/src/app/(console)/grants/request/actions.ts +++ b/apps/console/src/app/(console)/grants/request/actions.ts @@ -24,22 +24,22 @@ function asSourceKind(value: FormDataEntryValue | null): "connector" | "provider function readDraft(formData: FormData) { return { - initialAccessToken: asString(formData.get("initial_access_token")), + accessMode: asString(formData.get("access_mode")), clientId: asString(formData.get("client_id")), clientName: asString(formData.get("client_name")), clientUri: asString(formData.get("client_uri")), - redirectUri: asString(formData.get("redirect_uri")), - sourceKind: asSourceKind(formData.get("source_kind")), - sourceId: asString(formData.get("source_id")), + connectionId: asString(formData.get("connection_id")), + fields: asString(formData.get("fields")), + initialAccessToken: asString(formData.get("initial_access_token")), purposeCode: asString(formData.get("purpose_code")), purposeDescription: asString(formData.get("purpose_description")), - accessMode: asString(formData.get("access_mode")), + redirectUri: asString(formData.get("redirect_uri")), retention: asString(formData.get("retention")), + sourceId: asString(formData.get("source_id")), + sourceKind: asSourceKind(formData.get("source_kind")), streamName: asString(formData.get("stream_name")), - connectionId: asString(formData.get("connection_id")), - fields: asString(formData.get("fields")), - view: asString(formData.get("view")), subjectId: asString(formData.get("subject_id")), + view: asString(formData.get("view")), }; } diff --git a/apps/console/src/app/(console)/grants/request/connection-pin.test.ts b/apps/console/src/app/(console)/grants/request/connection-pin.test.ts index 54ddb3cb7..69b4e813f 100644 --- a/apps/console/src/app/(console)/grants/request/connection-pin.test.ts +++ b/apps/console/src/app/(console)/grants/request/connection-pin.test.ts @@ -67,34 +67,34 @@ function draft(overrides = {}) { test("buildConnectionPinOptions shows owner-set names verbatim and excludes other connectors", () => { const options = buildConnectionPinOptions({ id: "gmail", kind: "connector" }, [ - { connector_id: "gmail", connection_id: "cin_a", display_name: "Work Gmail", streams: ["messages"] }, - { connector_id: "gmail", connection_id: "cin_b", display_name: "Personal Gmail", streams: ["messages"] }, - { connector_id: "slack", connection_id: "cin_s", display_name: "Team Slack", streams: ["messages"] }, + { connection_id: "cin_a", connector_id: "gmail", display_name: "Work Gmail", streams: ["messages"] }, + { connection_id: "cin_b", connector_id: "gmail", display_name: "Personal Gmail", streams: ["messages"] }, + { connection_id: "cin_s", connector_id: "slack", display_name: "Team Slack", streams: ["messages"] }, ]); assert.deepEqual(options, [ - { value: "cin_a", label: "Work Gmail" }, - { value: "cin_b", label: "Personal Gmail" }, + { label: "Work Gmail", value: "cin_a" }, + { label: "Personal Gmail", value: "cin_b" }, ]); }); test("buildConnectionPinOptions filters to connections that expose the addressed stream", () => { const options = buildConnectionPinOptions({ id: "gmail", kind: "connector", streamName: "messages" }, [ - { connector_id: "gmail", connection_id: "cin_messages", display_name: "Messages Gmail", streams: ["messages"] }, - { connector_id: "gmail", connection_id: "cin_contacts", display_name: "Contacts Gmail", streams: ["contacts"] }, + { connection_id: "cin_messages", connector_id: "gmail", display_name: "Messages Gmail", streams: ["messages"] }, + { connection_id: "cin_contacts", connector_id: "gmail", display_name: "Contacts Gmail", streams: ["contacts"] }, ]); - assert.deepEqual(options, [{ value: "cin_messages", label: "Messages Gmail" }]); + assert.deepEqual(options, [{ label: "Messages Gmail", value: "cin_messages" }]); }); test("buildConnectionPinOptions derives an owner-meaningful default for never-renamed connections", () => { // A blank label and a bare-connector-type label are both fallbacks; they get // a stable `· account N` disambiguator instead of a placeholder/URL/id. const options = buildConnectionPinOptions({ id: "gmail", kind: "connector" }, [ - { connector_id: "gmail", connection_id: "cin_a", display_name: null, streams: ["messages"] }, - { connector_id: "gmail", connection_id: "cin_b", display_name: "gmail", streams: ["messages"] }, + { connection_id: "cin_a", connector_id: "gmail", display_name: null, streams: ["messages"] }, + { connection_id: "cin_b", connector_id: "gmail", display_name: "gmail", streams: ["messages"] }, ]); assert.deepEqual(options, [ - { value: "cin_a", label: "gmail · account 1" }, - { value: "cin_b", label: "gmail · account 2" }, + { label: "gmail · account 1", value: "cin_a" }, + { label: "gmail · account 2", value: "cin_b" }, ]); // No rendered label is ever the raw connection_id or a placeholder string. for (const opt of options) { @@ -105,14 +105,14 @@ test("buildConnectionPinOptions derives an owner-meaningful default for never-re test("buildConnectionPinOptions keeps a lone connection's label undisambiguated", () => { const options = buildConnectionPinOptions({ id: "gmail", kind: "connector" }, [ - { connector_id: "gmail", connection_id: "cin_a", display_name: "Work Gmail", streams: ["messages"] }, + { connection_id: "cin_a", connector_id: "gmail", display_name: "Work Gmail", streams: ["messages"] }, ]); - assert.deepEqual(options, [{ value: "cin_a", label: "Work Gmail" }]); + assert.deepEqual(options, [{ label: "Work Gmail", value: "cin_a" }]); }); test("buildConnectionPinOptions returns [] for provider-native and empty sources (no connection dimension)", () => { const summaries = [ - { connector_id: "gmail", connection_id: "cin_a", display_name: "Work Gmail", streams: ["messages"] }, + { connection_id: "cin_a", connector_id: "gmail", display_name: "Work Gmail", streams: ["messages"] }, ]; assert.deepEqual(buildConnectionPinOptions({ id: "gmail", kind: "provider_native" }, summaries), []); assert.deepEqual(buildConnectionPinOptions({ id: "", kind: "connector" }, summaries), []); diff --git a/apps/console/src/app/(console)/grants/request/page.tsx b/apps/console/src/app/(console)/grants/request/page.tsx index 93d6951ba..9194ff653 100644 --- a/apps/console/src/app/(console)/grants/request/page.tsx +++ b/apps/console/src/app/(console)/grants/request/page.tsx @@ -35,10 +35,10 @@ type Examples = NonNullable function HeaderActions({ ownerLoginUrl }: { ownerLoginUrl: string }) { return ( <> - + Pending approvals - + Owner access @@ -72,8 +72,8 @@ function WorkspaceError({ message }: { message: string }) { } const ACCESS_MODE_OPTIONS = [ - { value: "single_use", label: "single_use" }, - { value: "ongoing", label: "ongoing" }, + { label: "single_use", value: "single_use" }, + { label: "ongoing", value: "ongoing" }, ]; function DraftFormFields({ connectionOptions, draft }: { connectionOptions: ConnectionPinOption[]; draft: Draft }) { @@ -341,7 +341,7 @@ export default async function GrantRequestPage({ searchParams }: { searchParams: } - breadcrumbs={[{ label: "Grants", href: "/grants" }, { label: "Grant request" }]} + breadcrumbs={[{ href: "/grants", label: "Grants" }, { label: "Grant request" }]} description="Register a public client, stage a real PAR request with PDPP authorization details, then drive the resulting consent through the public approval path." meta={} title="Grant request workspace" diff --git a/apps/console/src/app/(console)/layout.tsx b/apps/console/src/app/(console)/layout.tsx index c2393b29a..267b66c48 100644 --- a/apps/console/src/app/(console)/layout.tsx +++ b/apps/console/src/app/(console)/layout.tsx @@ -14,7 +14,7 @@ import { isDashboardEnabled } from "./lib/dashboard-flag.ts"; // metadata export is defense in depth for the case where the dashboard // layout does render to completion. export const metadata: Metadata = { - robots: { index: false, follow: false, nocache: true }, + robots: { follow: false, index: false, nocache: true }, }; // Auth gating intentionally lives in two layers, not here: diff --git a/apps/console/src/app/(console)/lib/collection-report.test.ts b/apps/console/src/app/(console)/lib/collection-report.test.ts index 7d2b795eb..160697689 100644 --- a/apps/console/src/app/(console)/lib/collection-report.test.ts +++ b/apps/console/src/app/(console)/lib/collection-report.test.ts @@ -52,15 +52,15 @@ const STRATEGY_NUMERATOR_COPY = /not the coverage numerator/i; function entry(overrides: Partial): RefCollectionReportEntry { return { - stream: "items", + checkpoint: "unknown", collected: 0, considered: "unknown", - covered: "unknown", - checkpoint: "unknown", coverage_condition: "unknown", + covered: "unknown", forward_disposition: "resumable", pending_detail_gaps: 0, skipped: null, + stream: "items", ...overrides, }; } @@ -71,8 +71,8 @@ test("indexCollectionReportByStream tolerates absence and indexes by stream name assert.equal(indexCollectionReportByStream([]).size, 0); const byStream = indexCollectionReportByStream([ - entry({ stream: "items", collected: 3 }), - entry({ stream: "other_items", collected: 1 }), + entry({ collected: 3, stream: "items" }), + entry({ collected: 1, stream: "other_items" }), ]); assert.equal(byStream.size, 2); assert.equal(byStream.get("items")?.collected, 3); @@ -81,8 +81,8 @@ test("indexCollectionReportByStream tolerates absence and indexes by stream name test("a duplicate stream name keeps the first entry", () => { const byStream = indexCollectionReportByStream([ - entry({ stream: "items", collected: 5 }), - entry({ stream: "items", collected: 9 }), + entry({ collected: 5, stream: "items" }), + entry({ collected: 9, stream: "items" }), ]); assert.equal(byStream.size, 1); assert.equal(byStream.get("items")?.collected, 5); @@ -92,9 +92,9 @@ test("collectionReportHasOpenGaps distinguishes clean completion from unresolved assert.equal( collectionReportHasOpenGaps([ entry({ - coverage_condition: "complete", - considered: 1, collected: 0, + considered: 1, + coverage_condition: "complete", covered: 1, forward_disposition: "owner_refresh_due", }), @@ -121,7 +121,7 @@ test("runStatusWithCollectionReportGaps promotes only clean success statuses whe assert.equal(runStatusWithCollectionReportGaps("failed", gapReport), "failed"); assert.equal( runStatusWithCollectionReportGaps("succeeded", [ - entry({ coverage_condition: "complete", considered: 1, collected: 0, covered: 1 }), + entry({ collected: 0, considered: 1, coverage_condition: "complete", covered: 1 }), ]), "succeeded" ); @@ -129,7 +129,7 @@ test("runStatusWithCollectionReportGaps promotes only clean success statuses whe test("THE HONESTY GATE: collected records with an unknown considered denominator never imply completeness", () => { const facts = formatStreamCollectionFacts( - entry({ stream: "items", collected: 42, considered: "unknown", coverage_condition: "unknown" }) + entry({ collected: 42, considered: "unknown", coverage_condition: "unknown", stream: "items" }) ); // The coverage chip stays unknown, never complete. assert.equal(facts.coverage.value, "unknown"); @@ -144,7 +144,7 @@ test("THE HONESTY GATE: collected records with an unknown considered denominator test("a known considered denominator renders collected / considered", () => { const facts = formatStreamCollectionFacts( - entry({ stream: "items", collected: 7, considered: 10, coverage_condition: "partial" }) + entry({ collected: 7, considered: 10, coverage_condition: "partial", stream: "items" }) ); assert.equal(facts.countsLabel, "7 / 10 collected"); assert.equal(facts.coverage.value, "partial"); @@ -153,13 +153,13 @@ test("a known considered denominator renders collected / considered", () => { test("strategy-backed complete streams do not render collected / considered as a partial-looking fraction", () => { const facts = formatStreamCollectionFacts( entry({ - stream: "pull_requests", checkpoint: "committed", collected: 9, considered: 52, coverage_condition: "complete", coverage_strategy: "checkpoint_window", forward_disposition: "complete", + stream: "pull_requests", }) ); assert.equal(facts.countsLabel, "checkpoint covered · 9 collected"); @@ -172,13 +172,13 @@ test("strategy-backed complete streams do not render collected / considered as a test("full-inventory complete streams name the inventory proof instead of implying missing records", () => { const facts = formatStreamCollectionFacts( entry({ - stream: "repositories", checkpoint: "committed", collected: 5, considered: 100, coverage_condition: "complete", coverage_strategy: "full_inventory", forward_disposition: "complete", + stream: "repositories", }) ); assert.equal(facts.countsLabel, "inventory covered · 5 collected"); @@ -190,13 +190,13 @@ test("full-inventory complete streams name the inventory proof instead of implyi test("zero-emission singleton proofs still show the proof instead of collection count unavailable", () => { const facts = formatStreamCollectionFacts( entry({ - stream: "user", checkpoint: "committed", collected: 0, considered: "unknown", coverage_condition: "complete", coverage_strategy: "singleton_presence", forward_disposition: "complete", + stream: "user", }) ); assert.equal(facts.countsLabel, "presence checked"); @@ -208,7 +208,7 @@ test("THE CLAMP: collected > considered never renders an impossible fraction (ph // render "3 / 2 collected" — an impossible tuple. The displayed numerator is // clamped to the denominator; the raw count is disclosed in the title. const facts = formatStreamCollectionFacts( - entry({ stream: "items", collected: 3, considered: 2, coverage_condition: "complete" }) + entry({ collected: 3, considered: 2, coverage_condition: "complete", stream: "items" }) ); assert.equal(facts.countsLabel, "2 / 2 collected"); assert.doesNotMatch(facts.countsLabel ?? "", IMPOSSIBLE_COLLECTED_FRACTION); @@ -219,7 +219,7 @@ test("THE CLAMP: collected > considered never renders an impossible fraction (ph test("THE CLAMP: covered > considered is clamped too, raw covered preserved in the title", () => { const facts = formatStreamCollectionFacts( - entry({ stream: "items", collected: 5, considered: 4, covered: 6, coverage_condition: "complete" }) + entry({ collected: 5, considered: 4, coverage_condition: "complete", covered: 6, stream: "items" }) ); assert.equal(facts.countsLabel, "4 / 4 covered · 5 collected"); assert.doesNotMatch(facts.countsLabel ?? "", IMPOSSIBLE_COVERED_FRACTION); @@ -228,7 +228,7 @@ test("THE CLAMP: covered > considered is clamped too, raw covered preserved in t test("THE CLAMP: collected over-report does not imply covered exceeded the denominator", () => { const facts = formatStreamCollectionFacts( - entry({ stream: "items", collected: 5, considered: 4, covered: 3, coverage_condition: "partial" }) + entry({ collected: 5, considered: 4, coverage_condition: "partial", covered: 3, stream: "items" }) ); assert.equal(facts.countsLabel, "3 / 4 covered · 5 collected"); assert.doesNotMatch(facts.countsTitle, COVERED_EXCEEDED_DENOMINATOR_COPY); @@ -238,12 +238,12 @@ test("THE CLAMP: collected over-report does not imply covered exceeded the denom test("a known covered numerator renders covered / considered without hiding the collected count", () => { const facts = formatStreamCollectionFacts( entry({ - stream: "items", collected: 0, considered: 10, - covered: 10, coverage_condition: "complete", + covered: 10, forward_disposition: "complete", + stream: "items", }) ); assert.equal(facts.countsLabel, "10 / 10 covered · 0 collected"); @@ -255,11 +255,11 @@ test("a known covered numerator renders covered / considered without hiding the test("a satisfied known denominator can read complete (the reference's verdict, not ours)", () => { const facts = formatStreamCollectionFacts( entry({ - stream: "items", collected: 10, considered: 10, coverage_condition: "complete", forward_disposition: "complete", + stream: "items", }) ); assert.equal(facts.countsLabel, "10 / 10 collected"); @@ -271,7 +271,7 @@ test("a satisfied known denominator can read complete (the reference's verdict, }); test("zero collected with no considered denominator shows no fabricated progress number", () => { - const facts = formatStreamCollectionFacts(entry({ stream: "items", collected: 0, considered: "unknown" })); + const facts = formatStreamCollectionFacts(entry({ collected: 0, considered: "unknown", stream: "items" })); assert.equal(facts.countsLabel, null); }); @@ -385,7 +385,7 @@ test("an accepted-absence coverage axis never degrades a stream's coverage tone" // the coverage axis specifically, which is the signal that must not degrade.) for (const axis of ["deferred", "inventory_only", "unavailable", "unsupported"] as const) { const facts = formatStreamCollectionFacts( - entry({ coverage_condition: axis, forward_disposition: "unmeasured", collected: 0, considered: "unknown" }) + entry({ collected: 0, considered: "unknown", coverage_condition: axis, forward_disposition: "unmeasured" }) ); assert.equal(facts.coverage.tone, "neutral", `${axis} coverage tone must stay neutral`); assert.notEqual(facts.tone, "warning"); @@ -398,7 +398,7 @@ test("the stream-row deferred pill reads optional/not-collected, not policy jarg // per-stream row must pick up the same visible fix as the connection-level // chip — the owner reads this exact value on the source detail page. const facts = formatStreamCollectionFacts( - entry({ coverage_condition: "deferred", forward_disposition: "unmeasured", collected: 0, considered: "unknown" }) + entry({ collected: 0, considered: "unknown", coverage_condition: "deferred", forward_disposition: "unmeasured" }) ); assert.doesNotMatch(facts.coverage.value, /\bdeferre?s?\b/i); assert.match(facts.coverage.value, /optional/i); @@ -411,7 +411,7 @@ test("required missing evidence (unknown coverage) stays distinct from accepted // evidence, never as a settled accepted-absence policy, so the two states // remain distinguishable on the stream row after the copy-only fix. const unmeasured = formatStreamCollectionFacts( - entry({ coverage_condition: "unknown", forward_disposition: "unmeasured", collected: 0, considered: "unknown" }) + entry({ collected: 0, considered: "unknown", coverage_condition: "unknown", forward_disposition: "unmeasured" }) ); assert.equal(unmeasured.coverage.value, "unknown"); assert.notEqual( @@ -428,7 +428,7 @@ test("active pending work (checking) stays distinct from accepted absence", () = // A stream mid-run reads `checking`, which is a different signal than a // settled accepted-absence policy — neither implies the other. const checking = formatStreamCollectionFacts( - entry({ coverage_condition: "unknown", forward_disposition: "checking", collected: 0, considered: "unknown" }) + entry({ collected: 0, considered: "unknown", coverage_condition: "unknown", forward_disposition: "checking" }) ); assert.equal(checking.disposition?.value, "checking"); for (const axis of ["deferred", "inventory_only", "unavailable", "unsupported"] as const) { diff --git a/apps/console/src/app/(console)/lib/collection-report.ts b/apps/console/src/app/(console)/lib/collection-report.ts index 34e0cfdd6..74892563e 100644 --- a/apps/console/src/app/(console)/lib/collection-report.ts +++ b/apps/console/src/app/(console)/lib/collection-report.ts @@ -241,9 +241,9 @@ function buildSkipLabel(skip: RefCollectionReportEntry["skipped"]): string | nul const TONE_RANK: Record = { danger: 3, - warning: 2, - success: 1, neutral: 0, + success: 1, + warning: 2, }; /** The stronger of two tones (danger > warning > success > neutral). */ @@ -285,15 +285,15 @@ export function formatStreamCollectionFacts(entry: RefCollectionReportEntry): St } return { - stream: entry.stream, - coverage, - disposition, countsLabel: counts.label, countsTitle: counts.title, + coverage, + disposition, pendingDetailGaps, pendingDetailGapsIsFloor, pendingDetailGapsLabel: formatPendingDetailGapsLabel(pendingDetailGaps, pendingDetailGapsIsFloor), skipLabel, + stream: entry.stream, tone, }; } diff --git a/apps/console/src/app/(console)/lib/connection-catalog.test.ts b/apps/console/src/app/(console)/lib/connection-catalog.test.ts index 705108447..8e6356b69 100644 --- a/apps/console/src/app/(console)/lib/connection-catalog.test.ts +++ b/apps/console/src/app/(console)/lib/connection-catalog.test.ts @@ -84,7 +84,7 @@ test("catalogModalityFromManifest mirrors the filesystem>browser>network precede assert.equal( catalogModalityFromManifest({ connector_id: "x", - runtime_requirements: { bindings: { filesystem: {}, browser: {} } }, + runtime_requirements: { bindings: { browser: {}, filesystem: {} } }, }), "local_collector" ); @@ -150,12 +150,12 @@ test("browser-bound static-secret-capable connectors get the same dual choice ge display_name: "Browser Sample", runtime_requirements: { bindings: { browser: { required: true } } }, setup: { - modality: "static_secret", credential_capture: { + fields: [{ label: "Provider secret", name: "secret", required: true, secret: true }], kind: "username_password", label: "Browser sign-in", - fields: [{ name: "secret", label: "Provider secret", required: true, secret: true }], }, + modality: "static_secret", }, } as CatalogManifestLike, ]); @@ -176,12 +176,12 @@ test("non-browser static-secret connectors keep the existing single capture path display_name: "Gmail", runtime_requirements: { bindings: { network: { required: true } } }, setup: { - modality: "static_secret", credential_capture: { + fields: [{ label: "Provider secret", name: "secret", required: true, secret: true }], kind: "app_password", label: "Gmail app password", - fields: [{ name: "secret", label: "Provider secret", required: true, secret: true }], }, + modality: "static_secret", }, } as CatalogManifestLike, ]); @@ -247,8 +247,8 @@ function manualUploadConnectManifest(connectorId: string): CatalogManifestLike { return { ...manualUploadManifest(connectorId), setup: { - modality: "manual_or_upload", manual_or_upload: { + accepted_file_names: ["Timeline.json"], acquisition_methods: [ { detail: "Use the phone export and upload the JSON file.", @@ -264,10 +264,10 @@ function manualUploadConnectManifest(connectorId: string): CatalogManifestLike { posture: "advanced", }, ], - accepted_file_names: ["Timeline.json"], import_dir_env_var: "GOOGLE_MAPS_TIMELINE_DIR", label: "Timeline export", }, + modality: "manual_or_upload", }, }; } @@ -381,15 +381,15 @@ test("other network connectors stay flatly api_network_unsupported", async () => test("provider-authorization deployment blockers are separate from unsupported network entries", () => { const catalog = buildConnectorCatalog([ { - connector_id: "fitness_oauth", - display_name: "Fitness OAuth", - runtime_requirements: { bindings: { network: { required: true } } }, capabilities: { auth: { - kind: "oauth", deployment_config: ["FITNESS_OAUTH_CLIENT_ID", "FITNESS_OAUTH_CLIENT_SECRET"], + kind: "oauth", }, }, + connector_id: "fitness_oauth", + display_name: "Fitness OAuth", + runtime_requirements: { bindings: { network: { required: true } } }, }, ]); const [entry] = catalog; diff --git a/apps/console/src/app/(console)/lib/connection-catalog.ts b/apps/console/src/app/(console)/lib/connection-catalog.ts index 65c5a24e9..af65fb757 100644 --- a/apps/console/src/app/(console)/lib/connection-catalog.ts +++ b/apps/console/src/app/(console)/lib/connection-catalog.ts @@ -211,8 +211,8 @@ export function buildConnectorCatalog(manifests: readonly CatalogManifestLike[]) connectorKey, deploymentReadiness: plan.deploymentReadiness, displayName: displayNameFor(manifest, connectorKey), - modality: plan.connectorModality, disposition: plan.catalogDisposition, + modality: plan.connectorModality, nextStepKind: plan.nextStepKind, proofGate: plan.proofGate, runbookPath: plan.runbookPath, diff --git a/apps/console/src/app/(console)/lib/connection-control-result.test.ts b/apps/console/src/app/(console)/lib/connection-control-result.test.ts index c82c75ad8..3c0eea9c6 100644 --- a/apps/console/src/app/(console)/lib/connection-control-result.test.ts +++ b/apps/console/src/app/(console)/lib/connection-control-result.test.ts @@ -65,8 +65,8 @@ test("revoke on an unexpected status throws a described error", () => { test("delete 200 maps to deleted and carries the record count", () => { assert.deepEqual(classifyDeleteConnectionResponse(200, { deleted: true, deleted_record_count: 42 }, null), { - status: "deleted", deletedRecordCount: 42, + status: "deleted", }); }); diff --git a/apps/console/src/app/(console)/lib/connection-evidence.test.ts b/apps/console/src/app/(console)/lib/connection-evidence.test.ts index eda7871b3..8a1f6a805 100644 --- a/apps/console/src/app/(console)/lib/connection-evidence.test.ts +++ b/apps/console/src/app/(console)/lib/connection-evidence.test.ts @@ -66,12 +66,12 @@ test("formatForwardDisposition renders unmeasured coverage as resting missing ev function backlog(overrides: Partial = {}): RefDetailGapBacklog { return { + max_attempt_count: 0, + next_attempt_at: null, pending: 0, pending_is_floor: false, pending_other: 0, pending_other_is_floor: false, - max_attempt_count: 0, - next_attempt_at: null, recovered: null, ...overrides, }; @@ -79,16 +79,16 @@ function backlog(overrides: Partial = {}): RefDetailGapBack function snapshot(overrides: Partial = {}): RefConnectionHealthSnapshot { return { - state: "healthy", - reason_code: null, - unknown_reasons: [], - last_success_at: "2026-05-19T12:00:00Z", - next_attempt_at: null, - next_action: null, + axes: { attention: "none", coverage: "complete", freshness: "fresh", outbox: "idle" }, badges: { stale: false, syncing: false }, - axes: { coverage: "complete", freshness: "fresh", attention: "none", outbox: "idle" }, conditions: [], dominant_condition_id: null, + last_success_at: "2026-05-19T12:00:00Z", + next_action: null, + next_attempt_at: null, + reason_code: null, + state: "healthy", + unknown_reasons: [], ...overrides, }; } @@ -96,11 +96,11 @@ function snapshot(overrides: Partial = {}): RefConn function baseOverview(overrides: Partial = {}): ConnectorOverview { return { connector: { connector_id: "demo", display_name: "Demo" }, - streams: [], - totalRecords: 0, + isRunning: false, lastRun: null, lastSuccessfulRun: null, - isRunning: false, + streams: [], + totalRecords: 0, ...overrides, }; } @@ -268,39 +268,39 @@ test("outbox active is colour-coded as a progressing (non-neutral) state", () => test("formatSourceOutboxState distinguishes granular local collector states", () => { const failedUploadOutbox = formatSourceOutboxState({ - outbox_state: "dead_letter", outbox_diagnostics: { dead_letter: 1 }, + outbox_state: "dead_letter", }); assert.equal(failedUploadOutbox.tone, "danger"); assert.equal(failedUploadOutbox.label, "Outbox · failed uploads"); assert.equal(failedUploadOutbox.value, "failed uploads"); assert.doesNotMatch(failedUploadOutbox.title, /dead-letter/i); assert.equal( - formatSourceOutboxState({ outbox_state: "stale", outbox_diagnostics: { stale_leases: 1 } }).tone, + formatSourceOutboxState({ outbox_diagnostics: { stale_leases: 1 }, outbox_state: "stale" }).tone, "danger" ); assert.equal( - formatSourceOutboxState({ outbox_state: "retrying", outbox_diagnostics: { retrying: 1 } }).tone, + formatSourceOutboxState({ outbox_diagnostics: { retrying: 1 }, outbox_state: "retrying" }).tone, "warning" ); assert.equal( - formatSourceOutboxState({ outbox_state: "pending", outbox_diagnostics: { pending: 1 } }).label, + formatSourceOutboxState({ outbox_diagnostics: { pending: 1 }, outbox_state: "pending" }).label, "Outbox · pending" ); assert.equal( - formatSourceOutboxState({ outbox_state: "backlog", outbox_diagnostics: { backlog_open: 1 } }).tone, + formatSourceOutboxState({ outbox_diagnostics: { backlog_open: 1 }, outbox_state: "backlog" }).tone, "warning" ); assert.equal( - formatSourceOutboxState({ outbox_state: "drained", outbox_diagnostics: { total: 2, succeeded: 2 } }).tone, + formatSourceOutboxState({ outbox_diagnostics: { succeeded: 2, total: 2 }, outbox_state: "drained" }).tone, "success" ); - assert.equal(formatSourceOutboxState({ outbox_state: undefined, outbox_diagnostics: null }).tone, "neutral"); + assert.equal(formatSourceOutboxState({ outbox_diagnostics: null, outbox_state: undefined }).tone, "neutral"); }); test("summarizeAxisChips omits attention when none and always includes coverage/freshness/outbox", () => { const out = summarizeAxisChips( - snapshot({ axes: { coverage: "complete", freshness: "fresh", attention: "none", outbox: "idle" } }).axes + snapshot({ axes: { attention: "none", coverage: "complete", freshness: "fresh", outbox: "idle" } }).axes ); assert.equal( out.some((c) => c.label.startsWith("Attention")), @@ -315,7 +315,7 @@ test("summarizeAxisChips omits attention when none and always includes coverage/ test("summarizeAxisChips surfaces attention when open", () => { const out = summarizeAxisChips( - snapshot({ axes: { coverage: "gaps", freshness: "fresh", attention: "open", outbox: "idle" } }).axes + snapshot({ axes: { attention: "open", coverage: "gaps", freshness: "fresh", outbox: "idle" } }).axes ); assert.equal(out.length, 4); assert.ok(out.some((c) => c.label.startsWith("Attention"))); @@ -354,7 +354,7 @@ test("summarizeAxisChips omits the outbox chip for a non-local connection with u // The exact owner-reported defect: a Gmail/Chase-class connection shows // "Outbox · unknown" purely because the reference has no heartbeats for it. const out = summarizeAxisChips( - snapshot({ axes: { coverage: "complete", freshness: "fresh", attention: "none", outbox: "unknown" } }).axes, + snapshot({ axes: { attention: "none", coverage: "complete", freshness: "fresh", outbox: "unknown" } }).axes, { isLocalDeviceBacked: false } ); assert.equal(out.length, 2); @@ -366,7 +366,7 @@ test("summarizeAxisChips omits the outbox chip for a non-local connection with u test("summarizeAxisChips keeps the outbox chip for a local-backed connection with unknown outbox, sharpened to 'evidence unavailable'", () => { const out = summarizeAxisChips( - snapshot({ axes: { coverage: "complete", freshness: "fresh", attention: "none", outbox: "unknown" } }).axes, + snapshot({ axes: { attention: "none", coverage: "complete", freshness: "fresh", outbox: "unknown" } }).axes, { isLocalDeviceBacked: true } ); assert.equal(out.length, 3); @@ -382,7 +382,7 @@ test("summarizeAxisChips shows a stalled outbox even for a non-local connection // Defensive: if the reference ever projects a real stalled verdict without a // local_device_progress row, the danger signal must not be hidden. const out = summarizeAxisChips( - snapshot({ axes: { coverage: "complete", freshness: "fresh", attention: "none", outbox: "stalled" } }).axes, + snapshot({ axes: { attention: "none", coverage: "complete", freshness: "fresh", outbox: "stalled" } }).axes, { isLocalDeviceBacked: false } ); const outbox = out.find((c) => c.dimension === "Outbox"); @@ -392,7 +392,7 @@ test("summarizeAxisChips shows a stalled outbox even for a non-local connection test("summarizeAxisChips shows a colour-coded active outbox for a local-backed connection", () => { const out = summarizeAxisChips( - snapshot({ axes: { coverage: "complete", freshness: "fresh", attention: "none", outbox: "active" } }).axes, + snapshot({ axes: { attention: "none", coverage: "complete", freshness: "fresh", outbox: "active" } }).axes, { isLocalDeviceBacked: true } ); const outbox = out.find((c) => c.dimension === "Outbox"); @@ -404,7 +404,7 @@ test("summarizeAxisChips defaults to omitting an unknown outbox when no local-ba // Back-compat: callers that have not threaded the signal get the honest // (non-local) default — an absence-default unknown outbox is suppressed. const out = summarizeAxisChips( - snapshot({ axes: { coverage: "complete", freshness: "fresh", attention: "none", outbox: "unknown" } }).axes + snapshot({ axes: { attention: "none", coverage: "complete", freshness: "fresh", outbox: "unknown" } }).axes ); assert.equal( out.some((c) => c.dimension === "Outbox"), @@ -436,28 +436,28 @@ test("formatProjectionFreshness handles missing snapshot", () => { test("formatDominantCondition surfaces only false dominant evidence", () => { const out = formatDominantCondition( snapshot({ - state: "blocked", - dominant_condition_id: "CredentialsValid:auth_expired", conditions: [ { + expires_at: null, id: "CredentialsValid:auth_expired", - type: "CredentialsValid", - status: "false", - severity: "blocked", - reason: "auth_expired", message: "The source rejected the configured credentials.", - origin: "readiness", observed_at: null, - expires_at: null, - sensitivity: "secret_redacted", + origin: "readiness", + reason: "auth_expired", remediation: { action: "refresh_credentials", label: "Reconnect this account", retryable: false, target: "credentials", }, + sensitivity: "secret_redacted", + severity: "blocked", + status: "false", + type: "CredentialsValid", }, ], + dominant_condition_id: "CredentialsValid:auth_expired", + state: "blocked", }) ); assert.equal(out?.tone, "danger"); @@ -482,12 +482,12 @@ test("formatLastDurableProgress reports last successful event count when present hasError: false, lastRun: null, lastSuccessfulRun: { - run_id: "r1", + event_count: 42, + failure_reason: null, first_at: "x", last_at: "y", - event_count: 42, + run_id: "r1", status: "succeeded", - failure_reason: null, }, totalRecords: 50, }); @@ -499,12 +499,12 @@ test("formatLastDurableProgress reports last attempt when no success and no erro const out = formatLastDurableProgress({ hasError: false, lastRun: { - run_id: "r2", + event_count: 0, + failure_reason: "boom", first_at: "x", last_at: "y", - event_count: 0, + run_id: "r2", status: "failed", - failure_reason: "boom", }, lastSuccessfulRun: null, totalRecords: 0, @@ -579,13 +579,13 @@ test("formatLastDurableProgress still prefers scheduler-run evidence over local- hasError: false, lastRun: null, lastSuccessfulRun: { - run_id: "r9", - first_at: "x", - last_at: "y", event_count: 3, - status: "succeeded", failure_reason: null, + first_at: "x", known_gaps: [], + last_at: "y", + run_id: "r9", + status: "succeeded", }, localDeviceProgress: { last_heartbeat_at: "2026-05-22T16:30:00Z", @@ -603,11 +603,11 @@ test("formatLastDurableProgress still prefers scheduler-run evidence over local- test("summarizeOutboxForRow returns null for idle and a label otherwise", () => { assert.equal(summarizeOutboxForRow(snapshot()), null); const stalled = summarizeOutboxForRow( - snapshot({ axes: { coverage: "complete", freshness: "fresh", attention: "none", outbox: "stalled" } }) + snapshot({ axes: { attention: "none", coverage: "complete", freshness: "fresh", outbox: "stalled" } }) ); assert.equal(stalled?.tone, "danger"); const unknown = summarizeOutboxForRow( - snapshot({ axes: { coverage: "complete", freshness: "fresh", attention: "none", outbox: "unknown" } }) + snapshot({ axes: { attention: "none", coverage: "complete", freshness: "fresh", outbox: "unknown" } }) ); assert.equal(unknown?.tone, "neutral"); }); @@ -618,22 +618,22 @@ test("summarizeOutboxForRow returns null when there is no snapshot at all", () = function clearBacklogCondition(overrides: Partial = {}): RefConnectionHealthCondition { return { + expires_at: null, id: "cond-backlog", - type: "LocalExporterAvailable", - status: "false", - severity: "error", - reason: "local_exporter_stalled", message: "Local exporter work is stalled or blocked.", - origin: "local_device", observed_at: "2026-05-19T12:00:00Z", - expires_at: null, - sensitivity: "owner", + origin: "local_device", + reason: "local_exporter_stalled", remediation: { action: "clear_backlog", label: "Inspect the local collector backlog", retryable: true, target: "local_device", }, + sensitivity: "owner", + severity: "error", + status: "false", + type: "LocalExporterAvailable", ...overrides, }; } @@ -641,9 +641,9 @@ function clearBacklogCondition(overrides: Partial test("summarizeOutboxStallRemediation surfaces the reference remediation label for a clear_backlog condition", () => { const remediation = summarizeOutboxStallRemediation( snapshot({ - state: "degraded", - axes: { coverage: "complete", freshness: "fresh", attention: "none", outbox: "stalled" }, + axes: { attention: "none", coverage: "complete", freshness: "fresh", outbox: "stalled" }, conditions: [clearBacklogCondition()], + state: "degraded", }) ); assert.equal(remediation?.label, "Inspect the local collector backlog"); @@ -652,7 +652,7 @@ test("summarizeOutboxStallRemediation surfaces the reference remediation label f test("summarizeOutboxStallRemediation stays quiet for a stalled axis without a clear_backlog action", () => { const remediation = summarizeOutboxStallRemediation( - snapshot({ axes: { coverage: "complete", freshness: "fresh", attention: "none", outbox: "stalled" } }) + snapshot({ axes: { attention: "none", coverage: "complete", freshness: "fresh", outbox: "stalled" } }) ); assert.equal(remediation, null); @@ -662,7 +662,7 @@ test("summarizeOutboxStallRemediation stays quiet for healthy/idle/active/unknow for (const outbox of ["idle", "active", "unknown"] as const) { assert.equal( summarizeOutboxStallRemediation( - snapshot({ axes: { coverage: "complete", freshness: "fresh", attention: "none", outbox } }) + snapshot({ axes: { attention: "none", coverage: "complete", freshness: "fresh", outbox } }) ), null, `expected no remediation for outbox=${outbox}` @@ -676,7 +676,7 @@ test("summarizeOutboxStallRemediation ignores a clear_backlog remediation on a r // remediation noise once the outbox is no longer stalled. const remediation = summarizeOutboxStallRemediation( snapshot({ - axes: { coverage: "complete", freshness: "fresh", attention: "none", outbox: "idle" }, + axes: { attention: "none", coverage: "complete", freshness: "fresh", outbox: "idle" }, conditions: [clearBacklogCondition({ status: "true" })], }) ); @@ -686,7 +686,7 @@ test("summarizeOutboxStallRemediation ignores a clear_backlog remediation on a r test("summarizeOutboxStallRemediation: scale is null when no count rollup is available", () => { const remediation = summarizeOutboxStallRemediation( snapshot({ - axes: { coverage: "complete", freshness: "fresh", attention: "none", outbox: "stalled" }, + axes: { attention: "none", coverage: "complete", freshness: "fresh", outbox: "stalled" }, conditions: [clearBacklogCondition()], }) ); @@ -696,14 +696,14 @@ test("summarizeOutboxStallRemediation: scale is null when no count rollup is ava test("summarizeOutboxStallRemediation: surfaces a count-backed scale from outbox_counts on a stall", () => { const remediation = summarizeOutboxStallRemediation( snapshot({ - axes: { coverage: "complete", freshness: "fresh", attention: "none", outbox: "stalled" }, + axes: { attention: "none", coverage: "complete", freshness: "fresh", outbox: "stalled" }, conditions: [clearBacklogCondition()], }), { last_heartbeat_at: "2026-05-19T11:55:00Z", last_heartbeat_status: "blocked", last_ingest_at: null, - outbox_counts: { pending: 12, dead_letter: 2, stale_leases: 1, total: 15 }, + outbox_counts: { dead_letter: 2, pending: 12, stale_leases: 1, total: 15 }, records_pending: 12, source_count: 1, } @@ -714,14 +714,14 @@ test("summarizeOutboxStallRemediation: surfaces a count-backed scale from outbox test("summarizeOutboxStallRemediation: scale omits zero categories so it never reads as alarming noise", () => { const remediation = summarizeOutboxStallRemediation( snapshot({ - axes: { coverage: "complete", freshness: "fresh", attention: "none", outbox: "stalled" }, + axes: { attention: "none", coverage: "complete", freshness: "fresh", outbox: "stalled" }, conditions: [clearBacklogCondition()], }), { last_heartbeat_at: "2026-05-19T11:55:00Z", last_heartbeat_status: "blocked", last_ingest_at: null, - outbox_counts: { pending: 0, dead_letter: 3, stale_leases: 0, total: 3 }, + outbox_counts: { dead_letter: 3, pending: 0, stale_leases: 0, total: 3 }, records_pending: 0, source_count: 1, } @@ -735,12 +735,12 @@ test("summarizeOutboxStallRemediation: counts never attach to a quiet (non-stall for (const outbox of ["idle", "active", "unknown"] as const) { assert.equal( summarizeOutboxStallRemediation( - snapshot({ axes: { coverage: "complete", freshness: "fresh", attention: "none", outbox } }), + snapshot({ axes: { attention: "none", coverage: "complete", freshness: "fresh", outbox } }), { last_heartbeat_at: "2026-05-19T11:55:00Z", last_heartbeat_status: "healthy", last_ingest_at: "2026-05-19T11:55:00Z", - outbox_counts: { pending: 5, dead_letter: 1 }, + outbox_counts: { dead_letter: 1, pending: 5 }, records_pending: 5, source_count: 1, } @@ -939,8 +939,8 @@ test("deriveConnectionStatusDisplay: healthy scheduled connection reads as a hea const out = deriveConnectionStatusDisplay({ hasDurableProgress: true, health: snapshot({ + axes: { attention: "none", coverage: "complete", freshness: "fresh", outbox: "idle" }, state: "healthy", - axes: { coverage: "complete", freshness: "fresh", attention: "none", outbox: "idle" }, }), localDeviceProgress: null, }); @@ -957,8 +957,8 @@ test("deriveConnectionStatusDisplay: stale-but-healthy connection does not claim const out = deriveConnectionStatusDisplay({ hasDurableProgress: true, health: snapshot({ + axes: { attention: "none", coverage: "complete", freshness: "stale", outbox: "idle" }, state: "healthy", - axes: { coverage: "complete", freshness: "stale", attention: "none", outbox: "idle" }, }), localDeviceProgress: null, }); @@ -973,8 +973,8 @@ test("deriveConnectionStatusDisplay: fresh-and-healthy connection still reads cu const out = deriveConnectionStatusDisplay({ hasDurableProgress: true, health: snapshot({ + axes: { attention: "none", coverage: "complete", freshness: "fresh", outbox: "idle" }, state: "healthy", - axes: { coverage: "complete", freshness: "fresh", attention: "none", outbox: "idle" }, }), localDeviceProgress: null, }); @@ -1004,8 +1004,8 @@ test("deriveConnectionStatusDisplay: local collector with ingest evidence and id const out = deriveConnectionStatusDisplay({ hasDurableProgress: true, health: snapshot({ + axes: { attention: "none", coverage: "unknown", freshness: "unknown", outbox: "idle" }, state: "idle", - axes: { coverage: "unknown", freshness: "unknown", attention: "none", outbox: "idle" }, }), localDeviceProgress: { last_heartbeat_at: "2026-05-22T16:30:00Z", @@ -1054,8 +1054,8 @@ test("deriveConnectionStatusDisplay: outbox=active overrides idle as Syncing reg const out = deriveConnectionStatusDisplay({ hasDurableProgress: true, health: snapshot({ + axes: { attention: "none", coverage: "complete", freshness: "fresh", outbox: "active" }, state: "idle", - axes: { coverage: "complete", freshness: "fresh", attention: "none", outbox: "active" }, }), localDeviceProgress: null, }); @@ -1067,29 +1067,29 @@ test("deriveConnectionStatusDisplay: blocked connection surfaces dominant condit const out = deriveConnectionStatusDisplay({ hasDurableProgress: false, health: snapshot({ - state: "blocked", - reason_code: "credential_rejected", - dominant_condition_id: "CredentialsValid:credential_rejected", conditions: [ { + expires_at: null, id: "CredentialsValid:credential_rejected", - type: "CredentialsValid", - status: "false", - severity: "blocked", - reason: "credential_rejected", message: "The source rejected the configured credentials.", - origin: "readiness", observed_at: null, - expires_at: null, - sensitivity: "secret_redacted", + origin: "readiness", + reason: "credential_rejected", remediation: { action: "refresh_credentials", label: "Reconnect this account", retryable: false, target: "credentials", }, + sensitivity: "secret_redacted", + severity: "blocked", + status: "false", + type: "CredentialsValid", }, ], + dominant_condition_id: "CredentialsValid:credential_rejected", + reason_code: "credential_rejected", + state: "blocked", }), localDeviceProgress: null, }); @@ -1103,7 +1103,7 @@ test("deriveConnectionStatusDisplay: blocked connection surfaces dominant condit test("deriveConnectionStatusDisplay: source-pressure cooling_off reads as catch-up, never a raw token", () => { const out = deriveConnectionStatusDisplay({ hasDurableProgress: true, - health: snapshot({ state: "cooling_off", reason_code: "source_pressure" }), + health: snapshot({ reason_code: "source_pressure", state: "cooling_off" }), localDeviceProgress: null, }); assert.equal(out.label, "Cooling off"); @@ -1118,7 +1118,7 @@ test("deriveConnectionStatusDisplay: source-pressure cooling_off reads as catch- test("deriveConnectionStatusDisplay: failure-backoff cooling_off keeps the retry-wait copy", () => { const out = deriveConnectionStatusDisplay({ hasDurableProgress: true, - health: snapshot({ state: "cooling_off", reason_code: "scheduler_backoff_active" }), + health: snapshot({ reason_code: "scheduler_backoff_active", state: "cooling_off" }), localDeviceProgress: null, }); assert.equal(out.label, "Cooling off"); @@ -1129,8 +1129,8 @@ test("deriveConnectionStatusDisplay: degraded with partial coverage reads as Par const out = deriveConnectionStatusDisplay({ hasDurableProgress: true, health: snapshot({ + axes: { attention: "none", coverage: "partial", freshness: "fresh", outbox: "idle" }, state: "degraded", - axes: { coverage: "partial", freshness: "fresh", attention: "none", outbox: "idle" }, }), localDeviceProgress: null, }); @@ -1143,8 +1143,8 @@ test("deriveConnectionStatusDisplay: degraded with retryable coverage reads as R const out = deriveConnectionStatusDisplay({ hasDurableProgress: true, health: snapshot({ + axes: { attention: "none", coverage: "retryable_gap", freshness: "unknown", outbox: "unknown" }, state: "degraded", - axes: { coverage: "retryable_gap", freshness: "unknown", attention: "none", outbox: "unknown" }, }), localDeviceProgress: null, }); @@ -1160,28 +1160,28 @@ test("deriveConnectionStatusDisplay: needs_attention surfaces dominant condition const out = deriveConnectionStatusDisplay({ hasDurableProgress: true, health: snapshot({ - state: "needs_attention", - dominant_condition_id: "AttentionClear:otp_required", conditions: [ { + expires_at: null, id: "AttentionClear:otp_required", - type: "AttentionClear", - status: "false", - severity: "warning", - reason: "otp_required", message: "A one-time passcode is required to continue.", - origin: "operator", observed_at: null, - expires_at: null, - sensitivity: "owner", + origin: "operator", + reason: "otp_required", remediation: { action: "provide_value", label: "Provide the OTP", retryable: true, target: "dashboard", }, + sensitivity: "owner", + severity: "warning", + status: "false", + type: "AttentionClear", }, ], + dominant_condition_id: "AttentionClear:otp_required", + state: "needs_attention", }), localDeviceProgress: null, }); @@ -1226,8 +1226,8 @@ test("next-step guidance is suppressed when a structured next_action already ren hasDominantCondition: false, hasStructuredNextAction: true, health: snapshot({ + axes: { attention: "open", coverage: "complete", freshness: "stale", outbox: "idle" }, state: "needs_attention", - axes: { coverage: "complete", freshness: "stale", attention: "open", outbox: "idle" }, }), supportsOwnerSync: true, }); @@ -1277,8 +1277,8 @@ test("action-bearing guidance still fires even when a dominant condition is pres hasDominantCondition: true, hasStructuredNextAction: false, health: snapshot({ + axes: { attention: "none", coverage: "complete", freshness: "fresh", outbox: "stalled" }, state: "degraded", - axes: { coverage: "complete", freshness: "fresh", attention: "none", outbox: "stalled" }, }), supportsOwnerSync: true, }); @@ -1290,7 +1290,7 @@ test("cooling_off guidance names the next attempt time when known", () => { const out = deriveConnectionNextStep({ hasDominantCondition: false, hasStructuredNextAction: false, - health: snapshot({ state: "cooling_off", next_attempt_at: "2026-05-19T13:00:00Z" }), + health: snapshot({ next_attempt_at: "2026-05-19T13:00:00Z", state: "cooling_off" }), supportsOwnerSync: true, }); assert.ok(out); @@ -1304,7 +1304,7 @@ test("cooling_off with no source-pressure reason still reads as failure backoff" const out = deriveConnectionNextStep({ hasDominantCondition: false, hasStructuredNextAction: false, - health: snapshot({ state: "cooling_off", next_attempt_at: "2026-05-19T13:00:00Z" }), + health: snapshot({ next_attempt_at: "2026-05-19T13:00:00Z", state: "cooling_off" }), supportsOwnerSync: true, }); assert.ok(out); @@ -1321,9 +1321,9 @@ test("source-pressure cooling_off reads as catching up, not a failure", () => { hasDominantCondition: false, hasStructuredNextAction: false, health: snapshot({ - state: "cooling_off", - reason_code: "source_pressure", next_attempt_at: "2026-05-19T13:00:00Z", + reason_code: "source_pressure", + state: "cooling_off", }), supportsOwnerSync: true, }); @@ -1340,7 +1340,7 @@ test("source-pressure cooling_off without a next attempt still reads as catching const out = deriveConnectionNextStep({ hasDominantCondition: false, hasStructuredNextAction: false, - health: snapshot({ state: "cooling_off", reason_code: "source_pressure", next_attempt_at: null }), + health: snapshot({ next_attempt_at: null, reason_code: "source_pressure", state: "cooling_off" }), supportsOwnerSync: true, }); assert.ok(out); @@ -1369,10 +1369,10 @@ test("source-pressure cooling_off attaches a backlog count plus the backlog's ow hasDominantCondition: false, hasStructuredNextAction: false, health: snapshot({ - state: "cooling_off", - reason_code: "source_pressure", + detail_gap_backlog: backlog({ next_attempt_at: "2026-05-19T13:30:00Z", pending: 42 }), next_attempt_at: "2026-05-19T13:00:00Z", - detail_gap_backlog: backlog({ pending: 42, next_attempt_at: "2026-05-19T13:30:00Z" }), + reason_code: "source_pressure", + state: "cooling_off", }), supportsOwnerSync: true, }); @@ -1390,10 +1390,10 @@ test("the backlog resume floor is independent of the connection-level scheduler hasDominantCondition: false, hasStructuredNextAction: false, health: snapshot({ - state: "cooling_off", - reason_code: "source_pressure", + detail_gap_backlog: backlog({ next_attempt_at: "2026-05-19T14:00:00Z", pending: 8 }), next_attempt_at: null, - detail_gap_backlog: backlog({ pending: 8, next_attempt_at: "2026-05-19T14:00:00Z" }), + reason_code: "source_pressure", + state: "cooling_off", }), supportsOwnerSync: true, }); @@ -1406,10 +1406,10 @@ test("source-pressure backlog with a bounded read reads as a floor, never exact" hasDominantCondition: false, hasStructuredNextAction: false, health: snapshot({ - state: "cooling_off", - reason_code: "source_pressure", - next_attempt_at: null, detail_gap_backlog: backlog({ pending: 100, pending_is_floor: true }), + next_attempt_at: null, + reason_code: "source_pressure", + state: "cooling_off", }), supportsOwnerSync: true, }); @@ -1424,10 +1424,10 @@ test("an unreadable backlog rollup never invents a count", () => { hasDominantCondition: false, hasStructuredNextAction: false, health: snapshot({ - state: "cooling_off", - reason_code: "source_pressure", - next_attempt_at: "2026-05-19T13:00:00Z", detail_gap_backlog: null, + next_attempt_at: "2026-05-19T13:00:00Z", + reason_code: "source_pressure", + state: "cooling_off", }), supportsOwnerSync: true, }); @@ -1440,10 +1440,10 @@ test("a drained backlog with recoveries reads as caught up, not broken", () => { hasDominantCondition: false, hasStructuredNextAction: false, health: snapshot({ - state: "cooling_off", - reason_code: "source_pressure", - next_attempt_at: null, detail_gap_backlog: backlog({ pending: 0, recovered: 17 }), + next_attempt_at: null, + reason_code: "source_pressure", + state: "cooling_off", }), supportsOwnerSync: true, }); @@ -1457,10 +1457,10 @@ test("a drained backlog with no recovery aggregate still reads as caught up (rea hasDominantCondition: false, hasStructuredNextAction: false, health: snapshot({ - state: "cooling_off", - reason_code: "source_pressure", - next_attempt_at: null, detail_gap_backlog: backlog({ pending: 0, recovered: null }), + next_attempt_at: null, + reason_code: "source_pressure", + state: "cooling_off", }), supportsOwnerSync: true, }); @@ -1473,10 +1473,10 @@ test("a source-pressure-drained backlog does not say caught up when other detail hasDominantCondition: false, hasStructuredNextAction: false, health: snapshot({ - state: "cooling_off", - reason_code: "source_pressure", - next_attempt_at: null, detail_gap_backlog: backlog({ pending: 0, pending_other: 1899, pending_other_is_floor: true }), + next_attempt_at: null, + reason_code: "source_pressure", + state: "cooling_off", }), supportsOwnerSync: true, }); @@ -1490,10 +1490,10 @@ test("degraded+retryable_gap manual path carries the backlog count", () => { hasDominantCondition: false, hasStructuredNextAction: false, health: snapshot({ - state: "degraded", - next_attempt_at: null, - axes: { coverage: "retryable_gap", freshness: "unknown", attention: "none", outbox: "unknown" }, + axes: { attention: "none", coverage: "retryable_gap", freshness: "unknown", outbox: "unknown" }, detail_gap_backlog: backlog({ pending: 3 }), + next_attempt_at: null, + state: "degraded", }), supportsOwnerSync: true, }); @@ -1507,10 +1507,10 @@ test("a singular pending gap uses singular noun", () => { hasDominantCondition: false, hasStructuredNextAction: false, health: snapshot({ - state: "degraded", - next_attempt_at: null, - axes: { coverage: "retryable_gap", freshness: "unknown", attention: "none", outbox: "idle" }, + axes: { attention: "none", coverage: "retryable_gap", freshness: "unknown", outbox: "idle" }, detail_gap_backlog: backlog({ pending: 1 }), + next_attempt_at: null, + state: "degraded", }), supportsOwnerSync: false, }); @@ -1526,9 +1526,9 @@ test("the device-outbox scale and the source-pressure backlog scale never collid hasDominantCondition: false, hasStructuredNextAction: false, health: snapshot({ - state: "cooling_off", - reason_code: "source_pressure", detail_gap_backlog: backlog({ pending: 5 }), + reason_code: "source_pressure", + state: "cooling_off", }), supportsOwnerSync: true, }); @@ -1541,8 +1541,8 @@ test("degraded+partial coverage routes to a coverage review, not a sync", () => hasDominantCondition: false, hasStructuredNextAction: false, health: snapshot({ + axes: { attention: "none", coverage: "gaps", freshness: "fresh", outbox: "idle" }, state: "degraded", - axes: { coverage: "gaps", freshness: "fresh", attention: "none", outbox: "idle" }, }), supportsOwnerSync: true, }); @@ -1556,9 +1556,9 @@ test("degraded+retryable_gap with owner sync offers a non-alarming way to contin hasDominantCondition: false, hasStructuredNextAction: false, health: snapshot({ - state: "degraded", + axes: { attention: "none", coverage: "retryable_gap", freshness: "unknown", outbox: "unknown" }, next_attempt_at: null, - axes: { coverage: "retryable_gap", freshness: "unknown", attention: "none", outbox: "unknown" }, + state: "degraded", }), supportsOwnerSync: true, }); @@ -1573,9 +1573,9 @@ test("degraded+retryable_gap without owner sync points at the collector host", ( hasDominantCondition: false, hasStructuredNextAction: false, health: snapshot({ - state: "degraded", + axes: { attention: "none", coverage: "retryable_gap", freshness: "unknown", outbox: "idle" }, next_attempt_at: null, - axes: { coverage: "retryable_gap", freshness: "unknown", attention: "none", outbox: "idle" }, + state: "degraded", }), supportsOwnerSync: false, }); @@ -1590,9 +1590,9 @@ test("degraded+retryable_gap suppresses the CTA only when an automatic attempt i hasDominantCondition: false, hasStructuredNextAction: false, health: snapshot({ - state: "degraded", + axes: { attention: "none", coverage: "retryable_gap", freshness: "unknown", outbox: "unknown" }, next_attempt_at: "2026-05-19T13:00:00Z", - axes: { coverage: "retryable_gap", freshness: "unknown", attention: "none", outbox: "unknown" }, + state: "degraded", }), supportsOwnerSync: true, }); @@ -1604,8 +1604,8 @@ test("stale freshness suggests Sync now only when owner sync is supported", () = hasDominantCondition: false, hasStructuredNextAction: false, health: snapshot({ + axes: { attention: "none", coverage: "complete", freshness: "stale", outbox: "idle" }, state: "degraded", - axes: { coverage: "complete", freshness: "stale", attention: "none", outbox: "idle" }, }), supportsOwnerSync: true, }); @@ -1615,8 +1615,8 @@ test("stale freshness suggests Sync now only when owner sync is supported", () = hasDominantCondition: false, hasStructuredNextAction: false, health: snapshot({ + axes: { attention: "none", coverage: "complete", freshness: "stale", outbox: "idle" }, state: "degraded", - axes: { coverage: "complete", freshness: "stale", attention: "none", outbox: "idle" }, }), supportsOwnerSync: false, }); @@ -1629,8 +1629,8 @@ test("a stalled outbox always routes to the host, never a remote button", () => hasDominantCondition: false, hasStructuredNextAction: false, health: snapshot({ + axes: { attention: "none", coverage: "complete", freshness: "fresh", outbox: "stalled" }, state: "degraded", - axes: { coverage: "complete", freshness: "fresh", attention: "none", outbox: "stalled" }, }), supportsOwnerSync: true, }); @@ -1644,8 +1644,8 @@ test("an otherwise-healthy but stale connection still gets a nudge", () => { hasDominantCondition: false, hasStructuredNextAction: false, health: snapshot({ + axes: { attention: "none", coverage: "complete", freshness: "stale", outbox: "idle" }, state: "healthy", - axes: { coverage: "complete", freshness: "stale", attention: "none", outbox: "idle" }, }), supportsOwnerSync: true, }); @@ -1663,8 +1663,8 @@ test("an otherwise-healthy but stale connection still gets a nudge", () => { function stalledHealth(): RefConnectionHealthSnapshot { return snapshot({ + axes: { attention: "none", coverage: "complete", freshness: "fresh", outbox: "stalled" }, state: "degraded", - axes: { coverage: "complete", freshness: "fresh", attention: "none", outbox: "stalled" }, }); } @@ -1684,7 +1684,7 @@ test("stalled-row guidance carries a count-backed scale from outbox_counts", () hasDominantCondition: false, hasStructuredNextAction: false, health: stalledHealth(), - localDeviceProgress: localDeviceProgress({ pending: 12, dead_letter: 2, stale_leases: 1, total: 15 }), + localDeviceProgress: localDeviceProgress({ dead_letter: 2, pending: 12, stale_leases: 1, total: 15 }), supportsOwnerSync: false, }); assert.ok(out); @@ -1698,7 +1698,7 @@ test("stalled-row scale omits zero categories so a counted rollup never reads as hasDominantCondition: false, hasStructuredNextAction: false, health: stalledHealth(), - localDeviceProgress: localDeviceProgress({ pending: 0, dead_letter: 3, stale_leases: 0, total: 3 }), + localDeviceProgress: localDeviceProgress({ dead_letter: 3, pending: 0, stale_leases: 0, total: 3 }), supportsOwnerSync: false, }); assert.equal(out?.scale, "3 failed uploads"); @@ -1735,7 +1735,7 @@ test("stalled-row scale is null when every stuck-work category is zero", () => { hasDominantCondition: false, hasStructuredNextAction: false, health: stalledHealth(), - localDeviceProgress: localDeviceProgress({ pending: 0, dead_letter: 0, stale_leases: 0, succeeded: 40, total: 40 }), + localDeviceProgress: localDeviceProgress({ dead_letter: 0, pending: 0, stale_leases: 0, succeeded: 40, total: 40 }), supportsOwnerSync: false, }); assert.equal(out?.scale, null); @@ -1750,12 +1750,12 @@ test("non-stalled guidance never carries a count scale, even with a populated ro hasDominantCondition: false, hasStructuredNextAction: false, health: snapshot({ + axes: { attention: "none", coverage: "complete", freshness: "stale", outbox }, // freshness=stale forces a non-null guidance (Sync now / collector) so // we can assert scale is still null on that non-stalled path. state: "degraded", - axes: { coverage: "complete", freshness: "stale", attention: "none", outbox }, }), - localDeviceProgress: localDeviceProgress({ pending: 9, dead_letter: 4, total: 13 }), + localDeviceProgress: localDeviceProgress({ dead_letter: 4, pending: 9, total: 13 }), supportsOwnerSync: false, }); assert.ok(out, `expected guidance for outbox=${outbox}`); @@ -1809,12 +1809,12 @@ test("derivePrimaryRowAction keeps Sync now for an existing browser-bound connec test("derivePrimaryRowAction disables ordinary sync during source-pressure cooldown", () => { const action = derivePrimaryRowAction({ connectorId: "chatgpt", + hasLocalDeviceProgress: false, health: snapshot({ - state: "cooling_off", - reason_code: "source_pressure", next_attempt_at: "2026-06-10T22:30:00.000Z", + reason_code: "source_pressure", + state: "cooling_off", }), - hasLocalDeviceProgress: false, }); assert.equal(action.kind, "cooldown_wait"); if (action.kind === "cooldown_wait") { @@ -1827,17 +1827,17 @@ test("derivePrimaryRowAction disables ordinary sync during source-pressure coold test("derivePrimaryRowAction disables ordinary sync when source-pressure backlog coexists with a blocked headline state", () => { const action = derivePrimaryRowAction({ connectorId: "chatgpt", + hasLocalDeviceProgress: false, health: snapshot({ - state: "blocked", - reason_code: "connector_reported_failed", - next_attempt_at: "2026-06-11T14:17:24.984Z", detail_gap_backlog: backlog({ + max_attempt_count: 10, pending: 100, pending_is_floor: true, - max_attempt_count: 10, }), + next_attempt_at: "2026-06-11T14:17:24.984Z", + reason_code: "connector_reported_failed", + state: "blocked", }), - hasLocalDeviceProgress: false, }); assert.equal(action.kind, "cooldown_wait"); if (action.kind === "cooldown_wait") { @@ -1851,8 +1851,8 @@ test("derivePrimaryRowAction disables ordinary sync when source-pressure backlog test("derivePrimaryRowAction keeps ordinary sync for non-pressure cooling-off", () => { const action = derivePrimaryRowAction({ connectorId: "github", - health: snapshot({ state: "cooling_off", reason_code: "scheduler_backoff_active" }), hasLocalDeviceProgress: false, + health: snapshot({ reason_code: "scheduler_backoff_active", state: "cooling_off" }), }); assert.equal(action.kind, "sync"); }); @@ -1907,7 +1907,7 @@ test("deriveFailureSummary returns null for null input", () => { test("deriveFailureSummary degraded → 'What's missing?' trigger, view_runs CTA", () => { const result = deriveFailureSummary( - snapshot({ state: "degraded", axes: { coverage: "gaps", freshness: "fresh", attention: "none", outbox: "idle" } }) + snapshot({ axes: { attention: "none", coverage: "gaps", freshness: "fresh", outbox: "idle" }, state: "degraded" }) ); assert.ok(result); assert.equal(result.triggerLabel, "What's missing?"); @@ -1918,8 +1918,8 @@ test("deriveFailureSummary degraded → 'What's missing?' trigger, view_runs CTA test("deriveFailureSummary degraded with complete coverage → generic prose", () => { const result = deriveFailureSummary( snapshot({ + axes: { attention: "none", coverage: "complete", freshness: "stale", outbox: "idle" }, state: "degraded", - axes: { coverage: "complete", freshness: "stale", attention: "none", outbox: "idle" }, }) ); assert.ok(result); @@ -1928,7 +1928,7 @@ test("deriveFailureSummary degraded with complete coverage → generic prose", ( }); test("deriveFailureSummary cooling_off source_pressure → honest prose about throttling", () => { - const result = deriveFailureSummary(snapshot({ state: "cooling_off", reason_code: "source_pressure" })); + const result = deriveFailureSummary(snapshot({ reason_code: "source_pressure", state: "cooling_off" })); assert.ok(result); assert.equal(result.triggerLabel, "What's wrong?"); assert.equal(result.cta, "wait"); @@ -1936,7 +1936,7 @@ test("deriveFailureSummary cooling_off source_pressure → honest prose about th }); test("deriveFailureSummary cooling_off failure back-off → retry prose", () => { - const result = deriveFailureSummary(snapshot({ state: "cooling_off", reason_code: "reddit_login_unexpected_ui" })); + const result = deriveFailureSummary(snapshot({ reason_code: "reddit_login_unexpected_ui", state: "cooling_off" })); assert.ok(result); assert.equal(result.cta, "wait"); assert.match(result.prose, PROSE_BACKOFF_RE); @@ -1957,19 +1957,19 @@ test("deriveFailureSummary needs_attention → reconnect CTA", () => { }); test("deriveFailureSummary passes through reason_code", () => { - const result = deriveFailureSummary(snapshot({ state: "blocked", reason_code: "browser_context_died" })); + const result = deriveFailureSummary(snapshot({ reason_code: "browser_context_died", state: "blocked" })); assert.ok(result); assert.equal(result.reasonCode, "browser_context_died"); }); test("deriveFailureSummary passes through next_attempt_at", () => { - const result = deriveFailureSummary(snapshot({ state: "cooling_off", next_attempt_at: "2026-05-15T15:36:00Z" })); + const result = deriveFailureSummary(snapshot({ next_attempt_at: "2026-05-15T15:36:00Z", state: "cooling_off" })); assert.ok(result); assert.equal(result.nextAttemptAt, "2026-05-15T15:36:00Z"); }); test("deriveFailureSummary passes through last_success_at", () => { - const result = deriveFailureSummary(snapshot({ state: "blocked", last_success_at: "2026-04-28T19:33:00Z" })); + const result = deriveFailureSummary(snapshot({ last_success_at: "2026-04-28T19:33:00Z", state: "blocked" })); assert.ok(result); assert.equal(result.lastSuccessAt, "2026-04-28T19:33:00Z"); }); @@ -1980,7 +1980,7 @@ test("deriveFailureSummary passes through last_success_at", () => { // and confusing. deriveFailureSummary applies the shared isSourcePressureCooldown // guard (also load-bearing for the blocked-branch fallback in this file). (spec §6.2) test("deriveFailureSummary blocked + source_pressure reason_code → cta is 'wait', NOT 'reconnect'", () => { - const result = deriveFailureSummary(snapshot({ state: "blocked", reason_code: "source_pressure" })); + const result = deriveFailureSummary(snapshot({ reason_code: "source_pressure", state: "blocked" })); assert.ok(result); assert.notEqual(result.cta, "reconnect", "source-pressure blocked must not yield reconnect CTA"); assert.equal(result.cta, "wait"); @@ -1992,10 +1992,10 @@ test("deriveFailureSummary blocked + backlog + next_attempt_at (inferred source- // deriveFailureSummary blocked branch must honour the same inference. const result = deriveFailureSummary( snapshot({ - state: "blocked", - reason_code: null, - next_attempt_at: "2026-05-20T10:00:00Z", detail_gap_backlog: backlog({ pending: 5 }), + next_attempt_at: "2026-05-20T10:00:00Z", + reason_code: null, + state: "blocked", }) ); assert.ok(result); @@ -2015,10 +2015,10 @@ test("§6.3 backlog with terminal>0 does NOT say 'caught up' — emits caveat ab hasDominantCondition: false, hasStructuredNextAction: false, health: snapshot({ - state: "cooling_off", - reason_code: "source_pressure", - next_attempt_at: null, detail_gap_backlog: backlog({ pending: 0, recovered: 10, terminal: 3 }), + next_attempt_at: null, + reason_code: "source_pressure", + state: "cooling_off", }), supportsOwnerSync: true, }); @@ -2032,10 +2032,10 @@ test("§6.3 backlog with terminal===0 still says 'caught up' (no false caveat)", hasDominantCondition: false, hasStructuredNextAction: false, health: snapshot({ - state: "cooling_off", - reason_code: "source_pressure", - next_attempt_at: null, detail_gap_backlog: backlog({ pending: 0, recovered: 10, terminal: 0 }), + next_attempt_at: null, + reason_code: "source_pressure", + state: "cooling_off", }), supportsOwnerSync: true, }); @@ -2050,7 +2050,7 @@ test("deriveStreakDots returns empty array for no runs", () => { }); test("deriveStreakDots maps succeeded → ✓ success", () => { - const dots = deriveStreakDots([{ status: "succeeded", first_at: "2026-05-15T00:00:00Z" }]); + const dots = deriveStreakDots([{ first_at: "2026-05-15T00:00:00Z", status: "succeeded" }]); assert.equal(dots.length, 1); assert.ok(dots[0]); assert.equal(dots[0].symbol, "✓"); @@ -2059,7 +2059,7 @@ test("deriveStreakDots maps succeeded → ✓ success", () => { test("deriveStreakDots maps failed → ✕ danger", () => { const dots = deriveStreakDots([ - { status: "failed", first_at: "2026-05-15T00:00:00Z", failure_reason: "browser_timeout" }, + { failure_reason: "browser_timeout", first_at: "2026-05-15T00:00:00Z", status: "failed" }, ]); assert.ok(dots[0]); assert.equal(dots[0].symbol, "✕"); @@ -2068,21 +2068,21 @@ test("deriveStreakDots maps failed → ✕ danger", () => { }); test("deriveStreakDots maps cancelled → ⊘ neutral", () => { - const dots = deriveStreakDots([{ status: "cancelled", first_at: "2026-05-15T00:00:00Z" }]); + const dots = deriveStreakDots([{ first_at: "2026-05-15T00:00:00Z", status: "cancelled" }]); assert.ok(dots[0]); assert.equal(dots[0].symbol, "⊘"); assert.equal(dots[0].tone, "neutral"); }); test("deriveStreakDots maps degraded → ⚠ warning", () => { - const dots = deriveStreakDots([{ status: "degraded", first_at: "2026-05-15T00:00:00Z" }]); + const dots = deriveStreakDots([{ first_at: "2026-05-15T00:00:00Z", status: "degraded" }]); assert.ok(dots[0]); assert.equal(dots[0].symbol, "⚠"); assert.equal(dots[0].tone, "warning"); }); test("deriveStreakDots maps succeeded_with_gaps → ⚠ warning", () => { - const dots = deriveStreakDots([{ status: "succeeded_with_gaps", first_at: "2026-05-15T00:00:00Z" }]); + const dots = deriveStreakDots([{ first_at: "2026-05-15T00:00:00Z", status: "succeeded_with_gaps" }]); assert.ok(dots[0]); assert.equal(dots[0].symbol, "⚠"); assert.equal(dots[0].tone, "warning"); @@ -2091,18 +2091,18 @@ test("deriveStreakDots maps succeeded_with_gaps → ⚠ warning", () => { test("summarizeStreakDots distinguishes clean, failed, and completed-with-gaps runs", () => { assert.equal( - summarizeStreakDots(deriveStreakDots([{ status: "succeeded", first_at: "2026-05-15T00:00:00Z" }])), + summarizeStreakDots(deriveStreakDots([{ first_at: "2026-05-15T00:00:00Z", status: "succeeded" }])), "0 failures" ); assert.equal( - summarizeStreakDots(deriveStreakDots([{ status: "succeeded_with_gaps", first_at: "2026-05-15T00:00:00Z" }])), + summarizeStreakDots(deriveStreakDots([{ first_at: "2026-05-15T00:00:00Z", status: "succeeded_with_gaps" }])), "1 with gaps" ); assert.equal( summarizeStreakDots( deriveStreakDots([ - { status: "failed", first_at: "2026-05-15T00:00:00Z" }, - { status: "succeeded_with_gaps", first_at: "2026-05-14T00:00:00Z" }, + { first_at: "2026-05-15T00:00:00Z", status: "failed" }, + { first_at: "2026-05-14T00:00:00Z", status: "succeeded_with_gaps" }, ]) ), "1 failure · 1 with gaps" @@ -2111,15 +2111,15 @@ test("summarizeStreakDots distinguishes clean, failed, and completed-with-gaps r test("deriveStreakDots caps at 14 runs", () => { const runs = Array.from({ length: 20 }, (_, i) => ({ - status: "succeeded", first_at: `2026-05-${String(i + 1).padStart(2, "0")}T00:00:00Z`, + status: "succeeded", })); assert.equal(deriveStreakDots(runs).length, 14); }); test("deriveStreakDots preserves the at timestamp for tooltip", () => { const ts = "2026-05-15T13:00:00Z"; - const dots = deriveStreakDots([{ status: "succeeded", first_at: ts }]); + const dots = deriveStreakDots([{ first_at: ts, status: "succeeded" }]); assert.ok(dots[0]); assert.equal(dots[0].at, ts); }); diff --git a/apps/console/src/app/(console)/lib/connection-evidence.ts b/apps/console/src/app/(console)/lib/connection-evidence.ts index b3b3baea0..e8f36db96 100644 --- a/apps/console/src/app/(console)/lib/connection-evidence.ts +++ b/apps/console/src/app/(console)/lib/connection-evidence.ts @@ -49,13 +49,17 @@ export interface AxisChip { const COVERAGE_LABELS: Record = { complete: { dimension: "Coverage", - value: "complete", label: "Coverage · complete", title: "All required streams have durable evidence of complete coverage.", tone: "success", + value: "complete", }, deferred: { dimension: "Coverage", + label: "Coverage · optional, not collected", + title: + "The manifest declares this coverage out of scope. This is an accepted, settled state — not a queued task — and does not block connection health.", + tone: "neutral", // The underlying axis key stays "deferred" (durable manifest/runtime // contract — see AcceptedCoveragePolicy in connector-coverage-policy.ts). // "Deferred" read as queued/pending work to owners, contradicting the @@ -63,43 +67,43 @@ const COVERAGE_LABELS: Record = { fresh: { dimension: "Freshness", - value: "fresh", label: "Freshness · fresh", title: "The last successful run is within policy.", tone: "success", + value: "fresh", }, stale: { dimension: "Freshness", - value: "stale", label: "Freshness · stale", title: "The last successful run is outside the configured freshness window.", tone: "warning", + value: "stale", }, unknown: { dimension: "Freshness", - value: "unknown", label: "Freshness · unknown", title: "Freshness cannot be derived from current evidence.", tone: "neutral", + value: "unknown", }, }; const OUTBOX_LABELS: Record = { - idle: { - dimension: "Outbox", - value: "idle", - label: "Outbox · idle", - title: "No retryable outbound work is pending.", - tone: "success", - }, active: { dimension: "Outbox", - value: "active", label: "Outbox · active", title: "Outbound work is making progress.", // `active` means the local-device outbox is draining — a healthy, @@ -184,45 +176,53 @@ const OUTBOX_LABELS: Record = { - none: null, - open: { - dimension: "Attention", - value: "open", - label: "Attention · open", - title: "Owner action is open.", - tone: "warning", - }, acknowledged: { dimension: "Attention", - value: "acknowledged", label: "Attention · acknowledged", title: "Owner action is acknowledged but not yet resolved.", tone: "warning", + value: "acknowledged", }, in_progress: { dimension: "Attention", - value: "in progress", label: "Attention · in progress", title: "Owner action is in progress.", tone: "warning", + value: "in progress", + }, + none: null, + open: { + dimension: "Attention", + label: "Attention · open", + title: "Owner action is open.", + tone: "warning", + value: "open", }, }; @@ -255,10 +255,10 @@ export function formatAttentionAxis( } return { dimension: "Attention", - value: "unknown", label: "Attention · unknown", title: `Unknown attention axis "${axis}" from the reference server.`, tone: "neutral", + value: "unknown", }; } @@ -278,8 +278,8 @@ function formatKnownAxis( return { ...fallbackChip, dimension: labelPrefix, - value: "unknown", title: `Unknown ${labelPrefix.toLowerCase()} axis "${axis}" from the reference server.`, + value: "unknown", }; } @@ -353,10 +353,10 @@ export function summarizeAxisChips( if (axes.outbox === "unknown") { out.push({ ...outbox, - value: "evidence unavailable", label: "Outbox · evidence unavailable", title: "This local collector's outbox evidence could not be read right now. It is not a current data-loss signal; retry, or open the connection's diagnostics for the device state.", + value: "evidence unavailable", }); } else { out.push(outbox); @@ -394,61 +394,61 @@ export interface ForwardDispositionSummary { } const FORWARD_DISPOSITION_LABELS: Record = { - complete: { - value: "complete", - label: "nothing owed", - ownerActionNeeded: false, + awaiting_owner: { + label: "blocked on you", + ownerActionNeeded: true, title: - "Coverage is established and fresh. A future run re-checks the source but is not expected to collect anything new or fill a gap.", - tone: "success", + "A coverage gap is blocked on open owner attention (such as re-auth or a prompt). A run cannot make progress until you act; open the connection's attention to resolve it.", + tone: "warning", + value: "awaiting_owner", }, checking: { - value: "checking", label: "checking coverage", ownerActionNeeded: false, title: "Active work is expected to produce coverage evidence. This is a checking state, not an owner-action prompt.", tone: "neutral", + value: "checking", }, - unmeasured: { - value: "unmeasured", - label: "not measured", - ownerActionNeeded: false, - title: - "Coverage evidence is not available in the latest report. This is not an owner-action prompt and not an active checking state.", - tone: "neutral", - }, - resumable: { - value: "resumable", - label: "resumes collection", + complete: { + label: "nothing owed", ownerActionNeeded: false, title: - "There is outstanding work an ordinary future run is expected to pick up — an open boundary or retryable gap. Records already collected stay valid; no owner action is needed.", - tone: "warning", - }, - awaiting_owner: { - value: "awaiting_owner", - label: "blocked on you", - ownerActionNeeded: true, - title: - "A coverage gap is blocked on open owner attention (such as re-auth or a prompt). A run cannot make progress until you act; open the connection's attention to resolve it.", - tone: "warning", + "Coverage is established and fresh. A future run re-checks the source but is not expected to collect anything new or fill a gap.", + tone: "success", + value: "complete", }, owner_refresh_due: { - value: "owner_refresh_due", label: "refresh due", ownerActionNeeded: true, title: "Coverage is complete but the retained data has gone stale, and this connection needs an owner-initiated run to refresh — either because it refreshes only when you run it, or because it refreshes on schedule but may need your bounded help to catch up. Start a run on your instance to bring it current. This is aged data, not missing data.", tone: "warning", + value: "owner_refresh_due", + }, + resumable: { + label: "resumes collection", + ownerActionNeeded: false, + title: + "There is outstanding work an ordinary future run is expected to pick up — an open boundary or retryable gap. Records already collected stay valid; no owner action is needed.", + tone: "warning", + value: "resumable", }, terminal: { - value: "terminal", label: "won't backfill", ownerActionNeeded: false, title: "An outstanding gap will not backfill on its own — the source or connector cannot recover it without a change. Records already collected stay valid and usable; open the latest run to see which streams are affected.", tone: "danger", + value: "terminal", + }, + unmeasured: { + label: "not measured", + ownerActionNeeded: false, + title: + "Coverage evidence is not available in the latest report. This is not an owner-action prompt and not an active checking state.", + tone: "neutral", + value: "unmeasured", }, }; @@ -469,11 +469,11 @@ export function formatForwardDisposition( return FORWARD_DISPOSITION_LABELS[disposition as RefForwardDisposition]; } return { - value: disposition as RefForwardDisposition, label: "unknown", ownerActionNeeded: false, title: `The reference reported a forward disposition "${disposition}" this console does not recognize.`, tone: "neutral", + value: disposition as RefForwardDisposition, }; } @@ -540,13 +540,13 @@ export function formatProjectionFreshness( ): ProjectionFreshness { const reasons = snapshot?.unknown_reasons ?? []; if (reasons.length === 0) { - return { unreliable: false, reasons: [], detail: "" }; + return { detail: "", reasons: [], unreliable: false }; } const humanized = reasons.map(humanizeReason); return { - unreliable: true, - reasons: humanized, detail: `Projection evidence missing: ${humanized.join(", ")}.`, + reasons: humanized, + unreliable: true, }; } @@ -867,31 +867,31 @@ export function formatSourceOutboxState( case "dead_letter": return { dimension: "Outbox", - value: "failed uploads", label: "Outbox · failed uploads", title: counts, tone: "danger", + value: "failed uploads", }; case "stale": return { dimension: "Outbox", - value: "stale lease", label: "Outbox · stale lease", title: counts, tone: "danger", + value: "stale lease", }; case "retrying": - return { dimension: "Outbox", value: "retrying", label: "Outbox · retrying", title: counts, tone: "warning" }; + return { dimension: "Outbox", label: "Outbox · retrying", title: counts, tone: "warning", value: "retrying" }; case "pending": - return { dimension: "Outbox", value: "pending", label: "Outbox · pending", title: counts, tone: "neutral" }; + return { dimension: "Outbox", label: "Outbox · pending", title: counts, tone: "neutral", value: "pending" }; case "backlog": - return { dimension: "Outbox", value: "backlog", label: "Outbox · backlog", title: counts, tone: "warning" }; + return { dimension: "Outbox", label: "Outbox · backlog", title: counts, tone: "warning", value: "backlog" }; case "drained": - return { dimension: "Outbox", value: "drained", label: "Outbox · drained", title: counts, tone: "success" }; + return { dimension: "Outbox", label: "Outbox · drained", title: counts, tone: "success", value: "drained" }; case "unknown": - return { dimension: "Outbox", value: "unknown", label: "Outbox · unknown", title: counts, tone: "neutral" }; + return { dimension: "Outbox", label: "Outbox · unknown", title: counts, tone: "neutral", value: "unknown" }; default: - return { dimension: "Outbox", value: "unknown", label: "Outbox · unknown", title: counts, tone: "neutral" }; + return { dimension: "Outbox", label: "Outbox · unknown", title: counts, tone: "neutral", value: "unknown" }; } } @@ -927,11 +927,11 @@ export function summarizeSchedule(schedule: RefSchedule | null | undefined): Sch } } return { - enabled: schedule.enabled, - nextAttemptLabel: schedule.next_due_at ? `Next attempt ${schedule.next_due_at}` : null, backoffLabel, + enabled: schedule.enabled, ineligibilityReason: schedule.ineligibility_reason, mode: schedule.effective_mode, + nextAttemptLabel: schedule.next_due_at ? `Next attempt ${schedule.next_due_at}` : null, }; } @@ -1277,17 +1277,17 @@ function staleFreshnessGuidance(supportsOwnerSync: boolean): NextStepGuidance { if (supportsOwnerSync) { return { backlogScale: null, - label: "Sync now", detail: "The last successful sync is outside the freshness window. Sync now to refresh this connection.", + label: "Sync now", scale: null, tone: "warning", }; } return { backlogScale: null, - label: "Check the collector", detail: "The last successful sync is outside the freshness window. This connection fills in when its local-collector device pushes — confirm the collector is running on the host.", + label: "Check the collector", scale: null, tone: "warning", }; @@ -1318,20 +1318,20 @@ function coolingOffGuidance(health: RefConnectionHealthSnapshot): NextStepGuidan const backlogScale = formatSourcePressureBacklogScale(health.detail_gap_backlog); return { backlogScale, - label: "Catching up — cooling off", detail: health.next_attempt_at ? `The source is throttling this connection, so the scheduler is spacing out automatic attempts; the captured progress is retained and it resumes at ${health.next_attempt_at}.` : "The source is throttling this connection, so the scheduler is spacing out automatic attempts. The captured progress is retained and it resumes on the next scheduled attempt.", + label: "Catching up — cooling off", scale: null, tone: "warning", }; } return { backlogScale: null, - label: "Wait for the next retry", detail: health.next_attempt_at ? `In scheduler backoff after recent failures; the next automatic attempt is at ${health.next_attempt_at}. Open the connection to see the failure detail.` : "In scheduler backoff after recent failures. Open the connection to see the failure detail and the next attempt time.", + label: "Wait for the next retry", scale: null, tone: "warning", }; @@ -1351,18 +1351,18 @@ function degradedGuidance(health: RefConnectionHealthSnapshot, supportsOwnerSync if (supportsOwnerSync) { return { backlogScale, - label: "Continue the sync", detail: "Some required detail is still outstanding. The records already collected stay valid; sync this connection when you're ready and an ordinary run fills the rest.", + label: "Continue the sync", scale: null, tone: "warning", }; } return { backlogScale, - label: "Check the collector", detail: "Some required detail is still outstanding. The records already collected stay valid; this connection fills in when its local-collector device pushes the rest — confirm the collector is running on the host.", + label: "Check the collector", scale: null, tone: "warning", }; @@ -1370,9 +1370,9 @@ function degradedGuidance(health: RefConnectionHealthSnapshot, supportsOwnerSync if (health.axes.coverage === "gaps" || health.axes.coverage === "partial") { return { backlogScale: null, - label: "Review partial coverage", detail: "Useful data exists, but some required streams have gaps. Open the connection's latest run to see which streams are incomplete.", + label: "Review partial coverage", scale: null, tone: "warning", }; @@ -1382,8 +1382,8 @@ function degradedGuidance(health: RefConnectionHealthSnapshot, supportsOwnerSync } return { backlogScale: null, - label: "Open the connection", detail: "Coverage or freshness is incomplete. Open the connection to see which axis is degraded.", + label: "Open the connection", scale: null, tone: "warning", }; @@ -1427,9 +1427,9 @@ export function deriveConnectionNextStep(input: { if (health.axes.outbox === "stalled") { return { backlogScale: null, - label: "Check the collector host", detail: "Retryable work on the local collector is not draining. Open the connection for the exact command to run on the host that holds the data.", + label: "Check the collector host", scale: formatOutboxCountScale(localDeviceProgress?.outbox_counts), tone: "danger", }; @@ -1443,8 +1443,8 @@ export function deriveConnectionNextStep(input: { ? null : { backlogScale: null, - label: "Open the connection", detail: "This connection cannot make progress. Open it to read the blocking condition and how to clear it.", + label: "Open the connection", scale: null, tone: "danger", }; @@ -1455,8 +1455,8 @@ export function deriveConnectionNextStep(input: { ? null : { backlogScale: null, - label: "Open the connection", detail: "Owner action is required. Open the connection to see exactly what's needed.", + label: "Open the connection", scale: null, tone: "warning", }; @@ -1758,26 +1758,26 @@ export function deriveStreakDots( return runs.slice(0, 14).map((r): StreakDot => { const s = r.status; if (s === "succeeded_with_gaps") { - return { symbol: "⚠", tone: "warning", at: r.first_at, statusLabel: "Succeeded with gaps" }; + return { at: r.first_at, statusLabel: "Succeeded with gaps", symbol: "⚠", tone: "warning" }; } if (s === "succeeded" || s === "success" || s === "completed") { - return { symbol: "✓", tone: "success", at: r.first_at, statusLabel: "Succeeded" }; + return { at: r.first_at, statusLabel: "Succeeded", symbol: "✓", tone: "success" }; } if (s === "failed" || s === "error") { - return { symbol: "✕", tone: "danger", at: r.first_at, statusLabel: r.failure_reason ?? "Failed" }; + return { at: r.first_at, statusLabel: r.failure_reason ?? "Failed", symbol: "✕", tone: "danger" }; } if (s === "cancelled" || s === "canceled") { - return { symbol: "⊘", tone: "neutral", at: r.first_at, statusLabel: "Cancelled" }; + return { at: r.first_at, statusLabel: "Cancelled", symbol: "⊘", tone: "neutral" }; } if (s === "paused" || s === "skipped") { - return { symbol: "⏸", tone: "neutral", at: r.first_at, statusLabel: "Skipped" }; + return { at: r.first_at, statusLabel: "Skipped", symbol: "⏸", tone: "neutral" }; } if (s === "degraded" || s === "partial") { - return { symbol: "⚠", tone: "warning", at: r.first_at, statusLabel: "Partial" }; + return { at: r.first_at, statusLabel: "Partial", symbol: "⚠", tone: "warning" }; } // in_progress / started — not shown in a historical strip but included // defensively so the map never throws. - return { symbol: "⚠", tone: "neutral", at: r.first_at, statusLabel: s.replace(/_/g, " ") }; + return { at: r.first_at, statusLabel: s.replace(/_/g, " "), symbol: "⚠", tone: "neutral" }; }); } @@ -1838,9 +1838,9 @@ export function deriveAutoPausedBanner( } return { consecutiveFailures: backoff.consecutive_failures, - reasonLabel: backoff.reason_class ? backoff.reason_class.replace(/[_-]+/g, " ") : null, - nextRunAt: backoff.next_run_at, isTerminal: backoff.recommended_health_state === "blocked", + nextRunAt: backoff.next_run_at, + reasonLabel: backoff.reason_class ? backoff.reason_class.replace(/[_-]+/g, " ") : null, }; } @@ -1853,20 +1853,20 @@ export function derivePrimaryRowAction(input: { const { hasLocalDeviceProgress, health } = input; if (hasLocalDeviceProgress) { return { - kind: "device_wait", - label: "Waiting for the local device", detail: "This connection fills in when its local-collector device pushes new data — the dashboard cannot start a run. Confirm the collector is running on the host that holds the data.", + kind: "device_wait", + label: "Waiting for the local device", }; } if (health?.state === "cooling_off" && health.reason_code === SOURCE_PRESSURE_REASON_CODE) { return { - kind: "cooldown_wait", - label: "Cooling off", detail: health.next_attempt_at ? `This source is throttling PDPP, so the next ordinary sync waits until ${health.next_attempt_at}. Captured progress is retained.` : "This source is throttling PDPP, so ordinary sync is paused until the next scheduled retry. Captured progress is retained.", + kind: "cooldown_wait", + label: "Cooling off", }; } @@ -1874,11 +1874,11 @@ export function derivePrimaryRowAction(input: { if (sourcePressureBacklog && sourcePressureBacklog.pending > 0) { const pending = `${sourcePressureBacklog.pending_is_floor ? "at least " : ""}${sourcePressureBacklog.pending.toLocaleString()}`; return { - kind: "cooldown_wait", - label: "Cooling off", detail: `This connection has ${pending} pending provider-pressure gap${ sourcePressureBacklog.pending === 1 ? "" : "s" }, so ordinary sync may be rejected by the provider-pressure cooldown. Captured progress is retained.`, + kind: "cooldown_wait", + label: "Cooling off", }; } @@ -1911,7 +1911,7 @@ export function formatCollectionRateReadout( : null; return { backoffLabel, - currentLabel: `${rate.effective_rate_per_min.toLocaleString()}/min · interval ${rate.current_interval_ms.toLocaleString()}ms`, ceilingLabel: `ceiling ${rate.ceiling_rate_per_min.toLocaleString()}/min · interval ${rate.ceiling_interval_ms.toLocaleString()}ms`, + currentLabel: `${rate.effective_rate_per_min.toLocaleString()}/min · interval ${rate.current_interval_ms.toLocaleString()}ms`, }; } diff --git a/apps/console/src/app/(console)/lib/connection-label-ambiguity.test.ts b/apps/console/src/app/(console)/lib/connection-label-ambiguity.test.ts index bb6901c2c..056eca4e6 100644 --- a/apps/console/src/app/(console)/lib/connection-label-ambiguity.test.ts +++ b/apps/console/src/app/(console)/lib/connection-label-ambiguity.test.ts @@ -49,10 +49,10 @@ function overview({ test("a single unnamed connection of a type is NOT label-needed", () => { const overviews = [ - overview({ connectorId: "amazon", displayName: "Amazon", connectionId: "cin_amazon" }), - overview({ connectorId: "usaa", connectionId: "cin_usaa" }), - overview({ connectorId: "github", displayName: "GitHub", connectionId: "cin_github" }), - overview({ connectorId: "ynab", displayName: "YNAB", connectionId: "cin_ynab" }), + overview({ connectionId: "cin_amazon", connectorId: "amazon", displayName: "Amazon" }), + overview({ connectionId: "cin_usaa", connectorId: "usaa" }), + overview({ connectionId: "cin_github", connectorId: "github", displayName: "GitHub" }), + overview({ connectionId: "cin_ynab", connectorId: "ynab", displayName: "YNAB" }), ]; const keys = ambiguousFallbackLabelKeys(overviews); assert.equal(keys.size, 0, "no single-of-type fallback should be flagged"); @@ -62,10 +62,10 @@ test("a single unnamed connection of a type is NOT label-needed", () => { }); test("two unnamed connections of the SAME type are both label-needed", () => { - const loneAmazon = overview({ connectorId: "amazon", displayName: "Amazon", connectionId: "cin_amazon" }); + const loneAmazon = overview({ connectionId: "cin_amazon", connectorId: "amazon", displayName: "Amazon" }); const overviews = [ - overview({ connectorId: "gmail", displayName: "Gmail", connectionId: "cin_gmail_a" }), - overview({ connectorId: "gmail", connectionId: "cin_gmail_b" }), + overview({ connectionId: "cin_gmail_a", connectorId: "gmail", displayName: "Gmail" }), + overview({ connectionId: "cin_gmail_b", connectorId: "gmail" }), loneAmazon, ]; const keys = ambiguousFallbackLabelKeys(overviews); @@ -78,8 +78,8 @@ test("a renamed connection is never label-needed, and disambiguates its siblings // One Gmail owner-named, one unnamed → the named one is not a fallback at // all, so only ONE unnamed Gmail remains. A single unnamed of a type is not // ambiguous, so nothing is flagged. - const namedGmail = overview({ connectorId: "gmail", displayName: "Personal Gmail", connectionId: "cin_gmail_a" }); - const unnamedGmail = overview({ connectorId: "gmail", connectionId: "cin_gmail_b" }); + const namedGmail = overview({ connectionId: "cin_gmail_a", connectorId: "gmail", displayName: "Personal Gmail" }); + const unnamedGmail = overview({ connectionId: "cin_gmail_b", connectorId: "gmail" }); const overviews = [namedGmail, unnamedGmail]; const keys = ambiguousFallbackLabelKeys(overviews); assert.equal(keys.size, 0); @@ -89,17 +89,17 @@ test("a renamed connection is never label-needed, and disambiguates its siblings test("three unnamed of a type are all label-needed", () => { const overviews = [ - overview({ connectorId: "claude-code", connectionId: "cin_cc_1" }), - overview({ connectorId: "claude-code", connectionId: "cin_cc_2" }), - overview({ connectorId: "claude-code", connectionId: "cin_cc_3" }), + overview({ connectionId: "cin_cc_1", connectorId: "claude-code" }), + overview({ connectionId: "cin_cc_2", connectorId: "claude-code" }), + overview({ connectionId: "cin_cc_3", connectorId: "claude-code" }), ]; const keys = ambiguousFallbackLabelKeys(overviews); assert.equal(keys.size, 3); }); test("ambiguity is order-independent", () => { - const a = overview({ connectorId: "slack", connectionId: "cin_s1" }); - const b = overview({ connectorId: "slack", connectionId: "cin_s2" }); + const a = overview({ connectionId: "cin_s1", connectorId: "slack" }); + const b = overview({ connectionId: "cin_s2", connectorId: "slack" }); const forward = ambiguousFallbackLabelKeys([a, b]); const reverse = ambiguousFallbackLabelKeys([b, a]); assert.deepEqual(new Set(forward), new Set(reverse)); diff --git a/apps/console/src/app/(console)/lib/connection-summary-stats.test.ts b/apps/console/src/app/(console)/lib/connection-summary-stats.test.ts index 83e203025..796f070c9 100644 --- a/apps/console/src/app/(console)/lib/connection-summary-stats.test.ts +++ b/apps/console/src/app/(console)/lib/connection-summary-stats.test.ts @@ -12,12 +12,12 @@ type ConnectionHealthState = NonNullable[ function overview(partial: Partial): ConnectorOverview { return { - connector: { connector_id: "test", name: "Test", display_name: "Test" }, - streams: [], - totalRecords: 0, + connector: { connector_id: "test", display_name: "Test", name: "Test" }, isRunning: false, lastRun: null, lastSuccessfulRun: null, + streams: [], + totalRecords: 0, ...partial, }; } @@ -32,15 +32,15 @@ function withDataInState( outbox: "active" | "idle" | "stalled" | "unknown" = "unknown" ): ConnectorOverview { return overview({ - totalRecords: 10, connectionHealth: { - state, - reason_code: null, + axes: { attention: "none", coverage: "unknown", freshness, outbox, remote_surface: "none" }, + badges: { stale: freshness === "stale", syncing: false }, last_success_at: null, next_attempt_at: null, - axes: { freshness, coverage: "unknown", attention: "none", outbox, remote_surface: "none" }, - badges: { stale: freshness === "stale", syncing: false }, + reason_code: null, + state, } as ConnectorOverview["connectionHealth"], + totalRecords: 10, }); } @@ -104,17 +104,17 @@ test("stale is counted only when the freshness axis says stale", () => { test("running counts active runs and push-mode syncing badges", () => { const syncing = overview({ - totalRecords: 3, connectionHealth: { - state: "healthy", - reason_code: null, + axes: { attention: "none", coverage: "complete", freshness: "fresh", outbox: "active", remote_surface: "none" }, + badges: { stale: false, syncing: true }, last_success_at: null, next_attempt_at: null, - axes: { freshness: "fresh", coverage: "complete", attention: "none", outbox: "active", remote_surface: "none" }, - badges: { stale: false, syncing: true }, + reason_code: null, + state: "healthy", } as ConnectorOverview["connectionHealth"], + totalRecords: 3, }); - const stats = summarizeConnectionHealth([overview({ totalRecords: 1, isRunning: true }), syncing]); + const stats = summarizeConnectionHealth([overview({ isRunning: true, totalRecords: 1 }), syncing]); assert.equal(stats.running, 2); }); diff --git a/apps/console/src/app/(console)/lib/connection-summary-stats.ts b/apps/console/src/app/(console)/lib/connection-summary-stats.ts index 59f3bdd8f..9e1640656 100644 --- a/apps/console/src/app/(console)/lib/connection-summary-stats.ts +++ b/apps/console/src/app/(console)/lib/connection-summary-stats.ts @@ -118,11 +118,11 @@ export function summarizeConnectionHealth(overviews: readonly ConnectorOverview[ } } return { + degraded, + needsAttention, + noData: overviews.length - primaryList.length, primaryList: primaryList.length, registeredTotal: overviews.length, - noData: overviews.length - primaryList.length, - needsAttention, - degraded, running, stale, }; diff --git a/apps/console/src/app/(console)/lib/data-source.ts b/apps/console/src/app/(console)/lib/data-source.ts index 5d1423aa0..4cfef6efb 100644 --- a/apps/console/src/app/(console)/lib/data-source.ts +++ b/apps/console/src/app/(console)/lib/data-source.ts @@ -193,31 +193,31 @@ export interface DashboardDataSource { * weakening this binding would weaken `/**` owner gating. */ export const liveDashboardDataSource: DashboardDataSource = { - kind: "live", - supportsExploreTimelineDirection: async () => process.env.PDPP_EXPLORE_TIMELINE_DIRECTION === "1", - listConnectorSummaries, - listConnectorManifests, - listStreams, - getStreamMetadata, - getConnectorOverview, aggregateRecordsByTime, - listExploreRecordBuckets, - queryRecords, + getConnectorOverview, + getDatasetSummary, + getDeploymentDiagnostics, + getGrantTimeline, getRecord, - refSearch, - searchRecordsLexical, - searchRecordsSemantic, - searchRecordsHybrid, - isSemanticRetrievalAdvertised, + getRunTimeline, + getStreamMetadata, + getTraceTimeline, isHybridRetrievalAdvertised, + isSemanticRetrievalAdvertised, + kind: "live", + listConnectorManifests, + listConnectorSummaries, + listExploreRecordBuckets, listExploreTimeline, listGrants, + listPendingApprovals, listRuns, + listStreams, listTraces, - getGrantTimeline, - getRunTimeline, - getTraceTimeline, - getDatasetSummary, - listPendingApprovals, - getDeploymentDiagnostics, + queryRecords, + refSearch, + searchRecordsHybrid, + searchRecordsLexical, + searchRecordsSemantic, + supportsExploreTimelineDirection: async () => process.env.PDPP_EXPLORE_TIMELINE_DIRECTION === "1", }; diff --git a/apps/console/src/app/(console)/lib/describe-error.test.ts b/apps/console/src/app/(console)/lib/describe-error.test.ts index d3097ae42..63122dfaf 100644 --- a/apps/console/src/app/(console)/lib/describe-error.test.ts +++ b/apps/console/src/app/(console)/lib/describe-error.test.ts @@ -10,10 +10,10 @@ const FALLBACK = "request failed (409)"; test("describeError prefers the reference-server envelope error.message", () => { const body = { error: { - type: "about:blank", code: "subscription_already_disabled", message: "Subscription sub_abc is already disabled.", request_id: "req_1", + type: "about:blank", }, }; assert.equal(describeError(body, FALLBACK), "Subscription sub_abc is already disabled."); @@ -43,7 +43,7 @@ test("describeErrorText parses a JSON envelope string into the friendly message" // reference server's error envelope. The operator must see the message, // not the stringified blob. const raw = JSON.stringify({ - error: { type: "about:blank", code: "not_found", message: "Subscription not found." }, + error: { code: "not_found", message: "Subscription not found.", type: "about:blank" }, }); assert.equal( describeErrorText(raw, "_ref /_ref/event-subscriptions/sub_x/disable failed (404)"), diff --git a/apps/console/src/app/(console)/lib/grant-request-connection-pin.ts b/apps/console/src/app/(console)/lib/grant-request-connection-pin.ts index cce0e1e12..54f474843 100644 --- a/apps/console/src/app/(console)/lib/grant-request-connection-pin.ts +++ b/apps/console/src/app/(console)/lib/grant-request-connection-pin.ts @@ -117,7 +117,7 @@ export function buildConnectionPinOptions( connectorId, displayName: owned ? summary.display_name : null, }); - return { value: trim(summary.connection_id), base }; + return { base, value: trim(summary.connection_id) }; }); const labelCounts = new Map(); @@ -128,10 +128,10 @@ export function buildConnectionPinOptions( const seen = new Map(); return matching.map((entry) => { if ((labelCounts.get(entry.base) ?? 0) <= 1) { - return { value: entry.value, label: entry.base }; + return { label: entry.base, value: entry.value }; } const ordinal = (seen.get(entry.base) ?? 0) + 1; seen.set(entry.base, ordinal); - return { value: entry.value, label: `${entry.base} · account ${ordinal}` }; + return { label: `${entry.base} · account ${ordinal}`, value: entry.value }; }); } diff --git a/apps/console/src/app/(console)/lib/hybrid-preference.test.ts b/apps/console/src/app/(console)/lib/hybrid-preference.test.ts index 01e12a7b8..5a0cf736e 100644 --- a/apps/console/src/app/(console)/lib/hybrid-preference.test.ts +++ b/apps/console/src/app/(console)/lib/hybrid-preference.test.ts @@ -36,19 +36,19 @@ function shouldAttemptHybrid(input: HybridPreferenceInput): boolean { // ─── Tests ─────────────────────────────────────────────────────────────────── test("hybrid is preferred when advertised on first page", () => { - assert.equal(shouldAttemptHybrid({ hybridAdvertised: true, cursor: null }), true); + assert.equal(shouldAttemptHybrid({ cursor: null, hybridAdvertised: true }), true); }); test("hybrid is skipped when not advertised", () => { - assert.equal(shouldAttemptHybrid({ hybridAdvertised: false, cursor: null }), false); + assert.equal(shouldAttemptHybrid({ cursor: null, hybridAdvertised: false }), false); }); test("hybrid is skipped on subsequent pages even when advertised", () => { - assert.equal(shouldAttemptHybrid({ hybridAdvertised: true, cursor: "some-cursor" }), false); + assert.equal(shouldAttemptHybrid({ cursor: "some-cursor", hybridAdvertised: true }), false); }); test("hybrid is skipped on subsequent pages when not advertised", () => { - assert.equal(shouldAttemptHybrid({ hybridAdvertised: false, cursor: "some-cursor" }), false); + assert.equal(shouldAttemptHybrid({ cursor: "some-cursor", hybridAdvertised: false }), false); }); // ─── Decision gate: semantic uplift fallback ────────────────────────────────── @@ -77,7 +77,7 @@ test("semantic uplift is blocked when both advertised is false and participation test("when hybrid is advertised, semantic gate result does not matter for hybrid path", () => { // If hybrid is available, semantic should not be attempted regardless of gate. // This is enforced by searchRecords() returning early when hybrid succeeds. - const hybridWouldRun = shouldAttemptHybrid({ hybridAdvertised: true, cursor: null }); + const hybridWouldRun = shouldAttemptHybrid({ cursor: null, hybridAdvertised: true }); const semanticWouldRun = shouldAttemptSemanticUplift({ advertised: true, participationFieldCount: 3 }); // Both gates are open, but the code path that reaches semantic uplift is // only entered when hybrid is NOT advertised or NOT on page 1. The test @@ -87,7 +87,7 @@ test("when hybrid is advertised, semantic gate result does not matter for hybrid }); test("when hybrid is not advertised, semantic uplift can activate independently", () => { - const hybridWouldRun = shouldAttemptHybrid({ hybridAdvertised: false, cursor: null }); + const hybridWouldRun = shouldAttemptHybrid({ cursor: null, hybridAdvertised: false }); const semanticWouldRun = shouldAttemptSemanticUplift({ advertised: true, participationFieldCount: 3 }); assert.equal(hybridWouldRun, false); assert.equal(semanticWouldRun, true); diff --git a/apps/console/src/app/(console)/lib/next-action.ts b/apps/console/src/app/(console)/lib/next-action.ts index 7854a65da..0ee469962 100644 --- a/apps/console/src/app/(console)/lib/next-action.ts +++ b/apps/console/src/app/(console)/lib/next-action.ts @@ -85,9 +85,9 @@ export function humanizeReasonCode(code: string | null | undefined): string | nu } const OWNER_ACTION_VERB: Record, string> = { - provide_value: "Provide input", act_elsewhere: "Continue on the provider", operate_attachment: "Open the linked tool", + provide_value: "Provide input", }; const REASON_LABELS: Record = { @@ -133,11 +133,11 @@ export function formatNextAction(action: RefNextAction | null | undefined): Form action.source === "schedule_fallback" ? "Details unavailable — open the connection to see what's needed." : null; return { - label, - caveat, actionTarget: action.action_target ?? null, - variant: action.source === "schedule_fallback" ? "schedule_fallback" : "structured", + caveat, + label, notificationHint: formatNotificationHint(action.notification_state ?? null), + variant: action.source === "schedule_fallback" ? "schedule_fallback" : "structured", }; } diff --git a/apps/console/src/app/(console)/lib/operator-approvals.ts b/apps/console/src/app/(console)/lib/operator-approvals.ts index 1de18564f..c22372157 100644 --- a/apps/console/src/app/(console)/lib/operator-approvals.ts +++ b/apps/console/src/app/(console)/lib/operator-approvals.ts @@ -48,12 +48,12 @@ async function fetchAs(path: string, init: RequestInit): Promise { */ export async function approveConsentRequest(requestUri: string, subjectId = "owner_local") { const response = await fetchAs("/consent/approve", { - method: "POST", - headers: { "Content-Type": "application/json" }, body: JSON.stringify({ request_uri: requestUri, subject_id: subjectId, }), + headers: { "Content-Type": "application/json" }, + method: "POST", }); const body = await readBody(response); if (!response.ok) { @@ -64,11 +64,11 @@ export async function approveConsentRequest(requestUri: string, subjectId = "own export async function denyConsentRequest(requestUri: string) { const response = await fetchAs("/consent/deny", { - method: "POST", - headers: { "Content-Type": "application/json" }, body: JSON.stringify({ request_uri: requestUri, }), + headers: { "Content-Type": "application/json" }, + method: "POST", }); const body = await readBody(response); if (!response.ok) { @@ -90,12 +90,12 @@ export async function approvePendingApproval(input: { if (input.kind === "consent") { const response = await fetchAs("/consent/approve", { - method: "POST", - headers: { "Content-Type": "application/json" }, body: JSON.stringify({ approval_id: input.approvalId, subject_id: subjectId, }), + headers: { "Content-Type": "application/json" }, + method: "POST", }); const body = await readBody(response); if (!response.ok) { @@ -105,12 +105,12 @@ export async function approvePendingApproval(input: { } const response = await fetchAs("/device/approve", { - method: "POST", - headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: asForm({ approval_id: input.approvalId, subject_id: subjectId, }), + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + method: "POST", }); const body = await readBody(response); if (!response.ok) { @@ -132,11 +132,11 @@ export async function denyPendingApproval(input: { if (input.kind === "consent") { const response = await fetchAs("/consent/deny", { - method: "POST", - headers: { "Content-Type": "application/json" }, body: JSON.stringify({ approval_id: input.approvalId, }), + headers: { "Content-Type": "application/json" }, + method: "POST", }); const body = await readBody(response); if (!response.ok) { @@ -146,12 +146,12 @@ export async function denyPendingApproval(input: { } const response = await fetchAs("/device/deny", { - method: "POST", - headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: asForm({ approval_id: input.approvalId, subject_id: subjectId, }), + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + method: "POST", }); const body = await readBody(response); if (!response.ok) { diff --git a/apps/console/src/app/(console)/lib/operator-bootstrap.ts b/apps/console/src/app/(console)/lib/operator-bootstrap.ts index cb787db06..133b5afd4 100644 --- a/apps/console/src/app/(console)/lib/operator-bootstrap.ts +++ b/apps/console/src/app/(console)/lib/operator-bootstrap.ts @@ -131,15 +131,15 @@ export async function startOwnerBootstrapFlow( throw new Error("Token name is required"); } const registerResp = await fetchAs("/oauth/register", { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${DEFAULT_DCR_INITIAL_ACCESS_TOKEN}`, - }, body: JSON.stringify({ client_name: label, token_endpoint_auth_method: "none", }), + headers: { + Authorization: `Bearer ${DEFAULT_DCR_INITIAL_ACCESS_TOKEN}`, + "Content-Type": "application/json", + }, + method: "POST", }); const registerBody = await readBody(registerResp); if (!(registerResp.ok && registerBody) || typeof registerBody !== "object") { @@ -154,9 +154,9 @@ export async function startOwnerBootstrapFlow( // JSON content-type uses the documented CSRF exemption, like every other // server-to-server BFF call from the dashboard. const response = await fetchAs("/oauth/device_authorization", { - method: "POST", - headers: { "Content-Type": "application/json" }, body: JSON.stringify({ client_id: clientId }), + headers: { "Content-Type": "application/json" }, + method: "POST", }); const body = await readBody(response); if (!(response.ok && body) || typeof body !== "object") { @@ -181,26 +181,26 @@ export async function startOwnerBootstrapFlow( } const flow: OwnerBootstrapFlow = { - flowId: crypto.randomUUID(), + approvalUpdatedAt: null, clientId, - name: label, - subjectId: null, - status: "pending_approval", - startedAt: new Date().toISOString(), + deviceCode: payload.device_code, expiresAt: typeof payload.expires_in === "number" ? new Date(Date.now() + payload.expires_in * 1000).toISOString() : null, + flowId: crypto.randomUUID(), intervalSeconds: typeof payload.interval === "number" ? payload.interval : 5, - deviceCode: payload.device_code, + introspectedAt: null, + introspection: null, + lastError: null, + name: label, + startedAt: new Date().toISOString(), + status: "pending_approval", + subjectId: null, + token: null, + tokenIssuedAt: null, + tokenResponse: null, userCode: payload.user_code, verificationUri: payload.verification_uri ?? null, verificationUriComplete: payload.verification_uri_complete ?? null, - approvalUpdatedAt: null, - tokenIssuedAt: null, - token: null, - tokenResponse: null, - introspection: null, - introspectedAt: null, - lastError: null, }; return saveFlow(flow); @@ -215,9 +215,9 @@ export async function approveOwnerBootstrapFlow(flowId: string, subjectId: strin // derives the approved subject from the owner session when owner-auth is on; // `subjectId` here is retained only for the local UI transcript state. const response = await fetchAs("/device/approve", { - method: "POST", - headers: { "Content-Type": "application/json" }, body: JSON.stringify({ user_code: flow.userCode }), + headers: { "Content-Type": "application/json" }, + method: "POST", }); const body = await readBody(response); if (!response.ok) { @@ -225,10 +225,10 @@ export async function approveOwnerBootstrapFlow(flowId: string, subjectId: strin } return saveFlow({ ...flow, - subjectId, - status: "approved", approvalUpdatedAt: new Date().toISOString(), lastError: null, + status: "approved", + subjectId, }); } @@ -237,9 +237,9 @@ export async function denyOwnerBootstrapFlow(flowId: string, subjectId: string): // See approveOwnerBootstrapFlow: JSON content-type uses the documented CSRF // exemption for server-to-server BFF callers. const response = await fetchAs("/device/deny", { - method: "POST", - headers: { "Content-Type": "application/json" }, body: JSON.stringify({ user_code: flow.userCode }), + headers: { "Content-Type": "application/json" }, + method: "POST", }); const body = await readBody(response); if (!response.ok) { @@ -247,27 +247,27 @@ export async function denyOwnerBootstrapFlow(flowId: string, subjectId: string): } return saveFlow({ ...flow, - subjectId, - status: "denied", approvalUpdatedAt: new Date().toISOString(), - token: null, - tokenResponse: null, - introspection: null, introspectedAt: null, + introspection: null, lastError: null, + status: "denied", + subjectId, + token: null, + tokenResponse: null, }); } export async function exchangeOwnerBootstrapToken(flowId: string): Promise { const flow = requireFlow(flowId); const response = await fetchAs("/oauth/token", { - method: "POST", - headers: { "Content-Type": "application/json" }, body: JSON.stringify({ - grant_type: "urn:ietf:params:oauth:grant-type:device_code", - device_code: flow.deviceCode, client_id: flow.clientId, + device_code: flow.deviceCode, + grant_type: "urn:ietf:params:oauth:grant-type:device_code", }), + headers: { "Content-Type": "application/json" }, + method: "POST", }); const body = await readBody(response); if (!(response.ok && body) || typeof body !== "object") { @@ -281,11 +281,11 @@ export async function exchangeOwnerBootstrapToken(flowId: string): Promise, introspectedAt: new Date().toISOString(), + introspection: body as Record, lastError: null, }); } @@ -316,21 +316,16 @@ export async function buildOwnerBootstrapExamples(flow: OwnerBootstrapFlow) { const asUrl = referenceOrigin; const rsUrl = referenceOrigin; return { - cliLogin: `pdpp auth login --client-id ${shellQuote(flow.clientId)} --as-url ${shellQuote(asUrl)} --format json`, + approveCurl: `curl -sS -X POST ${shellQuote(`${asUrl}/device/approve`)} \\\n -H 'Content-Type: application/json' \\\n -H 'Cookie: pdpp_owner_session=' \\\n --data ${shellQuote(JSON.stringify({ user_code: flow.userCode }))}`, cliIntrospect: flow.token ? `pdpp auth introspect --as-url ${shellQuote(asUrl)} --token ${shellQuote(flow.token)} --format json` : "pdpp auth introspect --as-url --token --format json", - // Curl examples mirror what the BFF actually sends on the wire: - // application/json bodies with the owner-session cookie, using the - // documented isJsonRequest CSRF exemption. /device/approve does not - // accept subject_id from the body — the AS derives it from the session. - startCurl: `curl -sS -X POST ${shellQuote(`${asUrl}/oauth/device_authorization`)} \\\n -H 'Content-Type: application/json' \\\n --data ${shellQuote(JSON.stringify({ client_id: flow.clientId }))}`, - approveCurl: `curl -sS -X POST ${shellQuote(`${asUrl}/device/approve`)} \\\n -H 'Content-Type: application/json' \\\n -H 'Cookie: pdpp_owner_session=' \\\n --data ${shellQuote(JSON.stringify({ user_code: flow.userCode }))}`, + cliLogin: `pdpp auth login --client-id ${shellQuote(flow.clientId)} --as-url ${shellQuote(asUrl)} --format json`, exchangeCurl: `curl -sS -X POST ${shellQuote(`${asUrl}/oauth/token`)} \\\n -H 'Content-Type: application/json' \\\n --data ${shellQuote( JSON.stringify({ - grant_type: "urn:ietf:params:oauth:grant-type:device_code", - device_code: flow.deviceCode, client_id: flow.clientId, + device_code: flow.deviceCode, + grant_type: "urn:ietf:params:oauth:grant-type:device_code", }) )}`, introspectCurl: flow.token @@ -339,6 +334,11 @@ export async function buildOwnerBootstrapExamples(flow: OwnerBootstrapFlow) { ownerReadExample: flow.token ? `curl -sS ${shellQuote(`${rsUrl}/v1/streams`)} -H 'Authorization: Bearer ${flow.token}'` : `curl -sS ${shellQuote(`${rsUrl}/v1/streams`)} -H 'Authorization: Bearer '`, + // Curl examples mirror what the BFF actually sends on the wire: + // application/json bodies with the owner-session cookie, using the + // documented isJsonRequest CSRF exemption. /device/approve does not + // accept subject_id from the body — the AS derives it from the session. + startCurl: `curl -sS -X POST ${shellQuote(`${asUrl}/oauth/device_authorization`)} \\\n -H 'Content-Type: application/json' \\\n --data ${shellQuote(JSON.stringify({ client_id: flow.clientId }))}`, }; } diff --git a/apps/console/src/app/(console)/lib/operator-grant-request.ts b/apps/console/src/app/(console)/lib/operator-grant-request.ts index 823f27b67..83f79ed76 100644 --- a/apps/console/src/app/(console)/lib/operator-grant-request.ts +++ b/apps/console/src/app/(console)/lib/operator-grant-request.ts @@ -94,8 +94,8 @@ function normalizeSourceKind(value: string | undefined): GrantRequestDraft["sour function sourceFromDraft(draft: GrantRequestDraft) { return { - kind: draft.sourceKind, id: draft.sourceId, + kind: draft.sourceKind, }; } @@ -122,44 +122,44 @@ export async function loadConnectionPinOptions(draft: GrantRequestDraft): Promis export function createDefaultGrantRequestDraft(): GrantRequestDraft { return { - initialAccessToken: DEFAULT_DCR_INITIAL_ACCESS_TOKEN, + accessMode: "single_use", clientId: "", clientName: "Longview", clientUri: "", - redirectUri: "", - sourceKind: "connector", - sourceId: "", + connectionId: "", + fields: "", + initialAccessToken: DEFAULT_DCR_INITIAL_ACCESS_TOKEN, purposeCode: "https://pdpp.org/purpose/financial_planning", purposeDescription: "Compare personal data across providers.", - accessMode: "single_use", + redirectUri: "", retention: "P30D", + sourceId: "", + sourceKind: "connector", streamName: "", - connectionId: "", - fields: "", - view: "", subjectId: "owner_local", + view: "", }; } function sanitizeDraft(input: Partial = {}): GrantRequestDraft { const base = createDefaultGrantRequestDraft(); return { - initialAccessToken: trim(input.initialAccessToken) || base.initialAccessToken, + accessMode: trim(input.accessMode) || base.accessMode, clientId: trim(input.clientId), clientName: trim(input.clientName) || base.clientName, clientUri: trim(input.clientUri), - redirectUri: trim(input.redirectUri), - sourceKind: normalizeSourceKind(input.sourceKind), - sourceId: trim(input.sourceId), + connectionId: trim(input.connectionId), + fields: trim(input.fields), + initialAccessToken: trim(input.initialAccessToken) || base.initialAccessToken, purposeCode: trim(input.purposeCode) || base.purposeCode, purposeDescription: trim(input.purposeDescription) || base.purposeDescription, - accessMode: trim(input.accessMode) || base.accessMode, + redirectUri: trim(input.redirectUri), retention: trim(input.retention) || base.retention, + sourceId: trim(input.sourceId), + sourceKind: normalizeSourceKind(input.sourceKind), streamName: trim(input.streamName), - connectionId: trim(input.connectionId), - fields: trim(input.fields), - view: trim(input.view), subjectId: trim(input.subjectId) || base.subjectId, + view: trim(input.view), }; } @@ -200,13 +200,13 @@ function upsertWorkspace(workspaceId: string | undefined, input: Partial; return saveWorkspace({ ...workspace, - updatedAt: nowIso(), draft: { ...workspace.draft, clientId: typeof registeredClient.client_id === "string" ? registeredClient.client_id : workspace.draft.clientId, }, - registeredClient, lastError: null, + registeredClient, + updatedAt: nowIso(), }); } @@ -310,25 +310,25 @@ export async function stageGrantRequest( } const request = { - client_id: clientId, - client_display: workspace.draft.clientName ? { name: workspace.draft.clientName } : undefined, authorization_details: [ { - type: "https://pdpp.org/data-access", - source: sourceFromDraft(workspace.draft), + access_mode: workspace.draft.accessMode, purpose_code: workspace.draft.purposeCode, purpose_description: workspace.draft.purposeDescription, - access_mode: workspace.draft.accessMode, retention: workspace.draft.retention, + source: sourceFromDraft(workspace.draft), streams: [streamSelectionFromDraft(workspace.draft, workspace.draft.streamName)], + type: "https://pdpp.org/data-access", }, ], + client_display: workspace.draft.clientName ? { name: workspace.draft.clientName } : undefined, + client_id: clientId, }; const response = await fetchAs("/oauth/par", { - method: "POST", - headers: { "Content-Type": "application/json" }, body: JSON.stringify(request), + headers: { "Content-Type": "application/json" }, + method: "POST", }); const body = await readBody(response); if (!(response.ok && body) || typeof body !== "object") { @@ -337,13 +337,13 @@ export async function stageGrantRequest( return saveWorkspace({ ...workspace, - updatedAt: nowIso(), draft: { ...workspace.draft, clientId, }, - stagedRequest: body as Record, lastError: null, + stagedRequest: body as Record, + updatedAt: nowIso(), }); } @@ -357,8 +357,8 @@ export async function approveGrantRequestWorkspace(workspaceId: string): Promise await approveConsentRequest(requestUri, workspace.draft.subjectId); return saveWorkspace({ ...workspace, - updatedAt: nowIso(), lastError: null, + updatedAt: nowIso(), }); } @@ -372,8 +372,8 @@ export async function denyGrantRequestWorkspace(workspaceId: string): Promise"); const request = { - client_id: - workspace.draft.clientId || - (typeof workspace.registeredClient?.client_id === "string" - ? workspace.registeredClient.client_id - : ""), - client_display: workspace.draft.clientName ? { name: workspace.draft.clientName } : undefined, authorization_details: [ { - type: "https://pdpp.org/data-access", - source: sourceFromDraft(workspace.draft), + access_mode: workspace.draft.accessMode, purpose_code: workspace.draft.purposeCode, purpose_description: workspace.draft.purposeDescription, - access_mode: workspace.draft.accessMode, retention: workspace.draft.retention, + source: sourceFromDraft(workspace.draft), streams: [streamSelection], + type: "https://pdpp.org/data-access", }, ], + client_display: workspace.draft.clientName ? { name: workspace.draft.clientName } : undefined, + client_id: + workspace.draft.clientId || + (typeof workspace.registeredClient?.client_id === "string" + ? workspace.registeredClient.client_id + : ""), }; return { diff --git a/apps/console/src/app/(console)/lib/operator-runs.ts b/apps/console/src/app/(console)/lib/operator-runs.ts index 72e4013e7..5c56fa94c 100644 --- a/apps/console/src/app/(console)/lib/operator-runs.ts +++ b/apps/console/src/app/(console)/lib/operator-runs.ts @@ -62,10 +62,10 @@ function parseDurationInput(value: string, label: string): number { const amount = Number.parseInt(match[1] ?? "0", 10); const unit = (match[2] || "s").toLowerCase(); const multipliers: Record = { - s: 1, - m: 60, - h: 60 * 60, d: 24 * 60 * 60, + h: 60 * 60, + m: 60, + s: 1, }; const multiplier = multipliers[unit] ?? 1; return amount * multiplier; @@ -89,8 +89,8 @@ async function runNowAt(path: string, options: RunNowOptions = {}) { method: "POST", ...(force ? { - headers: { "Content-Type": "application/json" }, body: asJson({ force: true }), + headers: { "Content-Type": "application/json" }, } : {}), }); @@ -110,15 +110,15 @@ async function saveScheduleAt( } ) { const body = { - interval_seconds: parseDurationInput(input.every, "schedule interval"), enabled: input.enabled, + interval_seconds: parseDurationInput(input.every, "schedule interval"), ...(input.jitter?.trim() ? { jitter_seconds: parseDurationInput(input.jitter, "schedule jitter") } : {}), }; const response = await fetchAs(path, { - method: "PUT", - headers: { "Content-Type": "application/json" }, body: asJson(body), + headers: { "Content-Type": "application/json" }, + method: "PUT", }); const responseBody = await readBody(response); if (!response.ok) { @@ -147,9 +147,9 @@ async function postScheduleMutationAt(path: string, fallback: string) { */ export async function setConnectionDisplayName(connectionId: string, displayName: string) { const response = await fetchAs(connectionControlPath(connectionId, ""), { - method: "PATCH", - headers: { "Content-Type": "application/json" }, body: asJson({ display_name: displayName }), + headers: { "Content-Type": "application/json" }, + method: "PATCH", }); const body = await readBody(response); if (!response.ok) { @@ -229,9 +229,9 @@ export async function submitRunInteraction( payload.data = input.data; } const response = await fetchAs(`/_ref/runs/${encodeURIComponent(runId)}/interaction`, { - method: "POST", - headers: { "Content-Type": "application/json" }, body: asJson(payload), + headers: { "Content-Type": "application/json" }, + method: "POST", }); const body = await readBody(response); if (!response.ok) { @@ -319,9 +319,9 @@ export async function mintRunInteractionStream( payload.idempotency_key = input.idempotencyKey; } const response = await fetchAs(`/_ref/runs/${encodeURIComponent(runId)}/run-interaction-stream`, { - method: "POST", - headers: { "Content-Type": "application/json" }, body: asJson(payload), + headers: { "Content-Type": "application/json" }, + method: "POST", }); const body = await readBody(response); if (!response.ok) { @@ -346,13 +346,13 @@ export async function reportRunInteractionStreamReachFailure( input: { interactionId: string; reason: string; httpStatus: number | null } ): Promise { const response = await fetchAs(`/_ref/runs/${encodeURIComponent(runId)}/run-interaction-stream/reach-failure`, { - method: "POST", - headers: { "Content-Type": "application/json" }, body: asJson({ + http_status: input.httpStatus, interaction_id: input.interactionId, reason: input.reason, - http_status: input.httpStatus, }), + headers: { "Content-Type": "application/json" }, + method: "POST", }); if (!response.ok) { const body = await readBody(response); diff --git a/apps/console/src/app/(console)/lib/owner-token.ts b/apps/console/src/app/(console)/lib/owner-token.ts index 859f97539..9104b1be3 100644 --- a/apps/console/src/app/(console)/lib/owner-token.ts +++ b/apps/console/src/app/(console)/lib/owner-token.ts @@ -238,10 +238,10 @@ async function mintOwnerToken(): Promise { deviceRes = await fetch( `${asUrl}/oauth/device_authorization`, await withOwnerSessionCookie({ - method: "POST", - headers: { "Content-Type": "application/json" }, body: JSON.stringify({ client_id: CLIENT_ID }), cache: "no-store", + headers: { "Content-Type": "application/json" }, + method: "POST", }) ); } catch (err) { @@ -259,10 +259,10 @@ async function mintOwnerToken(): Promise { const approveRes = await fetch( `${asUrl}/device/approve`, await withOwnerSessionCookie({ - method: "POST", - headers: { "Content-Type": "application/json" }, body: JSON.stringify({ user_code: device.user_code }), cache: "no-store", + headers: { "Content-Type": "application/json" }, + method: "POST", }) ); if (!approveRes.ok) { @@ -276,14 +276,14 @@ async function mintOwnerToken(): Promise { const tokenRes = await fetch( `${asUrl}/oauth/token`, await withOwnerSessionCookie({ - method: "POST", - headers: { "Content-Type": "application/json" }, body: JSON.stringify({ - grant_type: "urn:ietf:params:oauth:grant-type:device_code", - device_code: device.device_code, client_id: CLIENT_ID, + device_code: device.device_code, + grant_type: "urn:ietf:params:oauth:grant-type:device_code", }), cache: "no-store", + headers: { "Content-Type": "application/json" }, + method: "POST", }) ); if (!tokenRes.ok) { diff --git a/apps/console/src/app/(console)/lib/read-envelope.test.ts b/apps/console/src/app/(console)/lib/read-envelope.test.ts index 330b96896..3c673dfce 100644 --- a/apps/console/src/app/(console)/lib/read-envelope.test.ts +++ b/apps/console/src/app/(console)/lib/read-envelope.test.ts @@ -30,13 +30,13 @@ test("adaptListEnvelope reads canonical links.next and meta.warnings when presen const env = adaptListEnvelope<{ id: string }>({ data: [], has_more: false, - links: { self: "/v1/streams/x/records", next: "/v1/streams/x/records?cursor=abc" }, + links: { next: "/v1/streams/x/records?cursor=abc", self: "/v1/streams/x/records" }, meta: { + count: { kind: "estimated", value: 42 }, warnings: [ { code: "deprecated_alias", message: "connector_instance_id is deprecated" }, { code: "count_downgraded", dropped_parameter: "count=exact" }, ], - count: { kind: "estimated", value: 42 }, }, }); assert.equal(env.links.self, "/v1/streams/x/records"); @@ -70,9 +70,9 @@ test("adaptListEnvelope ignores unknown count kinds", () => { test("extractReadWarnings works on single-record envelopes", () => { const warnings = extractReadWarnings({ - object: "record", data: { id: "rec_1" }, meta: { warnings: [{ code: "skipped_source", message: "x not applicable" }] }, + object: "record", }); assert.equal(warnings.length, 1); assert.equal(warnings[0]?.code, "skipped_source"); diff --git a/apps/console/src/app/(console)/lib/read-envelope.ts b/apps/console/src/app/(console)/lib/read-envelope.ts index 6cadf6a1a..f469de79b 100644 --- a/apps/console/src/app/(console)/lib/read-envelope.ts +++ b/apps/console/src/app/(console)/lib/read-envelope.ts @@ -108,9 +108,9 @@ export function adaptListEnvelope(body: unknown): CanonicalListEnvelope { return { data, has_more: hasMore, - next_cursor: nextCursor, links: extractLinks((root as { links?: unknown }).links), meta: extractMeta((root as { meta?: unknown }).meta), + next_cursor: nextCursor, }; } diff --git a/apps/console/src/app/(console)/lib/record-timestamps.ts b/apps/console/src/app/(console)/lib/record-timestamps.ts index 119326934..66e45c601 100644 --- a/apps/console/src/app/(console)/lib/record-timestamps.ts +++ b/apps/console/src/app/(console)/lib/record-timestamps.ts @@ -48,14 +48,14 @@ export function primaryTimestamp( ): { value: string; label: string; secondary: { label: string; value: string } | null } { if (semanticTimestamp) { return { - value: formatSemanticTimestamp(semanticTimestamp.value), label: humanizeFieldName(semanticTimestamp.field), secondary: { label: "ingested", value: formatTimestamp(emittedAt) }, + value: formatSemanticTimestamp(semanticTimestamp.value), }; } return { - value: formatTimestamp(emittedAt), label: "ingested", secondary: null, + value: formatTimestamp(emittedAt), }; } diff --git a/apps/console/src/app/(console)/lib/ref-client.ts b/apps/console/src/app/(console)/lib/ref-client.ts index 6273998e4..29b5776d1 100644 --- a/apps/console/src/app/(console)/lib/ref-client.ts +++ b/apps/console/src/app/(console)/lib/ref-client.ts @@ -136,12 +136,12 @@ function normalizeTimeline(raw: unknown): TimelineEnvelope { ? r.terminal_status : null; return { - object: r.object ?? "timeline", - trace_id: r.trace_id ?? null, event_count: typeof r.event_count === "number" ? r.event_count : events.length, events, next_cursor: typeof r.next_cursor === "string" && r.next_cursor.length > 0 ? r.next_cursor : undefined, + object: r.object ?? "timeline", terminal_status: terminalStatus, + trace_id: r.trace_id ?? null, truncated: r.truncated === true, }; } @@ -1439,14 +1439,14 @@ export async function listExploreTimeline( return (await refFetch("/_ref/explore/records", { connection: connection || undefined, cursor: opts.cursor ?? undefined, + // Only the oldest-first re-page sends a direction; newest-first is the default. + direction: opts.direction === "asc" ? "asc" : undefined, limit: opts.limit, - upcoming_limit: opts.upcomingLimit, rewind: opts.rewindToFirstPage ? 1 : undefined, stream: stream || undefined, + upcoming_limit: opts.upcomingLimit, xconnection: xconnection || undefined, xstream: xstream || undefined, - // Only the oldest-first re-page sends a direction; newest-first is the default. - direction: opts.direction === "asc" ? "asc" : undefined, })) as ExploreTimelinePage; } @@ -2124,9 +2124,9 @@ async function postManualUploadFile( url.searchParams.set("display_name", options.displayName); } const init = await withOwnerSessionCookie({ - method: "POST", body: file, cache: "no-store", + method: "POST", }); let res: Response; try { @@ -2621,7 +2621,7 @@ export async function lookupGrantPackageIdForGrant(grantId: string): Promise 0) { return row.grant_package_id; diff --git a/apps/console/src/app/(console)/lib/rs-client.ts b/apps/console/src/app/(console)/lib/rs-client.ts index 0af70a731..353646a14 100644 --- a/apps/console/src/app/(console)/lib/rs-client.ts +++ b/apps/console/src/app/(console)/lib/rs-client.ts @@ -229,8 +229,8 @@ async function authedFetch(path: string, params?: Record { const body = (await authedFetch("/v1/streams", { - connector_id: connectorId, connection_id: opts.connectionId, + connector_id: connectorId, connector_instance_id: opts.connectionId ? undefined : opts.connectorInstanceId, })) as { data: StreamSummary[]; @@ -264,8 +264,8 @@ export async function getStreamMetadata( opts: ConnectionReadOptions = {} ): Promise { return (await authedFetch(`/v1/streams/${encodeURIComponent(stream)}`, { - connector_id: connectorId, connection_id: opts.connectionId, + connector_id: connectorId, connector_instance_id: opts.connectionId ? undefined : opts.connectorInstanceId, })) as StreamMetadata; } @@ -297,12 +297,12 @@ export async function queryRecords( } } const body = (await authedFetch(`/v1/streams/${encodeURIComponent(stream)}/records`, { - connector_id: connectorId, connection_id: opts.connectionId, + connector_id: connectorId, connector_instance_id: opts.connectionId ? undefined : opts.connectorInstanceId, count: opts.count, - limit: opts.limit ?? 50, cursor: opts.cursor, + limit: opts.limit ?? 50, order: opts.order, window: opts.window, ...filterParams, @@ -378,12 +378,12 @@ export async function aggregateRecordsByTime( } ): Promise { const body = (await authedFetch(`/v1/streams/${encodeURIComponent(stream)}/aggregate`, { - connector_id: connectorId, connection_id: opts.connectionId, + connector_id: connectorId, connector_instance_id: opts.connectionId ? undefined : opts.connectorInstanceId, - metric: "count", - group_by_time: opts.groupByTime, granularity: opts.granularity, + group_by_time: opts.groupByTime, + metric: "count", time_zone: opts.timeZone, })) as TimeBucketAggregate; return { @@ -456,13 +456,13 @@ export async function listExploreRecordBuckets(opts: { // which silently suppressed the over-time chart on the default browse. const body = (await refFetch("/_ref/explore/records/buckets", { connections: joinList(opts.connections), + granularity: opts.granularity, + since: opts.since ?? undefined, streams: joinList(opts.streams), + time_zone: opts.timeZone, + until: opts.until ?? undefined, xconnections: joinList(opts.excludeConnections), xstreams: joinList(opts.excludeStreams), - since: opts.since ?? undefined, - until: opts.until ?? undefined, - granularity: opts.granularity, - time_zone: opts.timeZone, })) as ExploreRecordBucketsResponse; return { ...body, @@ -486,8 +486,8 @@ export async function getRecord( opts: ConnectionReadOptions = {} ): Promise { const body = (await authedFetch(`/v1/streams/${encodeURIComponent(stream)}/records/${encodeURIComponent(recordId)}`, { - connector_id: connectorId, connection_id: opts.connectionId, + connector_id: connectorId, connector_instance_id: opts.connectionId ? undefined : opts.connectorInstanceId, })) as StreamRecord; return { @@ -580,8 +580,8 @@ export async function searchRecordsLexical( let res: Response; try { res = await fetch(url.toString(), { - headers: { Authorization: `Bearer ${token}` }, cache: "no-store", + headers: { Authorization: `Bearer ${token}` }, }); } catch (err) { throw new ReferenceServerUnreachableError(`Cannot reach resource server at ${getRsInternalUrl()}`, err); @@ -639,8 +639,8 @@ export async function searchRecordsSemantic( let res: Response; try { res = await fetch(url.toString(), { - headers: { Authorization: `Bearer ${token}` }, cache: "no-store", + headers: { Authorization: `Bearer ${token}` }, }); } catch (err) { throw new ReferenceServerUnreachableError(`Cannot reach resource server at ${getRsInternalUrl()}`, err); @@ -725,8 +725,8 @@ export async function searchRecordsHybrid( let res: Response; try { res = await fetch(url.toString(), { - headers: { Authorization: `Bearer ${token}` }, cache: "no-store", + headers: { Authorization: `Bearer ${token}` }, }); } catch (err) { throw new ReferenceServerUnreachableError(`Cannot reach resource server at ${getRsInternalUrl()}`, err); @@ -904,7 +904,7 @@ function computeColumnStats(key: string, records: StreamRecord[]): ColumnStats { allLong = false; } } - return { nonNullCount, allSameValue, allLong }; + return { allLong, allSameValue, nonNullCount }; } function shouldKeepColumn(key: string, records: StreamRecord[]): boolean { @@ -1159,8 +1159,8 @@ async function paginateSampleRecords( const page = await queryRecords(connectorId, streamName, { connectionId: opts.connectionId, connectorInstanceId: opts.connectorInstanceId, - limit: Math.min(pageSize, remaining), cursor, + limit: Math.min(pageSize, remaining), }); records.push(...page.data); if (!(page.has_more && page.next_cursor)) { @@ -1199,11 +1199,11 @@ function initAggMap(fieldNames: Set): Map { const agg = new Map(); for (const f of fieldNames) { agg.set(f, { - present: false, - nullCount: 0, - nonNullCount: 0, distinct: new Set(), distinctCapped: false, + nonNullCount: 0, + nullCount: 0, + present: false, sampleValue: null, }); } @@ -1263,8 +1263,8 @@ interface ScanResult { function scanRecords(records: StreamRecord[], fieldNames: Set, cursorField: string | null): ScanResult { const agg = initAggMap(fieldNames); - const emittedRange: RangeTracker = { min: null, max: null }; - const cursorRange: RangeTracker = { min: null, max: null }; + const emittedRange: RangeTracker = { max: null, min: null }; + const cursorRange: RangeTracker = { max: null, min: null }; for (const r of records) { if (r.emitted_at) { @@ -1283,19 +1283,19 @@ function scanRecords(records: StreamRecord[], fieldNames: Set, cursorFie } } - return { agg, emittedRange, cursorRange }; + return { agg, cursorRange, emittedRange }; } function projectFields(agg: Map, declaredProps: string[]): FieldHealth[] { const declaredSet = new Set(declaredProps); return Array.from(agg, ([name, a]) => ({ - name, declared: declaredSet.has(name), - present: a.present, - nullCount: a.nullCount, - nonNullCount: a.nonNullCount, - distinctValues: a.distinct.size, distinctCapped: a.distinctCapped, + distinctValues: a.distinct.size, + name, + nonNullCount: a.nonNullCount, + nullCount: a.nullCount, + present: a.present, sampleValue: a.sampleValue, })).sort((x, y) => { // declared-first, then by name, for stable display @@ -1308,11 +1308,11 @@ function projectFields(agg: Map, declaredProps: string[]): Fie function computeFieldSummary(fields: FieldHealth[]) { return { - declared: fields.filter((f) => f.declared).length, - present: fields.filter((f) => f.present).length, - entirelyNull: fields.filter((f) => f.present && f.nonNullCount === 0).length, constValued: fields.filter((f) => f.nonNullCount > 0 && f.distinctValues === 1).length, + declared: fields.filter((f) => f.declared).length, declaredButAbsent: fields.filter((f) => f.declared && !f.present).length, + entirelyNull: fields.filter((f) => f.present && f.nonNullCount === 0).length, + present: fields.filter((f) => f.present).length, undeclaredPresent: fields.filter((f) => !f.declared && f.present).length, }; } @@ -1348,16 +1348,16 @@ export async function streamHealth( return { connectorId, - streamName, - totalRecords, - sampled: records.length, - sampleLimit, - limited: totalRecords > records.length, - emittedAt: { min: emittedRange.min, max: emittedRange.max }, cursorField, - cursorRange: cursorField ? { min: cursorRange.min, max: cursorRange.max } : null, + cursorRange: cursorField ? { max: cursorRange.max, min: cursorRange.min } : null, + emittedAt: { max: emittedRange.max, min: emittedRange.min }, fields, + limited: totalRecords > records.length, + sampled: records.length, + sampleLimit, + streamName, summary, + totalRecords, }; } @@ -1378,13 +1378,13 @@ function projectRun( return null; } return { - run_id: summary.run_id, - first_at: summary.first_at, - last_at: summary.last_at, event_count: summary.event_count, - status: summary.status, failure_reason: summary.failure_reason, + first_at: summary.first_at, known_gaps: summary.known_gaps ?? [], + last_at: summary.last_at, + run_id: summary.run_id, + status: summary.status, }; } @@ -1399,7 +1399,7 @@ export async function getConnectorOverview(connector: ConnectorManifest): Promis const { listRuns } = await import("./ref-client.ts"); const [latestResp, successResp] = await Promise.all([ listRuns({ connector_id: connector.connector_id, limit: 1 }), - listRuns({ connector_id: connector.connector_id, status: "succeeded", limit: 1 }), + listRuns({ connector_id: connector.connector_id, limit: 1, status: "succeeded" }), ]); const lastRun = projectRun(latestResp.data?.[0]); const lastSuccessfulRun = projectRun(successResp.data?.[0]); @@ -1407,11 +1407,11 @@ export async function getConnectorOverview(connector: ConnectorManifest): Promis return { connector, - streams, - totalRecords, + isRunning, lastRun, lastSuccessfulRun, - isRunning, + streams, + totalRecords, }; } catch (err) { if (err instanceof ReferenceServerUnreachableError) { @@ -1419,12 +1419,12 @@ export async function getConnectorOverview(connector: ConnectorManifest): Promis } return { connector, - streams: [], - totalRecords: 0, + error: err instanceof Error ? err.message : String(err), + isRunning: false, lastRun: null, lastSuccessfulRun: null, - isRunning: false, - error: err instanceof Error ? err.message : String(err), + streams: [], + totalRecords: 0, }; } } diff --git a/apps/console/src/app/(console)/lib/run-assistance.test.ts b/apps/console/src/app/(console)/lib/run-assistance.test.ts index ce01a12d6..7bf2cbce3 100644 --- a/apps/console/src/app/(console)/lib/run-assistance.test.ts +++ b/apps/console/src/app/(console)/lib/run-assistance.test.ts @@ -149,8 +149,8 @@ test("active browser-surface events keep stream fallback in the browser-preparin }), event("run.browser_surface_ready", { browser_surface: { - browser_surface_status: "leased", browser_surface_lease_id: "lease_1", + browser_surface_status: "leased", }, }), event("run.started", { diff --git a/apps/console/src/app/(console)/lib/run-assistance.ts b/apps/console/src/app/(console)/lib/run-assistance.ts index be8b3953a..c789ead7f 100644 --- a/apps/console/src/app/(console)/lib/run-assistance.ts +++ b/apps/console/src/app/(console)/lib/run-assistance.ts @@ -195,15 +195,15 @@ function isStreamableBrowserSurfaceAssistance(assistance: CurrentRunAssistance): function assistanceFromEvent(event: SpineEvent, id: string): CurrentRunAssistance { const data = event.data ?? {}; return { + attachments: parseAttachments(data.attachments), + fields: parseFields(data.input_schema ?? data.schema), id, isLegacyInteraction: false, kind: stringField(data.kind) ?? "assistance", message: stringField(data.message) ?? "Waiting for the requested run assistance.", - progressPosture: progressPostureField(data.progress_posture) ?? "blocked", ownerAction: ownerActionField(data.owner_action) ?? "provide_value", + progressPosture: progressPostureField(data.progress_posture) ?? "blocked", responseContract: responseContractField(data.response_contract) ?? "response_required", - attachments: parseAttachments(data.attachments), - fields: parseFields(data.input_schema ?? data.schema), timeoutLabel: timeoutLabel(data.timeout_seconds), }; } @@ -213,15 +213,15 @@ function assistanceFromLegacyInteraction(event: SpineEvent, id: string): Current const kind = stringField(data.kind) ?? "interaction"; const isManualAction = kind === "manual_action"; return { + attachments: isManualAction ? [{ kind: "browser_surface", label: null, ref: null, status: null }] : [], + fields: parseFields(data.schema), id, isLegacyInteraction: true, kind, message: stringField(data.message) ?? "Awaiting operator response.", - progressPosture: "blocked", ownerAction: isManualAction ? "operate_attachment" : "provide_value", + progressPosture: "blocked", responseContract: "response_required", - attachments: isManualAction ? [{ kind: "browser_surface", label: null, ref: null, status: null }] : [], - fields: parseFields(data.schema), timeoutLabel: timeoutLabel(data.timeout_seconds), }; } @@ -298,9 +298,9 @@ function parseFields(schema: unknown): AssistanceField[] { .map(([name, rawDef]): AssistanceField => { const def = rawDef && typeof rawDef === "object" ? (rawDef as Record) : {}; return { - name, - label: stringField(def.title), format: def.format === "password" ? "password" : "text", + label: stringField(def.title), + name, required: requiredFields.has(name), }; }) diff --git a/apps/console/src/app/(console)/lib/run-gaps.test.ts b/apps/console/src/app/(console)/lib/run-gaps.test.ts index 7c5530f01..695d3cc77 100644 --- a/apps/console/src/app/(console)/lib/run-gaps.test.ts +++ b/apps/console/src/app/(console)/lib/run-gaps.test.ts @@ -70,29 +70,29 @@ test("classifyKnownGaps keeps protocol violations distinct from source coverage test("connectorHasPartialCoverageHint requires produced records and a non-protocol coverage gap", () => { assert.equal( connectorHasPartialCoverageHint({ - totalRecords: 12, lastRunKnownGaps: [{ kind: "skip_result", reason: "missing_credentials" }], + totalRecords: 12, }), true ); assert.equal( connectorHasPartialCoverageHint({ - totalRecords: 0, lastRunKnownGaps: [{ kind: "skip_result", reason: "missing_credentials" }], + totalRecords: 0, }), false ); assert.equal( connectorHasPartialCoverageHint({ - totalRecords: 12, lastRunKnownGaps: [{ kind: "run_failed", reason: "connector_protocol_violation" }], + totalRecords: 12, }), false ); assert.equal( connectorHasPartialCoverageHint({ - totalRecords: 12, lastRunKnownGaps: [{ kind: "skip_result", reason: "not_available", severity: "informational" }], + totalRecords: 12, }), false ); @@ -111,7 +111,7 @@ test("extractTerminalKnownGaps reads the latest terminal event payload", () => { recovery_hint: { action: "retry_by_runtime" }, }, ], - known_gaps_summary: { count: 1, truncated: false, by_reason: { partially_committed: 1 } }, + known_gaps_summary: { by_reason: { partially_committed: 1 }, count: 1, truncated: false }, }, event_id: "evt_1", event_type: "run.failed", @@ -164,25 +164,25 @@ test("normalizeKnownGaps and formatRecoveryHint tolerate unknown payloads", () = }); test("normalizeKnownGaps propagates bounded SKIP_RESULT diagnostics object", () => { - const diagnostics = { phase: "export_artifact_wait_failed", error: "download_empty", dialogs_open: 1 }; - const [gap] = normalizeKnownGaps([{ kind: "skip_result", reason: "export_no_download", diagnostics }]); + const diagnostics = { dialogs_open: 1, error: "download_empty", phase: "export_artifact_wait_failed" }; + const [gap] = normalizeKnownGaps([{ diagnostics, kind: "skip_result", reason: "export_no_download" }]); assert.ok(gap); assert.deepEqual(gap.diagnostics, diagnostics); }); test("normalizeKnownGaps drops diagnostics when value is an array or scalar", () => { - const [gapArray] = normalizeKnownGaps([{ kind: "skip_result", reason: "r", diagnostics: ["a", "b"] }]); + const [gapArray] = normalizeKnownGaps([{ diagnostics: ["a", "b"], kind: "skip_result", reason: "r" }]); assert.ok(gapArray); assert.equal(gapArray.diagnostics, undefined); - const [gapString] = normalizeKnownGaps([{ kind: "skip_result", reason: "r", diagnostics: "text" }]); + const [gapString] = normalizeKnownGaps([{ diagnostics: "text", kind: "skip_result", reason: "r" }]); assert.ok(gapString); assert.equal(gapString.diagnostics, undefined); }); test("normalizeKnownGaps passes sentinel diagnostics object through unchanged", () => { - const sentinel = { truncated: true, reason: "size_overflow" }; - const [gap] = normalizeKnownGaps([{ kind: "skip_result", reason: "export_no_download", diagnostics: sentinel }]); + const sentinel = { reason: "size_overflow", truncated: true }; + const [gap] = normalizeKnownGaps([{ diagnostics: sentinel, kind: "skip_result", reason: "export_no_download" }]); assert.ok(gap); assert.deepEqual(gap.diagnostics, sentinel); }); @@ -194,8 +194,8 @@ test("extractTerminalKnownGaps preserves diagnostics on known gaps", () => { actor_type: "runtime", client_id: null, data: { - known_gaps: [{ kind: "skip_result", reason: "export_no_download", diagnostics }], - known_gaps_summary: { count: 1, truncated: false, by_reason: { export_no_download: 1 } }, + known_gaps: [{ diagnostics, kind: "skip_result", reason: "export_no_download" }], + known_gaps_summary: { by_reason: { export_no_download: 1 }, count: 1, truncated: false }, }, event_id: "evt_2", event_type: "run.completed", diff --git a/apps/console/src/app/(console)/lib/run-gaps.ts b/apps/console/src/app/(console)/lib/run-gaps.ts index e5c3ef11a..94981ba65 100644 --- a/apps/console/src/app/(console)/lib/run-gaps.ts +++ b/apps/console/src/app/(console)/lib/run-gaps.ts @@ -244,8 +244,8 @@ function summarizeKnownGaps(gaps: readonly KnownGap[]): KnownGapSummary { byReason[gap.reason] = (byReason[gap.reason] ?? 0) + 1; } return { + by_reason: byReason, count: gaps.length, truncated: false, - by_reason: byReason, }; } diff --git a/apps/console/src/app/(console)/lib/source-actionability.test.ts b/apps/console/src/app/(console)/lib/source-actionability.test.ts index 8213134e4..6dad1fdbc 100644 --- a/apps/console/src/app/(console)/lib/source-actionability.test.ts +++ b/apps/console/src/app/(console)/lib/source-actionability.test.ts @@ -244,8 +244,8 @@ test("source actionability routes a Needs refresh pill (no wired owner action) t connector({ rendered_verdict: verdict({ channel: "advisory", - pill: { label: "Needs refresh", tone: "amber" }, forward_statement: "This connection is paused.", + pill: { label: "Needs refresh", tone: "amber" }, required_actions: [], }), }) @@ -269,8 +269,8 @@ test("source actionability routes a Syncing pill (active run over stale/owner-re }), rendered_verdict: verdict({ channel: "calm", - pill: { label: "Syncing", tone: "amber" }, forward_statement: "Refreshing now.", + pill: { label: "Syncing", tone: "amber" }, required_actions: [], }), }) @@ -288,8 +288,8 @@ test("source actionability keeps a Degraded pill (no wired owner action) in syst connector({ rendered_verdict: verdict({ channel: "advisory", - pill: { label: "Degraded", tone: "amber" }, forward_statement: "Connector code needs a fix before this can collect again.", + pill: { label: "Degraded", tone: "amber" }, required_actions: [ action({ audience: "maintainer", @@ -438,7 +438,7 @@ test("source actionability projects a draft connection as needs-you setup_in_pro test("source actionability: revoked outranks draft — a revoked connection never reads as setup_in_progress", () => { const actionability = projectSourceActionability( - draftConnector({ status: "revoked", revoked_at: "2026-07-10T00:00:00Z" }) + draftConnector({ revoked_at: "2026-07-10T00:00:00Z", status: "revoked" }) ); assert.equal(actionability.revoked, true); @@ -493,8 +493,8 @@ test("source actionability headline counts only needs-owner work and exposes sta display_name: "Review source", rendered_verdict: verdict({ channel: "advisory", - pill: { label: "Healthy", tone: "green" }, forward_statement: "Run a refresh to bring this up to date.", + pill: { label: "Healthy", tone: "green" }, required_actions: [ action({ cta: "Refresh now", @@ -510,8 +510,8 @@ test("source actionability headline counts only needs-owner work and exposes sta display_name: "System source", rendered_verdict: verdict({ channel: "advisory", - pill: { label: "Degraded", tone: "amber" }, forward_statement: "Connector code needs a fix before this can collect again.", + pill: { label: "Degraded", tone: "amber" }, required_actions: [ action({ audience: "maintainer", @@ -528,8 +528,8 @@ test("source actionability headline counts only needs-owner work and exposes sta display_name: "Not measured source", rendered_verdict: verdict({ channel: "calm", - pill: { label: "Not measured", tone: "grey" }, forward_statement: "Freshness has not been measured yet.", + pill: { label: "Not measured", tone: "grey" }, required_actions: [], }), }), @@ -538,8 +538,8 @@ test("source actionability headline counts only needs-owner work and exposes sta display_name: "Working source", rendered_verdict: verdict({ channel: "calm", - pill: { label: "Checking", tone: "grey" }, forward_statement: "Measuring coverage now.", + pill: { label: "Checking", tone: "grey" }, required_actions: [], }), }), @@ -556,6 +556,10 @@ test("source actionability headline counts only needs-owner work and exposes sta label: "Needs you", note: "Requires your input before collection can continue.", }, + notMeasured: { + label: "Not measured", + note: "Evidence is missing and no active check is running.", + }, review: { label: "Available actions", note: "Optional refreshes and retries you can start.", @@ -568,10 +572,6 @@ test("source actionability headline counts only needs-owner work and exposes sta label: "PDPP is working", note: "Collection, recovery, or a bounded check is active.", }, - notMeasured: { - label: "Not measured", - note: "Evidence is missing and no active check is running.", - }, }); }); @@ -586,8 +586,8 @@ test("source actionability groups a Needs refresh connection under review, never display_name: "Paused source", rendered_verdict: verdict({ channel: "advisory", - pill: { label: "Needs refresh", tone: "amber" }, forward_statement: "This connection is paused.", + pill: { label: "Needs refresh", tone: "amber" }, required_actions: [], }), }), diff --git a/apps/console/src/app/(console)/lib/source-actionability.ts b/apps/console/src/app/(console)/lib/source-actionability.ts index 03eee4a68..386452bbd 100644 --- a/apps/console/src/app/(console)/lib/source-actionability.ts +++ b/apps/console/src/app/(console)/lib/source-actionability.ts @@ -103,6 +103,10 @@ export const SOURCE_WORK_GROUP_COPY: Record> = { - green: { kind: "healthy", dot: "●", tone: "success" }, - amber: { kind: "degraded", dot: "◐", tone: "warning" }, - red: { kind: "blocked", dot: "⊘", tone: "destructive" }, - grey: { kind: "unknown", dot: "○", tone: "muted" }, + amber: { dot: "◐", kind: "degraded", tone: "warning" }, + green: { dot: "●", kind: "healthy", tone: "success" }, + grey: { dot: "○", kind: "unknown", tone: "muted" }, + red: { dot: "⊘", kind: "blocked", tone: "destructive" }, }; function readableConnectorId(connectorId: string): string { @@ -232,30 +232,30 @@ export function deriveRenderedSourceStatus( pending = false ): SourceStatusFlag { if (revoked) { - return { kind: "revoked", dot: "⊘", tone: "muted", label: "Revoked", freshnessNote: null }; + return { dot: "⊘", freshnessNote: null, kind: "revoked", label: "Revoked", tone: "muted" }; } // Setup-in-progress overrides any verdict shape, same priority as revoked: // a draft has no meaningful health/coverage evidence yet (see // `isSetupInProgressConnector`), so its verdict tone (if any) must never be // shown as the status. if (pending) { - return { kind: "pending", dot: "◌", tone: "muted", label: "Setup in progress", freshnessNote: null }; + return { dot: "◌", freshnessNote: null, kind: "pending", label: "Setup in progress", tone: "muted" }; } if (!verdict) { return { - kind: "unknown", dot: "○", - tone: "muted", - label: "Verdict unavailable", freshnessNote: null, + kind: "unknown", + label: "Verdict unavailable", + tone: "muted", }; } const status = VERDICT_TONE_STATUS[verdict.pill.tone]; const freshnessNote = freshnessNoteFromVerdict(verdict); return { ...status, - label: labelWithFreshness(verdict.pill.label, freshnessNote), freshnessNote, + label: labelWithFreshness(verdict.pill.label, freshnessNote), }; } @@ -404,15 +404,15 @@ function isNotMeasured(verdict: NonNullable> = { active: "is syncing details", - queued: "is catching up", cooling: "is waiting to retry", + queued: "is catching up", stalled: "recovery is stalled", }; const RECOVERY_WHAT: Partial> = { active: "Syncing details now.", - queued: "Catching up details when it is safe to retry.", cooling: "Waiting until it is safe to retry details.", + queued: "Catching up details when it is safe to retry.", stalled: "Recovery has stopped making progress and needs a look.", }; diff --git a/apps/console/src/app/(console)/lib/source-add-support.test.ts b/apps/console/src/app/(console)/lib/source-add-support.test.ts index ffd0ced3a..5be813152 100644 --- a/apps/console/src/app/(console)/lib/source-add-support.test.ts +++ b/apps/console/src/app/(console)/lib/source-add-support.test.ts @@ -41,11 +41,11 @@ function staticSecretManifest(connectorId: string): CatalogManifestLike { display_name: connectorId, runtime_requirements: { bindings: { network: {} } }, setup: { - modality: "static_secret", credential_capture: { credential_kind: "api_token", - fields: [{ name: "api_token", label: "API token", secret: true }], + fields: [{ label: "API token", name: "api_token", secret: true }], }, + modality: "static_secret", }, } as unknown as CatalogManifestLike; } @@ -80,12 +80,12 @@ function manualUploadManifest(connectorId: string): CatalogManifestLike { display_name: connectorId, runtime_requirements: { bindings: { filesystem: {} } }, setup: { - modality: "manual_or_upload", manual_or_upload: { accepted_file_names: ["Timeline.json"], import_dir_env_var: "GOOGLE_MAPS_TIMELINE_DIR", label: "Timeline export", }, + modality: "manual_or_upload", }, }; } @@ -108,8 +108,8 @@ function providerAuthManifest(connectorId: string): CatalogManifestLike { display_name: connectorId, runtime_requirements: { bindings: { network: {} } }, setup: { - modality: "provider_authorization", deployment_config: ["GOOGLE_DATAPORTABILITY_CLIENT_ID", "GOOGLE_DATAPORTABILITY_CLIENT_SECRET"], + modality: "provider_authorization", }, }; } diff --git a/apps/console/src/app/(console)/lib/source-add-support.ts b/apps/console/src/app/(console)/lib/source-add-support.ts index 48a0440a5..891fa313d 100644 --- a/apps/console/src/app/(console)/lib/source-add-support.ts +++ b/apps/console/src/app/(console)/lib/source-add-support.ts @@ -45,17 +45,17 @@ export interface SourceAddSupport { } const SUPPORT_LABELS: Record = { - self_service: "Add another account", - packaged_path_pending: "Add path not packaged", deployment_prerequisite: "Server setup required to add another account", not_self_service: "Add path not available here", + packaged_path_pending: "Add path not packaged", + self_service: "Add another account", }; const SUPPORT_TONES: Record = { - self_service: "border-emerald-500/30 bg-emerald-500/10 text-emerald-700", - packaged_path_pending: "border-amber-500/30 bg-amber-500/10 text-amber-700", deployment_prerequisite: "border-amber-500/30 bg-amber-500/10 text-amber-700", not_self_service: "border-border bg-muted/30 text-muted-foreground", + packaged_path_pending: "border-amber-500/30 bg-amber-500/10 text-amber-700", + self_service: "border-emerald-500/30 bg-emerald-500/10 text-emerald-700", }; /** @@ -91,9 +91,9 @@ export function buildSourceAddSupport(manifests: readonly CatalogManifestLike[]) for (const entry of catalog) { const support = addAccountSupport(entry); map.set(entry.connectorKey, { + action: addAnotherAccountAction(entry), connectorKey: entry.connectorKey, support, - action: addAnotherAccountAction(entry), supportLabel: SUPPORT_LABELS[support], supportTone: SUPPORT_TONES[support], }); diff --git a/apps/console/src/app/(console)/lib/source-copy-negative.test.ts b/apps/console/src/app/(console)/lib/source-copy-negative.test.ts index 18089ed5e..c3377ae27 100644 --- a/apps/console/src/app/(console)/lib/source-copy-negative.test.ts +++ b/apps/console/src/app/(console)/lib/source-copy-negative.test.ts @@ -86,8 +86,8 @@ function stripComments(src: string): string { function entryForDisposition(disposition: ConnectorCatalogEntry["disposition"]): ConnectorCatalogEntry { return { connectorKey: `stub-${disposition}`, + deploymentReadiness: { blockers: [], ready: true }, disposition, - deploymentReadiness: { ready: true, blockers: [] }, } as unknown as ConnectorCatalogEntry; } @@ -172,8 +172,8 @@ test("the agreed add-account labels are exactly the realignment-plan vocabulary" display_name: "ynab", runtime_requirements: { bindings: { network: {} } }, setup: { + credential_capture: { credential_kind: "api_token", fields: [{ label: "T", name: "t", secret: true }] }, modality: "static_secret", - credential_capture: { credential_kind: "api_token", fields: [{ name: "t", label: "T", secret: true }] }, }, } as never, { diff --git a/apps/console/src/app/(console)/lib/source-groups.test.ts b/apps/console/src/app/(console)/lib/source-groups.test.ts index af5e2f7b2..5f9541506 100644 --- a/apps/console/src/app/(console)/lib/source-groups.test.ts +++ b/apps/console/src/app/(console)/lib/source-groups.test.ts @@ -17,31 +17,31 @@ type ConnectionHealthState = NonNullable[ function overview(partial: Partial): ConnectorOverview { return { - connector: { connector_id: "test", name: "Test", display_name: "Test" }, - streams: [], - totalRecords: 0, + connector: { connector_id: "test", display_name: "Test", name: "Test" }, isRunning: false, lastRun: null, lastSuccessfulRun: null, + streams: [], + totalRecords: 0, ...partial, }; } function healthState(state: ConnectionHealthState): ConnectorOverview["connectionHealth"] { return { - state, - reason_code: null, + axes: { attention: "none", coverage: "unknown", freshness: "unknown", outbox: "unknown", remote_surface: "none" }, + badges: { stale: false, syncing: false }, last_success_at: null, next_attempt_at: null, - axes: { freshness: "unknown", coverage: "unknown", attention: "none", outbox: "unknown", remote_surface: "none" }, - badges: { stale: false, syncing: false }, + reason_code: null, + state, } as ConnectorOverview["connectionHealth"]; } function connection(connectorId: string, opts: Partial = {}): ConnectorOverview { return overview({ - connector: { connector_id: connectorId, name: connectorId, display_name: connectorId }, connectionId: `${connectorId}-${Math.random().toString(36).slice(2, 8)}`, + connector: { connector_id: connectorId, display_name: connectorId, name: connectorId }, ...opts, }); } @@ -75,11 +75,11 @@ test("existing-data count is distinct from connection count", () => { test("needs_attention and blocked connections drive the attention count and a repair route", () => { const group = only( groupSourcesByConnector([ - connection("chase", { totalRecords: 500, connectionHealth: healthState("healthy") }), + connection("chase", { connectionHealth: healthState("healthy"), totalRecords: 500 }), connection("chase", { - totalRecords: 10, - connectionId: "chase-broken", connectionHealth: healthState("needs_attention"), + connectionId: "chase-broken", + totalRecords: 10, }), ]) ); @@ -104,8 +104,8 @@ test("revoked connections stay counted as existing source connections", () => { test("sources needing attention sort ahead of healthy sources", () => { const groups = groupSourcesByConnector([ - connection("aaa_healthy", { totalRecords: 1, connectionHealth: healthState("healthy") }), - connection("zzz_broken", { totalRecords: 1, connectionHealth: healthState("blocked") }), + connection("aaa_healthy", { connectionHealth: healthState("healthy"), totalRecords: 1 }), + connection("zzz_broken", { connectionHealth: healthState("blocked"), totalRecords: 1 }), ]); assert.equal(groups[0]?.connectorId, "zzz_broken", "attention-needed source leads despite later alphabetically"); assert.equal(groups[1]?.connectorId, "aaa_healthy"); @@ -113,7 +113,7 @@ test("sources needing attention sort ahead of healthy sources", () => { test("a healthy source carries no repair route", () => { const group = only( - groupSourcesByConnector([connection("gmail", { totalRecords: 100, connectionHealth: healthState("healthy") })]) + groupSourcesByConnector([connection("gmail", { connectionHealth: healthState("healthy"), totalRecords: 100 })]) ); assert.equal(group.needsAttentionCount, 0); assert.equal(group.attentionRouteId, null); diff --git a/apps/console/src/app/(console)/lib/source-groups.ts b/apps/console/src/app/(console)/lib/source-groups.ts index 006026636..67da6d895 100644 --- a/apps/console/src/app/(console)/lib/source-groups.ts +++ b/apps/console/src/app/(console)/lib/source-groups.ts @@ -53,17 +53,17 @@ function needsAttention(overview: ConnectorOverview): boolean { function seedGroup(overview: ConnectorOverview): SourceGroup { const attention = needsAttention(overview); return { + attentionRouteId: attention ? routeId(overview) : null, + connectionCount: 1, connectorId: overview.connector.connector_id, displayName: formatConnectorNameForDisplay({ connectorId: overview.connector.connector_id, displayName: overview.connectorDisplayName, name: overview.connector.name, }), - connectionCount: 1, needsAttentionCount: attention ? 1 : 0, revokedCount: isRevokedConnection(overview) ? 1 : 0, withDataCount: overview.totalRecords > 0 ? 1 : 0, - attentionRouteId: attention ? routeId(overview) : null, }; } diff --git a/apps/console/src/app/(console)/lib/source-recovery-state.test.ts b/apps/console/src/app/(console)/lib/source-recovery-state.test.ts index 368345af5..418f58f05 100644 --- a/apps/console/src/app/(console)/lib/source-recovery-state.test.ts +++ b/apps/console/src/app/(console)/lib/source-recovery-state.test.ts @@ -120,8 +120,8 @@ test("recovery: an active retry floor makes the step cooling", () => { const step = deriveRecoveryStep( verdict(), health({ + detail_gap_backlog: backlog({ next_attempt_at: "2026-07-06T15:40:00Z", pending: 2093 }), state: "cooling_off", - detail_gap_backlog: backlog({ pending: 2093, next_attempt_at: "2026-07-06T15:40:00Z" }), }) ); assert.equal(step, "cooling"); @@ -136,7 +136,7 @@ test("recovery: an owner-satisfiable non-attention action makes recovery eligibl action({ audience: "owner", cta: "Retry now", kind: "retry_gap", satisfied_when: { kind: "gap_recovered" } }), ], }), - health({ state: "degraded", detail_gap_backlog: backlog({ pending: 12 }) }) + health({ detail_gap_backlog: backlog({ pending: 12 }), state: "degraded" }) ); assert.equal(step, "eligible"); assert.equal(recoveryStateGroup(step), "review"); @@ -157,7 +157,7 @@ test("recovery: an attention-channel owner action is owner-required, not passive }), ], }), - health({ state: "blocked", detail_gap_backlog: backlog({ pending: 12 }) }) + health({ detail_gap_backlog: backlog({ pending: 12 }), state: "blocked" }) ); assert.equal(step, "owner_required"); assert.equal(recoveryStateGroup(step), "needsOwner"); @@ -172,7 +172,7 @@ test("recovery: a connector-defect code_fix verdict is a system issue with no re action({ audience: "maintainer", cta: "Connector code needs a fix", kind: "code_fix", terminal: true }), ], }), - health({ state: "degraded", detail_gap_backlog: backlog({ pending: 12 }) }) + health({ detail_gap_backlog: backlog({ pending: 12 }), state: "degraded" }) ); assert.equal(step, "system_issue"); assert.equal(recoveryStateGroup(step), "systemIssue"); @@ -181,8 +181,8 @@ test("recovery: a connector-defect code_fix verdict is a system issue with no re test("recovery: eligible work with a stale attempt floor beyond cadence is stalled, not queued", () => { const step = deriveRecoveryStep( verdict(), - health({ detail_gap_backlog: backlog({ pending: 2093, next_attempt_at: "2026-07-06T10:00:00Z" }) }), - { now: "2026-07-06T14:00:00Z", cadenceWindowMs: 60 * 60 * 1000 } + health({ detail_gap_backlog: backlog({ next_attempt_at: "2026-07-06T10:00:00Z", pending: 2093 }) }), + { cadenceWindowMs: 60 * 60 * 1000, now: "2026-07-06T14:00:00Z" } ); assert.equal(step, "stalled"); assert.equal(recoveryStateGroup(step), "systemIssue"); @@ -192,10 +192,10 @@ test("recovery: a future retry floor is a live cooldown, never a stall", () => { const step = deriveRecoveryStep( verdict(), health({ + detail_gap_backlog: backlog({ next_attempt_at: "2026-07-06T15:40:00Z", pending: 2093 }), state: "cooling_off", - detail_gap_backlog: backlog({ pending: 2093, next_attempt_at: "2026-07-06T15:40:00Z" }), }), - { now: "2026-07-06T14:00:00Z", cadenceWindowMs: 60 * 60 * 1000 } + { cadenceWindowMs: 60 * 60 * 1000, now: "2026-07-06T14:00:00Z" } ); assert.equal(step, "cooling"); }); @@ -223,10 +223,10 @@ test("panel: queued recovery carries progress floor counts and a next eligible a verdict(), health({ detail_gap_backlog: backlog({ + next_attempt_at: "2026-07-06T15:40:00Z", pending: 2093, pending_is_floor: true, recovered: 396, - next_attempt_at: "2026-07-06T15:40:00Z", }), }) ); @@ -241,8 +241,8 @@ test("panel: a cooldown blocker shows a wait/next-attempt line and no normal ret const model = buildRecoveryPanelViewModel( verdict(), health({ + detail_gap_backlog: backlog({ next_attempt_at: "2026-07-06T15:40:00Z", pending: 2093 }), state: "cooling_off", - detail_gap_backlog: backlog({ pending: 2093, next_attempt_at: "2026-07-06T15:40:00Z" }), }) ); assert.equal(model.step, "cooling"); @@ -270,8 +270,8 @@ const RAW_INTERNAL_LABEL_RE = /pending_is_floor|detail_gap|reason_code|upstream_ test("panel: eligible work with no attempt beyond the cadence window renders as a system condition, not passive progress", () => { const model = buildRecoveryPanelViewModel( verdict(), - health({ detail_gap_backlog: backlog({ pending: 2093, next_attempt_at: "2026-07-06T10:00:00Z" }) }), - { now: "2026-07-06T18:00:00Z", cadenceWindowMs: 6 * 60 * 60 * 1000 } + health({ detail_gap_backlog: backlog({ next_attempt_at: "2026-07-06T10:00:00Z", pending: 2093 }) }), + { cadenceWindowMs: 6 * 60 * 60 * 1000, now: "2026-07-06T18:00:00Z" } ); // A stale attempt floor beyond the cadence window is a stall — a system // condition surfaced with evidence, never an owner retry prompt. @@ -288,10 +288,10 @@ test("panel: a fresh attempt floor within the cadence window stays queued/coolin const model = buildRecoveryPanelViewModel( verdict(), health({ + detail_gap_backlog: backlog({ next_attempt_at: "2026-07-06T17:30:00Z", pending: 2093 }), state: "cooling_off", - detail_gap_backlog: backlog({ pending: 2093, next_attempt_at: "2026-07-06T17:30:00Z" }), }), - { now: "2026-07-06T18:00:00Z", cadenceWindowMs: 6 * 60 * 60 * 1000 } + { cadenceWindowMs: 6 * 60 * 60 * 1000, now: "2026-07-06T18:00:00Z" } ); assert.notEqual(model.step, "stalled"); }); @@ -309,15 +309,15 @@ test("panel: every recovery step exposes exactly one primary sentence, evidence buildRecoveryPanelViewModel( verdict(), health({ + detail_gap_backlog: backlog({ next_attempt_at: "2026-07-06T20:00:00Z", pending: 2093 }), state: "cooling_off", - detail_gap_backlog: backlog({ pending: 2093, next_attempt_at: "2026-07-06T20:00:00Z" }), }) ), // stalled buildRecoveryPanelViewModel( verdict(), - health({ detail_gap_backlog: backlog({ pending: 2093, next_attempt_at: "2026-07-06T10:00:00Z" }) }), - { now: "2026-07-06T18:00:00Z", cadenceWindowMs: 6 * 60 * 60 * 1000 } + health({ detail_gap_backlog: backlog({ next_attempt_at: "2026-07-06T10:00:00Z", pending: 2093 }) }), + { cadenceWindowMs: 6 * 60 * 60 * 1000, now: "2026-07-06T18:00:00Z" } ), ]; for (const model of cases) { diff --git a/apps/console/src/app/(console)/lib/source-recovery-state.ts b/apps/console/src/app/(console)/lib/source-recovery-state.ts index 831334c83..1168dd7e9 100644 --- a/apps/console/src/app/(console)/lib/source-recovery-state.ts +++ b/apps/console/src/app/(console)/lib/source-recovery-state.ts @@ -149,7 +149,7 @@ function progressCount(value: number, isFloor: boolean | null | undefined): Reco if (count === 0) { return null; } - return { value: count, isFloor: Boolean(isFloor) }; + return { isFloor: Boolean(isFloor), value: count }; } /** @@ -207,8 +207,8 @@ function readEvidence( nextAttemptAt, ownerRequired, recoverableWork: hasRecoverableWork(backlog), - systemIssue: Boolean(systemIssue), syncing: Boolean(health?.badges.syncing), + systemIssue: Boolean(systemIssue), }; } @@ -294,13 +294,13 @@ function isStalled( const STEP_GROUP: Record = { active: "working", - queued: "working", cooling: "working", eligible: "review", + none: null, owner_required: "needsOwner", - system_issue: "systemIssue", + queued: "working", stalled: "systemIssue", - none: null, + system_issue: "systemIssue", }; /** @@ -352,13 +352,13 @@ function progressEvidence(progress: RecoveryProgress): string[] { const PRIMARY_SENTENCE: Record = { active: "Syncing details now.", - queued: "Catching up details when it is safe to retry.", cooling: "Waiting until it is safe to retry details.", eligible: "Recoverable details are ready for another run.", + none: "No recovery work is queued.", owner_required: "Waiting on you before recovery can continue.", - system_issue: "This connector needs a fix before it can recover this.", + queued: "Catching up details when it is safe to retry.", stalled: "Recovery has stopped making progress and needs a look.", - none: "No recovery work is queued.", + system_issue: "This connector needs a fix before it can recover this.", }; /** @@ -382,12 +382,12 @@ export function buildRecoveryPanelViewModel( } return { - step, - primarySentence: PRIMARY_SENTENCE[step], - progress, - nextEligibleAt: step === "active" ? null : nextEligibleAt, blocker: blockerFor(step, nextEligibleAt), evidence, + nextEligibleAt: step === "active" ? null : nextEligibleAt, + primarySentence: PRIMARY_SENTENCE[step], + progress, + step, }; } diff --git a/apps/console/src/app/(console)/lib/stream-evidence-state.ts b/apps/console/src/app/(console)/lib/stream-evidence-state.ts index f7cda09a3..359ad6ef0 100644 --- a/apps/console/src/app/(console)/lib/stream-evidence-state.ts +++ b/apps/console/src/app/(console)/lib/stream-evidence-state.ts @@ -33,29 +33,29 @@ const COUNT_STATE_LABELS: Record< > = { known: (count) => ({ text: count === null ? "count unavailable" : `${count.toLocaleString()} records`, - tone: "neutral", title: "The canonical stable-records snapshot for this stream is current.", + tone: "neutral", }), known_zero: () => ({ text: "0 records", - tone: "neutral", title: "The canonical snapshot proves this stream has exactly zero retained records right now.", - }), - unobserved: () => ({ - text: "count not yet observed", tone: "neutral", - title: - "No observation has completed for this stream yet; a count will appear once the next read reconciles evidence.", }), stale: () => ({ text: "count unavailable", - tone: "warning", title: "The last known count is stale relative to the live canonical checkpoint.", + tone: "warning", }), unknown: () => ({ text: "count unavailable", - tone: "warning", title: "The count could not be determined; evidence collection failed or is otherwise unreliable.", + tone: "warning", + }), + unobserved: () => ({ + text: "count not yet observed", + title: + "No observation has completed for this stream yet; a count will appear once the next read reconciles evidence.", + tone: "neutral", }), }; @@ -73,8 +73,8 @@ export function streamCountLabel(record: { } return { text: record.record_count === null ? "count unavailable" : `${record.record_count.toLocaleString()} records`, - tone: "neutral", title: "", + tone: "neutral", }; } diff --git a/apps/console/src/app/(console)/lib/version-churn-summary.test.ts b/apps/console/src/app/(console)/lib/version-churn-summary.test.ts index 438048bfe..96e66c305 100644 --- a/apps/console/src/app/(console)/lib/version-churn-summary.test.ts +++ b/apps/console/src/app/(console)/lib/version-churn-summary.test.ts @@ -118,9 +118,9 @@ test("summarizeVersionChurn returns null for an empty row set", () => { test("summarizeVersionChurn does NOT say 'needs review' when every row is expected retained history", () => { const rows = [ - expectedRow({ connector_id: "github", stream: "user", risk_level: "high" }), - expectedRow({ connector_id: "slack", stream: "channels", risk_level: "high" }), - expectedRow({ connector_id: "ynab", stream: "accounts", risk_level: "watch" }), + expectedRow({ connector_id: "github", risk_level: "high", stream: "user" }), + expectedRow({ connector_id: "slack", risk_level: "high", stream: "channels" }), + expectedRow({ connector_id: "ynab", risk_level: "watch", stream: "accounts" }), ]; const summary = summarizeVersionChurn(rows); assert.ok(summary); @@ -134,9 +134,9 @@ test("summarizeVersionChurn does NOT say 'needs review' when every row is expect test("recurring snapshots fold into the 'expected retained history' headline bucket", () => { const rows = [ - recurringRow({ connector_id: "claude-code", stream: "sessions", risk_level: "watch" }), - recurringRow({ connector_id: "codex", stream: "sessions", risk_level: "watch" }), - expectedRow({ connector_id: "github", stream: "user", risk_level: "high" }), + recurringRow({ connector_id: "claude-code", risk_level: "watch", stream: "sessions" }), + recurringRow({ connector_id: "codex", risk_level: "watch", stream: "sessions" }), + expectedRow({ connector_id: "github", risk_level: "high", stream: "user" }), ]; const summary = summarizeVersionChurn(rows); assert.ok(summary); @@ -148,9 +148,9 @@ test("recurring snapshots fold into the 'expected retained history' headline buc test("summarizeVersionChurn does NOT say 'needs review' when every row is a registered compaction candidate", () => { const rows = [ - candidateRow({ connector_id: "ynab", stream: "budgets", risk_level: "high" }), - candidateRow({ connector_id: "gmail", stream: "labels", risk_level: "watch" }), - candidateRow({ connector_id: "amazon", stream: "orders", risk_level: "watch" }), + candidateRow({ connector_id: "ynab", risk_level: "high", stream: "budgets" }), + candidateRow({ connector_id: "gmail", risk_level: "watch", stream: "labels" }), + candidateRow({ connector_id: "amazon", risk_level: "watch", stream: "orders" }), ]; const summary = summarizeVersionChurn(rows); assert.ok(summary); @@ -162,8 +162,8 @@ test("summarizeVersionChurn does NOT say 'needs review' when every row is a regi test("summarizeVersionChurn SAYS 'needs review' when at least one row is unclassified", () => { const rows = [ - expectedRow({ connector_id: "github", stream: "user", risk_level: "high" }), - candidateRow({ connector_id: "ynab", stream: "budgets", risk_level: "high" }), + expectedRow({ connector_id: "github", risk_level: "high", stream: "user" }), + candidateRow({ connector_id: "ynab", risk_level: "high", stream: "budgets" }), unclassifiedRow({ risk_level: "high" }), ]; const summary = summarizeVersionChurn(rows); @@ -205,7 +205,7 @@ test("countChurnDispositions buckets rows by disposition", () => { candidateRow({ connector_id: "ynab", stream: "budgets" }), unclassifiedRow(), ]); - assert.deepEqual(counts, { needsReview: 1, compactionCandidates: 1, expectedRetained: 2, reviewedResidueCount: 0 }); + assert.deepEqual(counts, { compactionCandidates: 1, expectedRetained: 2, needsReview: 1, reviewedResidueCount: 0 }); }); test("countChurnDispositions folds recurring snapshots into expectedRetained", () => { @@ -214,7 +214,7 @@ test("countChurnDispositions folds recurring snapshots into expectedRetained", ( recurringRow({ connector_id: "codex", stream: "sessions" }), expectedRow({ connector_id: "github", stream: "user" }), ]); - assert.deepEqual(counts, { needsReview: 0, compactionCandidates: 0, expectedRetained: 3, reviewedResidueCount: 0 }); + assert.deepEqual(counts, { compactionCandidates: 0, expectedRetained: 3, needsReview: 0, reviewedResidueCount: 0 }); }); test("isExpectedRetainedHistory / needsReview predicates agree with the disposition", () => { @@ -230,7 +230,7 @@ test("isExpectedRetainedHistory / needsReview predicates agree with the disposit }); test("churnRowLabel falls back to the connector key when no display name is set", () => { - assert.equal(churnRowLabel(row({ display_name: null, connector_id: "ynab" })), "ynab / budgets"); + assert.equal(churnRowLabel(row({ connector_id: "ynab", display_name: null })), "ynab / budgets"); }); test("churnRowLabel prefers an owner-set display name", () => { @@ -240,7 +240,7 @@ test("churnRowLabel prefers an owner-set display name", () => { // ─── buildChurnDrilldownRows ───────────────────────────────────────────────── test("buildChurnDrilldownRows surfaces all supplied rows in order", () => { - const rows = [candidateRow({ stream: "budgets" }), candidateRow({ stream: "accounts", risk_level: "watch" })]; + const rows = [candidateRow({ stream: "budgets" }), candidateRow({ risk_level: "watch", stream: "accounts" })]; const built = buildChurnDrilldownRows(rows); assert.equal(built.length, 2); assert.equal(built[0]?.label, "ynab / budgets"); @@ -314,7 +314,7 @@ test("churnDryRunCommand shell-quotes metadata and omits absent connector id", ( test("buildChurnDrilldownRows omits the dry-run command for point-in-time rows and carries guidance", () => { const built = buildChurnDrilldownRows([ - expectedRow({ connector_id: "github", stream: "user", connector_instance_id: "cin_gh_1" }), + expectedRow({ connector_id: "github", connector_instance_id: "cin_gh_1", stream: "user" }), ]); assert.equal(built[0]?.remediation, "point_in_time_retained_history"); assert.equal(built[0]?.dryRunCommand, null, "point-in-time rows must not offer a (failing) compaction command"); @@ -327,7 +327,7 @@ test("buildChurnDrilldownRows omits the dry-run command for point-in-time rows a test("buildChurnDrilldownRows omits the dry-run command for recurring snapshots and carries guidance", () => { const built = buildChurnDrilldownRows([ - recurringRow({ connector_id: "claude-code", stream: "sessions", connector_instance_id: "cin_cc_1" }), + recurringRow({ connector_id: "claude-code", connector_instance_id: "cin_cc_1", stream: "sessions" }), ]); assert.equal(built[0]?.remediation, "recurring_point_in_time_snapshot"); assert.equal(built[0]?.dryRunCommand, null, "recurring snapshots are not compactable — no command"); @@ -352,7 +352,7 @@ test("buildChurnDrilldownRows keeps the dry-run command (as a diagnostic) for un test("buildChurnDrilldownRows keeps the dry-run command for reviewed residue rows", () => { const built = buildChurnDrilldownRows([ - reviewedRow({ connector_id: "usaa", stream: "accounts", connector_instance_id: "cin_usaa_1" }), + reviewedRow({ connector_id: "usaa", connector_instance_id: "cin_usaa_1", stream: "accounts" }), ]); assert.equal(built[0]?.remediation, "reviewed_historical_residue"); assert.ok(built[0]?.dryRunCommand, "reviewed residue rows keep the dry-run command (--apply frees disk)"); @@ -386,10 +386,10 @@ test("pointInTimeGuidance describes recurring snapshots as expected non-compacta test("summarizeVersionChurn names reviewed residue streams and says no review needed when all classified", () => { const rows = [ - reviewedRow({ connector_id: "usaa", stream: "accounts", risk_level: "watch" }), - reviewedRow({ connector_id: "usaa", stream: "statements", risk_level: "watch" }), - reviewedRow({ connector_id: "chase", stream: "statements", risk_level: "watch" }), - recurringRow({ connector_id: "claude-code", stream: "sessions", risk_level: "watch" }), + reviewedRow({ connector_id: "usaa", risk_level: "watch", stream: "accounts" }), + reviewedRow({ connector_id: "usaa", risk_level: "watch", stream: "statements" }), + reviewedRow({ connector_id: "chase", risk_level: "watch", stream: "statements" }), + recurringRow({ connector_id: "claude-code", risk_level: "watch", stream: "sessions" }), ]; const summary = summarizeVersionChurn(rows); assert.ok(summary); @@ -406,9 +406,9 @@ test("summarizeVersionChurn names reviewed residue streams and says no review ne test("summarizeVersionChurn includes reviewed residue count even alongside other dispositions", () => { const rows = [ - reviewedRow({ connector_id: "usaa", stream: "accounts", risk_level: "watch" }), - candidateRow({ connector_id: "ynab", stream: "budgets", risk_level: "high" }), - expectedRow({ connector_id: "github", stream: "user", risk_level: "high" }), + reviewedRow({ connector_id: "usaa", risk_level: "watch", stream: "accounts" }), + candidateRow({ connector_id: "ynab", risk_level: "high", stream: "budgets" }), + expectedRow({ connector_id: "github", risk_level: "high", stream: "user" }), ]; const summary = summarizeVersionChurn(rows); assert.ok(summary); diff --git a/apps/console/src/app/(console)/lib/version-churn-summary.ts b/apps/console/src/app/(console)/lib/version-churn-summary.ts index b0c67ecf6..857badc98 100644 --- a/apps/console/src/app/(console)/lib/version-churn-summary.ts +++ b/apps/console/src/app/(console)/lib/version-churn-summary.ts @@ -285,7 +285,7 @@ export function countChurnDispositions(rows: readonly RefRecordVersionStatsRow[] break; } } - return { needsReview: needsReviewCount, compactionCandidates, expectedRetained, reviewedResidueCount }; + return { compactionCandidates, expectedRetained, needsReview: needsReviewCount, reviewedResidueCount }; } function pluralStreams(n: number): string { @@ -355,10 +355,10 @@ export function summarizeVersionChurn(rows: readonly RefRecordVersionStatsRow[]) : `Version churn is classified — no review needed: ${breakdown}.`; return { + dispositions, headline, highestSignal: `Highest signal: ${churnRowLabel(strongest)} retains ${strongest.versions_per_record.toLocaleString()} versions per current record.`, needsReview: dispositions.needsReview > 0, - dispositions, }; } @@ -461,14 +461,7 @@ export function buildChurnDrilldownRows(rows: readonly RefRecordVersionStatsRow[ return { connectorId: row.connector_id, connectorInstanceId: row.connector_instance_id, - key: `${row.connector_instance_id}:${row.stream}`, - label: churnRowLabel(row), - risk: row.risk_level, - stream: row.stream, - remediation, - remediationAction: remediationForRow(row), - remediationChip: remediationChipLabel(row), - remediationGuidance: remediationGuidance(row), + current: countCell(row.current_record_count), // Non-compactable rows have no compaction policy the script resolves (it // exits 2), so they carry redesign/expected-history guidance instead of a // command. Compaction candidates, reviewed residue, and unclassified rows @@ -476,15 +469,22 @@ export function buildChurnDrilldownRows(rows: readonly RefRecordVersionStatsRow[ // plan, for reviewed residue `--apply` frees disk, and for an unclassified // row it is the safe first diagnostic (it prints whether a policy exists). dryRunCommand: notCompactable ? null : churnDryRunCommand(row), - pointInTimeGuidance: notCompactable ? pointInTimeGuidance(row) : null, - versionsPerRecord: { - label: row.versions_per_record.toLocaleString(undefined, { maximumFractionDigits: 2 }), - }, - current: countCell(row.current_record_count), history: countCell(row.record_history_count), + key: `${row.connector_instance_id}:${row.stream}`, keys: countCell(row.record_key_count), + label: churnRowLabel(row), lastHistoryAt: row.last_history_at, + pointInTimeGuidance: notCompactable ? pointInTimeGuidance(row) : null, reasons: row.risk_reasons.length > 0 ? row.risk_reasons.join("; ") : null, + remediation, + remediationAction: remediationForRow(row), + remediationChip: remediationChipLabel(row), + remediationGuidance: remediationGuidance(row), + risk: row.risk_level, + stream: row.stream, + versionsPerRecord: { + label: row.versions_per_record.toLocaleString(undefined, { maximumFractionDigits: 2 }), + }, }; }); } diff --git a/apps/console/src/app/(console)/page.tsx b/apps/console/src/app/(console)/page.tsx index 9238eb057..224b916bd 100644 --- a/apps/console/src/app/(console)/page.tsx +++ b/apps/console/src/app/(console)/page.tsx @@ -48,18 +48,18 @@ export const dynamic = "force-dynamic"; const SCHEME_RE = /^https?:\/\//; const HREFS: StandingHrefs = { - grants: dashboardRoutes.section.grants, - grantPackages: `${dashboardRoutes.section.grants}/packages`, - runs: dashboardRoutes.section.runs, - sources: dashboardRoutes.section.records, - traces: dashboardRoutes.section.traces, + connection: (connectorKey) => dashboardRoutes.connector(connectorKey), deployment: dashboardRoutes.section.deployment, deploymentTokens: dashboardRoutes.section.deploymentTokens, - notifications: dashboardRoutes.section.notifications, - connection: (connectorKey) => dashboardRoutes.connector(connectorKey), grant: (id) => dashboardRoutes.grant(id), + grantPackages: `${dashboardRoutes.section.grants}/packages`, + grants: dashboardRoutes.section.grants, + notifications: dashboardRoutes.section.notifications, run: (id) => dashboardRoutes.run(id), + runs: dashboardRoutes.section.runs, + sources: dashboardRoutes.section.records, trace: (id) => dashboardRoutes.trace(id), + traces: dashboardRoutes.section.traces, }; interface SafeRead { @@ -119,21 +119,21 @@ async function loadStandingInputs(): Promise { const connectors = connectorsRes.value.data; return { - now: new Date(), - hrefs: HREFS, - summary: summary.value, - grants: grantsRes.value.data, - grantPackageCount: packageCountRes.value.count, - traces: tracesRes.value.data, - failedTraces: [], + advisoryOwnerActions: advisoryOwnerActionsFromConnectors(connectors), + attentionConnections: attentionConnectionsFromConnectors(connectors), + bearerClients: clientsRes.value.data, failedRuns: [], + failedTraces: [], + grantPackageCount: packageCountRes.value.count, + grants: grantsRes.value.data, + hrefs: HREFS, + now: new Date(), overviewLoadIssues, pendingApprovals: pendingRes.value.data, - bearerClients: clientsRes.value.data, - sourceWork: sourceWorkFromConnectors(connectors), - advisoryOwnerActions: advisoryOwnerActionsFromConnectors(connectors), - attentionConnections: attentionConnectionsFromConnectors(connectors), sourceIssues: sourceIssueConnectionsFromConnectors(connectors), + sourceWork: sourceWorkFromConnectors(connectors), + summary: summary.value, + traces: tracesRes.value.data, }; } diff --git a/apps/console/src/app/(console)/schedules/actions.ts b/apps/console/src/app/(console)/schedules/actions.ts index fba0460cc..4c1aa81a5 100644 --- a/apps/console/src/app/(console)/schedules/actions.ts +++ b/apps/console/src/app/(console)/schedules/actions.ts @@ -31,7 +31,7 @@ export async function upsertScheduleAction( revalidatePath("/sources"); return { ok: true, policy_warning: body?.policy_warning ?? null }; } catch (err) { - return { ok: false, message: err instanceof Error ? err.message : String(err) }; + return { message: err instanceof Error ? err.message : String(err), ok: false }; } } @@ -45,7 +45,7 @@ export async function pauseScheduleAction( revalidatePath("/sources"); return { ok: true }; } catch (err) { - return { ok: false, message: err instanceof Error ? err.message : String(err) }; + return { message: err instanceof Error ? err.message : String(err), ok: false }; } } @@ -59,7 +59,7 @@ export async function resumeScheduleAction( revalidatePath("/sources"); return { ok: true }; } catch (err) { - return { ok: false, message: err instanceof Error ? err.message : String(err) }; + return { message: err instanceof Error ? err.message : String(err), ok: false }; } } @@ -73,6 +73,6 @@ export async function deleteScheduleAction( revalidatePath("/sources"); return { ok: true }; } catch (err) { - return { ok: false, message: err instanceof Error ? err.message : String(err) }; + return { message: err instanceof Error ? err.message : String(err), ok: false }; } } diff --git a/apps/console/src/app/(console)/schedules/schedule-row.tsx b/apps/console/src/app/(console)/schedules/schedule-row.tsx index ec52ab816..78d30b6f2 100644 --- a/apps/console/src/app/(console)/schedules/schedule-row.tsx +++ b/apps/console/src/app/(console)/schedules/schedule-row.tsx @@ -110,9 +110,9 @@ export function ScheduleRow({ summary, runsHref }: ScheduleRowProps) { startTransition(async () => { const res = await upsertScheduleAction(summary.connector_id, { connectionId: summary.connection_id ?? summary.connector_instance_id ?? null, + enabled: true, every, jitter: jitter || undefined, - enabled: true, }); if (!res.ok) { showToast("error", res.message); diff --git a/apps/console/src/app/(console)/search/page.tsx b/apps/console/src/app/(console)/search/page.tsx index 50beeed36..e7fcf8180 100644 --- a/apps/console/src/app/(console)/search/page.tsx +++ b/apps/console/src/app/(console)/search/page.tsx @@ -57,9 +57,9 @@ async function loadSearchResult(query: string, jump: string | undefined): Promis return { result: { exact: spineResult.exact, - traces: spineResult.traces, grants: spineResult.grants, runs: spineResult.runs, + traces: spineResult.traces, }, unreachable: false, }; diff --git a/apps/console/src/app/(console)/sources/[connector]/[stream]/[recordKey]/page.tsx b/apps/console/src/app/(console)/sources/[connector]/[stream]/[recordKey]/page.tsx index 65428374c..1101c1115 100644 --- a/apps/console/src/app/(console)/sources/[connector]/[stream]/[recordKey]/page.tsx +++ b/apps/console/src/app/(console)/sources/[connector]/[stream]/[recordKey]/page.tsx @@ -106,8 +106,8 @@ export default async function RecordDetailPage({ connectorInstanceId, }).catch(() => null); return { - parentStream, expandCapabilities: Array.isArray(metadata?.expand_capabilities) ? metadata.expand_capabilities : [], + parentStream, }; }) ); @@ -209,7 +209,7 @@ export default async function RecordDetailPage({ ); const reverseChildListLinks = reverseChildListLinksFromManifest( connectorStreams, - { connectionId, parentStream: streamName, parentRecordKey: record.id }, + { connectionId, parentRecordKey: record.id, parentStream: streamName }, forwardChildListKeys ); // Child → parent back-links from two sources: @@ -228,9 +228,9 @@ export default async function RecordDetailPage({ // chase current_activity `amount: 3000` (declared `currency`) must read // `$30.00`, never the raw `3000`. const rendered = renderValue(3000, "currency"); - assert.deepEqual(rendered, { text: "$30.00", empty: false, money: true }); + assert.deepEqual(rendered, { empty: false, money: true, text: "$30.00" }); assert.ok(valueClassName(rendered).includes("tabular-nums")); }); @@ -38,12 +38,12 @@ test("an undeclared integer is NOT reinterpreted as cents", () => { // Without a declared currency type the value is shown verbatim; the detail // table never guesses a unit from magnitude. const rendered = renderValue(3000, undefined); - assert.deepEqual(rendered, { text: "3000", empty: false, money: false }); + assert.deepEqual(rendered, { empty: false, money: false, text: "3000" }); }); test("a plain string renders verbatim and is not marked empty or money", () => { const rendered = renderValue("posted", "text"); - assert.deepEqual(rendered, { text: "posted", empty: false, money: false }); + assert.deepEqual(rendered, { empty: false, money: false, text: "posted" }); assert.equal(valueClassName(rendered), "pdpp-caption break-words"); }); diff --git a/apps/console/src/app/(console)/sources/[connector]/[stream]/[recordKey]/record-fields-display.ts b/apps/console/src/app/(console)/sources/[connector]/[stream]/[recordKey]/record-fields-display.ts index a248c7f3f..5e4bfd76f 100644 --- a/apps/console/src/app/(console)/sources/[connector]/[stream]/[recordKey]/record-fields-display.ts +++ b/apps/console/src/app/(console)/sources/[connector]/[stream]/[recordKey]/record-fields-display.ts @@ -52,16 +52,16 @@ export interface RenderedValue { */ export function renderValue(value: unknown, declaredType: string | undefined): RenderedValue { if (value === null || value === undefined) { - return { text: value === null ? "null" : "—", empty: true, money: false }; + return { empty: true, money: false, text: value === null ? "null" : "—" }; } const amount = formatDeclaredAmount(value, declaredType); if (amount) { - return { text: amount.text, empty: false, money: true }; + return { empty: false, money: true, text: amount.text }; } if (typeof value === "string" && value.length === 0) { - return { text: "empty", empty: true, money: false }; + return { empty: true, money: false, text: "empty" }; } - return { text: stringifyValue(value), empty: false, money: false }; + return { empty: false, money: false, text: stringifyValue(value) }; } /** Tailwind classes for a value cell, tinting empties and aligning money. */ diff --git a/apps/console/src/app/(console)/sources/[connector]/[stream]/health/page.tsx b/apps/console/src/app/(console)/sources/[connector]/[stream]/health/page.tsx index e6113b883..ee2e726e0 100644 --- a/apps/console/src/app/(console)/sources/[connector]/[stream]/health/page.tsx +++ b/apps/console/src/app/(console)/sources/[connector]/[stream]/health/page.tsx @@ -93,9 +93,9 @@ function StreamHealthReport({ null); return { - parentStream, expandCapabilities: Array.isArray(metadata?.expand_capabilities) ? metadata.expand_capabilities : [], + parentStream, }; }) ); @@ -285,8 +285,8 @@ export default async function StreamPage({ {streamName}} @@ -405,8 +405,8 @@ export default async function StreamPage({ hasReverseChildEdges ? reverseChildListLinksFromManifest(connectorStreams, { connectionId, - parentStream: streamName, parentRecordKey: record.id, + parentStream: streamName, }) : []; @@ -423,14 +423,14 @@ export default async function StreamPage({ selectedColumns={columns} /> )} - + Stream health → } breadcrumbs={[ - { label: "Sources", href: "/sources" }, - { label: sourceLabel, href: `/sources/${encodeURIComponent(connectionId)}` }, + { href: "/sources", label: "Sources" }, + { href: `/sources/${encodeURIComponent(connectionId)}`, label: sourceLabel }, { label: streamName }, ]} count={headerCount} @@ -659,11 +659,11 @@ function latestStreamRunEvidence( const href = latestRun ? `/syncs/${encodeURIComponent(latestRun.run_id)}` : null; if (!facts) { return { - href, - value: latestRun ? "stream report unavailable" : "not seen yet", detail: latestRun ? "The latest source run did not include stream-level collection facts for this stream." : "No attributed source run has reached this dashboard yet.", + href, + value: latestRun ? "stream report unavailable" : "not seen yet", }; } const detailParts = [ @@ -674,9 +674,9 @@ function latestStreamRunEvidence( facts.skipLabel, ].filter((part): part is string => part !== null); return { + detail: detailParts.join(" · "), href, value: facts.countsLabel ?? facts.coverage.label, - detail: detailParts.join(" · "), }; } diff --git a/apps/console/src/app/(console)/sources/[connector]/[stream]/record-list-filter-roundtrip.test.ts b/apps/console/src/app/(console)/sources/[connector]/[stream]/record-list-filter-roundtrip.test.ts index 1a23f182b..7563763a3 100644 --- a/apps/console/src/app/(console)/sources/[connector]/[stream]/record-list-filter-roundtrip.test.ts +++ b/apps/console/src/app/(console)/sources/[connector]/[stream]/record-list-filter-roundtrip.test.ts @@ -96,8 +96,8 @@ test("BEHAVIOR a non-filter search param (cursors/columns) is ignored", () => { // The page also receives pagination/column params; only `filter[...]` keys feed // the exact-filter map. Mixing them must not fabricate a filter. const filters = readExactFiltersReplica({ - cursors: "abc,def", columns: "amount,description", + cursors: "abc,def", "filter[account_id]": "acc1", }); assert.deepEqual(filters, { account_id: "acc1" }, "only filter[...] keys contribute; cursors/columns are ignored"); diff --git a/apps/console/src/app/(console)/sources/[connector]/[stream]/relationship-navigation-smoke.test.ts b/apps/console/src/app/(console)/sources/[connector]/[stream]/relationship-navigation-smoke.test.ts index fda45122e..1cca37114 100644 --- a/apps/console/src/app/(console)/sources/[connector]/[stream]/relationship-navigation-smoke.test.ts +++ b/apps/console/src/app/(console)/sources/[connector]/[stream]/relationship-navigation-smoke.test.ts @@ -116,7 +116,7 @@ test("DIRECTION 1 transaction -> account: detail-page chain yields the parent ac // (parentStream, field). const candidates = candidateParentStreamsForChild(connectorManifest.streams, streamName); const parentRelations = parentRelationsForChild( - candidates.map((parentStream) => ({ parentStream, expandCapabilities: [] })), + candidates.map((parentStream) => ({ expandCapabilities: [], parentStream })), streamName ); const parentBackLinkFromMeta = findParentBackLink(streamName, recordData, parentRelations, { @@ -153,8 +153,8 @@ test("DIRECTION 2 account -> transactions: list-page chain yields the filtered c const reverseLinks = reverseChildListLinksFromManifest(connectorStreams, { connectionId: CONNECTION_ID, - parentStream, parentRecordKey: ACCOUNT_KEY, + parentStream, }); assert.equal(reverseLinks.length, edges.length, "one reverse link per declared child has_one edge"); @@ -182,8 +182,8 @@ test("ROUND-TRIP: the account key is the shared join value both directions use", )[0]; const down = reverseChildListLinksFromManifest(connectorStreams, { connectionId: CONNECTION_ID, - parentStream: "accounts", parentRecordKey: ACCOUNT_KEY, + parentStream: "accounts", }).find((l) => l.childStream === "transactions"); assert.ok(up?.href.endsWith(`/accounts/${ACCOUNT_KEY}`), "ascending lands on exactly that account"); diff --git a/apps/console/src/app/(console)/sources/[connector]/actions.ts b/apps/console/src/app/(console)/sources/[connector]/actions.ts index 95ef5e5ef..bbf6b2ee5 100644 --- a/apps/console/src/app/(console)/sources/[connector]/actions.ts +++ b/apps/console/src/app/(console)/sources/[connector]/actions.ts @@ -88,21 +88,21 @@ export async function renameConnectionAction( await requireDashboardAccess(connectorHref(connectionId ?? "")); const trimmed = displayName.trim(); if (!connectionId) { - return { ok: false, message: "This connector has no addressable connection to rename yet." }; + return { message: "This connector has no addressable connection to rename yet.", ok: false }; } if (!trimmed) { - return { ok: false, message: "Enter a label before saving." }; + return { message: "Enter a label before saving.", ok: false }; } if (trimmed.length > MAX_DISPLAY_NAME_LENGTH) { - return { ok: false, message: `Label is too long (max ${MAX_DISPLAY_NAME_LENGTH} characters).` }; + return { message: `Label is too long (max ${MAX_DISPLAY_NAME_LENGTH} characters).`, ok: false }; } try { await setConnectionDisplayName(connectionId, trimmed); revalidatePath("/sources"); revalidatePath(`/sources/${encodeURIComponent(connectionId)}`); - return { ok: true, display_name: trimmed }; + return { display_name: trimmed, ok: true }; } catch (err) { - return { ok: false, message: errorMessage(err) }; + return { message: errorMessage(err), ok: false }; } } @@ -119,8 +119,8 @@ export async function saveConnectorScheduleAction(formData: FormData) { let error: string | undefined; try { await (connectionId - ? saveConnectionSchedule(connectionId, { every, jitter, enabled }) - : saveConnectorSchedule(connectorId, { every, jitter, enabled })); + ? saveConnectionSchedule(connectionId, { enabled, every, jitter }) + : saveConnectorSchedule(connectorId, { enabled, every, jitter })); message = enabled ? "Schedule saved and enabled" : "Schedule saved as paused"; } catch (err) { error = errorMessage(err); diff --git a/apps/console/src/app/(console)/sources/[connector]/connection-diagnostics.tsx b/apps/console/src/app/(console)/sources/[connector]/connection-diagnostics.tsx index a256346a2..3a3eb4ecd 100644 --- a/apps/console/src/app/(console)/sources/[connector]/connection-diagnostics.tsx +++ b/apps/console/src/app/(console)/sources/[connector]/connection-diagnostics.tsx @@ -1003,7 +1003,6 @@ function OutboxStallRemediationPanel({ // fully resolved by the safe CLI builders). const steps: { caption: string; command: string | null; label: string }[] = verdictRemediation ? verdictRemediation.commands.map((command) => ({ - label: command.label, caption: remediationCommandCaption(command), command: substituteCommandTemplate(command.command_template, { connectionId, @@ -1011,23 +1010,24 @@ function OutboxStallRemediationPanel({ providerUrl: providerOrigin, sourceInstanceId, }), + label: command.label, })) : [ { - label: "1. Diagnose", caption: "See saved upload rows and local upload health.", command: pdppLocalCollectorDoctorCommand(scope), + label: "1. Diagnose", }, { - label: "2. Preview the requeue", caption: "Dry run — shows what would be requeued, changes nothing.", command: pdppLocalCollectorRetryDeadLettersCommand(scope), + label: "2. Preview the requeue", }, { - label: "3. Requeue", caption: "Marks failed uploads for another attempt after backing up the local database first. The next collector run drains them — it does not ingest on its own.", command: pdppLocalCollectorRetryDeadLettersCommand({ ...scope, apply: true }), + label: "3. Requeue", }, ]; // Name the host when we know it; otherwise keep the honest generic phrasing. diff --git a/apps/console/src/app/(console)/sources/[connector]/page.tsx b/apps/console/src/app/(console)/sources/[connector]/page.tsx index 897beaa3d..75c7b10fb 100644 --- a/apps/console/src/app/(console)/sources/[connector]/page.tsx +++ b/apps/console/src/app/(console)/sources/[connector]/page.tsx @@ -166,13 +166,13 @@ function toConnectorRunRef(summary: RefConnectorRunSummary | null) { return null; } return { - run_id: summary.run_id, - first_at: summary.first_at, - last_at: summary.last_at, event_count: summary.event_count, - status: summary.status, failure_reason: summary.failure_reason, + first_at: summary.first_at, known_gaps: summary.known_gaps ?? [], + last_at: summary.last_at, + run_id: summary.run_id, + status: summary.status, }; } @@ -240,17 +240,17 @@ function toConnectorOverview(summary: RefConnectorSummary, streams: StreamSummar }, connectorDisplayName: summary.connector_display_name, connectorInstanceId: summary.connector_instance_id ?? summary.connection_id, - streams, - streamCount: summary.stream_count, + isRunning: lastRun != null && isActiveConnectorRunSummaryStatus(lastRun.status), + lastRun, + lastSuccessfulRun, + localDeviceProgress: summary.local_device_progress ?? null, retainedBytes: summary.retained_bytes ?? null, revokedAt: summary.revoked_at ?? null, - totalRetainedBytes: summary.total_retained_bytes, + streamCount: summary.stream_count, + streams, totalRecords: summary.total_records, totalRecordsState: summary.total_records_state, - localDeviceProgress: summary.local_device_progress ?? null, - lastRun, - lastSuccessfulRun, - isRunning: lastRun != null && isActiveConnectorRunSummaryStatus(lastRun.status), + totalRetainedBytes: summary.total_retained_bytes, }; } @@ -266,6 +266,9 @@ function streamsFromConnectorSummary(summary: RefConnectorSummary): StreamSummar orderedNames.add(name); const record = recordsByStream.get(name); streams.push({ + count_state: record?.count_state, + declaration_state: record?.declaration_state, + last_updated: record?.last_updated ?? null, name, object: "stream", // An absent retained-size row is NOT zero: the server synthesizes @@ -275,9 +278,6 @@ function streamsFromConnectorSummary(summary: RefConnectorSummary): StreamSummar // (count_state: "unobserved"/"stale"/"unknown") — never coerced to a // fabricated 0 either. record_count: record ? record.record_count : null, - last_updated: record?.last_updated ?? null, - declaration_state: record?.declaration_state, - count_state: record?.count_state, }); }; @@ -418,16 +418,16 @@ async function loadConnectorPageModel(routeId: string): Promise } - breadcrumbs={[{ label: "Sources", href: "/sources" }, { label: displayName }]} + breadcrumbs={[{ href: "/sources", label: "Sources" }, { label: displayName }]} count={headerCount} description={ {activeRunHref ? ( - + Active sync → ) : null} - + All syncs → {/* Update credential: visible on static-secret connections so the owner can @@ -771,7 +771,7 @@ function ConnectorHeaderActions({ declared for the connector. */} {storedCredentialUpdateHref && !revoked && hasStaticSecretCredentialUpdate ? ( @@ -836,7 +836,7 @@ function ConnectorPrimaryHeaderAction({ if (revoked) { return ( @@ -863,7 +863,7 @@ function ConnectorPrimaryHeaderAction({ return ( <> @@ -974,7 +974,7 @@ function RenderedVerdictHeaderAction({ }); return ( @@ -1119,7 +1119,7 @@ function AcquisitionBatchRow({ ) : null}
Open receipt @@ -1378,7 +1378,7 @@ function RevokedConnectionSection({ connectorId, revokedAt }: { connectorId: str title="Revoked connection" > Reconnect source diff --git a/apps/console/src/app/(console)/sources/[connector]/rename-connection.tsx b/apps/console/src/app/(console)/sources/[connector]/rename-connection.tsx index 9b83fc0f6..85b1ffe3f 100644 --- a/apps/console/src/app/(console)/sources/[connector]/rename-connection.tsx +++ b/apps/console/src/app/(console)/sources/[connector]/rename-connection.tsx @@ -73,11 +73,11 @@ export function RenameConnection({ connectionId, currentLabel, typeName }: Props const res: RenameConnectionResult = await renameConnectionAction(connectionId, value); if (res.ok === true) { setEditing(false); - setToast({ tone: "info", message: `Renamed to "${res.display_name}"` }); + setToast({ message: `Renamed to "${res.display_name}"`, tone: "info" }); router.refresh(); return; } - setToast({ tone: "error", message: res.message }); + setToast({ message: res.message, tone: "error" }); }); }, [connectionId, router, value]); diff --git a/apps/console/src/app/(console)/sources/[connector]/source-detail-demo-data.ts b/apps/console/src/app/(console)/sources/[connector]/source-detail-demo-data.ts index 3f57ec88a..66ec25d2a 100644 --- a/apps/console/src/app/(console)/sources/[connector]/source-detail-demo-data.ts +++ b/apps/console/src/app/(console)/sources/[connector]/source-detail-demo-data.ts @@ -243,15 +243,15 @@ export function buildRecoveryDemoModel(): ConnectorPageModel { records_pending: 3, source_count: 1, }, - streams, streamCount: streams.length, + streams, totalRecords: 464, totalRetainedBytes: null, }; return { + activeRunId: null, collectionFactsByStream, collectionOwnerActionByStream: { messages: true, sessions: true }, - activeRunId: null, connectionHealth, connectionId, connectionLabelSeed: "Claude Code workstation", @@ -260,7 +260,6 @@ export function buildRecoveryDemoModel(): ConnectorPageModel { connectorId: "claude_code", connectorInstanceId: connectionId, deviceLabels: ["workstation"], - sourceBindingKind: null, displayName: "Claude Code workstation", headerCount: "464 records, 2 streams", manifest: { @@ -275,6 +274,7 @@ export function buildRecoveryDemoModel(): ConnectorPageModel { recentRuns: [run], schedule: null, scheduleError: null, + sourceBindingKind: null, sourceInstances, sourceInstancesError: null, streams, diff --git a/apps/console/src/app/(console)/sources/actions.ts b/apps/console/src/app/(console)/sources/actions.ts index 31be08c55..fff1c07d7 100644 --- a/apps/console/src/app/(console)/sources/actions.ts +++ b/apps/console/src/app/(console)/sources/actions.ts @@ -69,12 +69,12 @@ export async function runConnectorNowAction( // give a deployment-status / retry hint instead of a raw network string. if (err instanceof ReferenceServerUnreachableError) { return { + message: + "Couldn't reach the reference server, so the sync was not started. Check the deployment is running, then retry.", ok: false, - reason: "error", phase: "before_server", reached_server: false, - message: - "Couldn't reach the reference server, so the sync was not started. Check the deployment is running, then retry.", + reason: "error", }; } const message = err instanceof Error ? err.message : String(err); @@ -83,16 +83,16 @@ export async function runConnectorNowAction( if (ALREADY_ACTIVE_RE.test(message) || RUN_ALREADY_ACTIVE_RE.test(message)) { const match = message.match(RUN_ID_MATCH_RE); return { + message: "Sync already in progress.", ok: false, reason: "already_running", run_id: match?.[0], - message: "Sync already in progress.", }; } // Everything after the transport-failure branch came from the reference // server. Keep the server envelope text and mark it separately from a // before-server failure so the row can stay local and tell the owner where // the failure occurred. - return { ok: false, reason: "error", phase: "after_server", reached_server: true, message }; + return { message, ok: false, phase: "after_server", reached_server: true, reason: "error" }; } } diff --git a/apps/console/src/app/(console)/sources/error.tsx b/apps/console/src/app/(console)/sources/error.tsx index d4a17168f..bd456b0e1 100644 --- a/apps/console/src/app/(console)/sources/error.tsx +++ b/apps/console/src/app/(console)/sources/error.tsx @@ -109,7 +109,7 @@ export default function RecordsError({ error, reset }: { error: Error & { digest

- + Reload Sources
diff --git a/apps/console/src/app/(console)/sources/last-known-read.test.ts b/apps/console/src/app/(console)/sources/last-known-read.test.ts index 576acc3f3..6149a7e50 100644 --- a/apps/console/src/app/(console)/sources/last-known-read.test.ts +++ b/apps/console/src/app/(console)/sources/last-known-read.test.ts @@ -26,12 +26,12 @@ function withFakeSessionStorage(run: (store: Map) => void): void const fakeWindow = { sessionStorage: { getItem: (k: string) => (store.has(k) ? (store.get(k) as string) : null), - setItem: (k: string, v: string) => { - store.set(k, v); - }, removeItem: (k: string) => { store.delete(k); }, + setItem: (k: string, v: string) => { + store.set(k, v); + }, }, }; const g = globalThis as unknown as { window?: unknown }; diff --git a/apps/console/src/app/(console)/sources/last-known-sync-start.test.ts b/apps/console/src/app/(console)/sources/last-known-sync-start.test.ts index b82d31d96..546a8f000 100644 --- a/apps/console/src/app/(console)/sources/last-known-sync-start.test.ts +++ b/apps/console/src/app/(console)/sources/last-known-sync-start.test.ts @@ -20,12 +20,12 @@ function withFakeSessionStorage(run: (store: Map) => void): void const fakeWindow = { sessionStorage: { getItem: (k: string) => (store.has(k) ? (store.get(k) as string) : null), - setItem: (k: string, v: string) => { - store.set(k, v); - }, removeItem: (k: string) => { store.delete(k); }, + setItem: (k: string, v: string) => { + store.set(k, v); + }, }, }; const g = globalThis as unknown as { window?: unknown }; @@ -74,7 +74,7 @@ test("sync-start marker round-trips while unexpired", () => { test("expired or corrupt sync-start markers are cleared", () => { withFakeSessionStorage((store) => { const key = syncStartToastKey("source-a"); - store.set(key, JSON.stringify({ message: "Sync started.", tone: "info", expiresAt: 1 })); + store.set(key, JSON.stringify({ expiresAt: 1, message: "Sync started.", tone: "info" })); assert.equal(readSyncStartToast("source-a"), null); store.set(key, "not-json"); assert.equal(readSyncStartToast("source-a"), null); diff --git a/apps/console/src/app/(console)/sources/lib/relationships.test.ts b/apps/console/src/app/(console)/sources/lib/relationships.test.ts index a491c4cbd..6ff88d6de 100644 --- a/apps/console/src/app/(console)/sources/lib/relationships.test.ts +++ b/apps/console/src/app/(console)/sources/lib/relationships.test.ts @@ -24,13 +24,13 @@ import { } from "./relationships.ts"; const USER_STATS_CAP: ExpandCapability = { - name: "user_stats", - stream: "user_stats", - target_stream: "user_stats", cardinality: "has_many", child_parent_key_field: "user_id", foreign_key: "user_id", granted: true, + name: "user_stats", + stream: "user_stats", + target_stream: "user_stats", usable: true, }; @@ -53,7 +53,7 @@ test("has_many usable relation links to the filtered child list, not a child det }); test("has_many link percent-encodes connection, stream, field, and parent key", () => { - const cap: ExpandCapability = { ...USER_STATS_CAP, target_stream: "user stats", child_parent_key_field: "user id" }; + const cap: ExpandCapability = { ...USER_STATS_CAP, child_parent_key_field: "user id", target_stream: "user stats" }; const links = buildRelatedLinks([cap], { connectionId: "git hub", parentRecordKey: "1/0 1" }); const link = links[0]; assert.ok(link); @@ -64,8 +64,8 @@ test("unusable relation renders inert with the manifest reason as advisory", () const cap: ExpandCapability = { ...USER_STATS_CAP, granted: false, - usable: false, reason: "related_stream_not_granted", + usable: false, }; const link = onlyLink([cap], "101"); assert.equal(link.navigable, false); @@ -82,13 +82,13 @@ test("each declared reason maps to calm advisory copy", () => { test("usable has_one without a resolvable child key renders inert, never a parent-key child URL", () => { const cap: ExpandCapability = { - name: "profile", - target_stream: "profile", - stream: "profile", cardinality: "has_one", child_parent_key_field: "user_id", foreign_key: "user_id", granted: true, + name: "profile", + stream: "profile", + target_stream: "profile", usable: true, }; const link = onlyLink([cap], "101"); @@ -107,8 +107,8 @@ test("no expand capabilities yields no related links", () => { test("child field matching a declared relation links back to the parent record", () => { const link = findParentBackLink( "user_stats", - { id: "101:2026-04-01", user_id: "101", observed_on: "2026-04-01" }, - [{ parentStream: "user", capability: USER_STATS_CAP }], + { id: "101:2026-04-01", observed_on: "2026-04-01", user_id: "101" }, + [{ capability: USER_STATS_CAP, parentStream: "user" }], { connectionId: "github" } ); assert.ok(link); @@ -127,8 +127,8 @@ test("child back-link can be resolved for the requested relation field", () => { "user_stats", { id: "101:2026-04-01", owner_id: "owner-1", user_id: "user-1" }, [ - { parentStream: "user", capability: USER_STATS_CAP }, - { parentStream: "owners", capability: ownerStatsCap }, + { capability: USER_STATS_CAP, parentStream: "user" }, + { capability: ownerStatsCap, parentStream: "owners" }, ], { childParentKeyField: "owner_id", connectionId: "github" } ); @@ -144,7 +144,7 @@ test("child back-link is absent when no declared relation targets the child stre const link = findParentBackLink( "issues", { id: "i1", repository_id: "r1" }, - [{ parentStream: "user", capability: USER_STATS_CAP }], + [{ capability: USER_STATS_CAP, parentStream: "user" }], { connectionId: "github" } ); assert.equal(link, null); @@ -154,18 +154,18 @@ test("candidateParentStreamsForChild uses the manifest only to prune parent meta const streams = [ { name: "user", + query: { expand: [{ name: "user_stats" }] }, relationships: [ - { name: "user_stats", stream: "user_stats", foreign_key: "user_id", cardinality: "has_many" as const }, + { cardinality: "has_many" as const, foreign_key: "user_id", name: "user_stats", stream: "user_stats" }, ], - query: { expand: [{ name: "user_stats" }] }, }, { name: "repositories", + query: { expand: [] }, // Declared but NOT enabled in query.expand → must be ignored. relationships: [ - { name: "issues", stream: "issues", foreign_key: "repository_id", cardinality: "has_many" as const }, + { cardinality: "has_many" as const, foreign_key: "repository_id", name: "issues", stream: "issues" }, ], - query: { expand: [] }, }, { name: "user_stats" }, ]; @@ -217,7 +217,7 @@ test("short connection connector key resolves child-declared relationship manife const stream = connectorManifest?.streams.find((candidate) => candidate.name === "transactions"); const links = childHasOneBackLinksFromManifest( stream, - { id: "1212486749|2026042024323046109400600036029", account_id: "1212486749" }, + { account_id: "1212486749", id: "1212486749|2026042024323046109400600036029" }, { connectionId: "cin_029a67a16d8a252f6e3eb896" } ); @@ -227,22 +227,22 @@ test("short connection connector key resolves child-declared relationship manife test("parentRelationsForChild derives linkable relations from live expand_capabilities metadata", () => { const relations = parentRelationsForChild( [ - { parentStream: "user", expandCapabilities: [USER_STATS_CAP] }, + { expandCapabilities: [USER_STATS_CAP], parentStream: "user" }, { - parentStream: "repositories", expandCapabilities: [ { - name: "issues", - stream: "issues", - target_stream: "issues", cardinality: "has_many", child_parent_key_field: "repository_id", foreign_key: "repository_id", granted: false, - usable: false, + name: "issues", reason: "related_stream_not_granted", + stream: "issues", + target_stream: "issues", + usable: false, }, ], + parentStream: "repositories", }, ], "user_stats" @@ -258,7 +258,7 @@ test("child back-link is absent when the child field value is missing or empty", const missing = findParentBackLink( "user_stats", { id: "101:2026-04-01", observed_on: "2026-04-01" }, - [{ parentStream: "user", capability: USER_STATS_CAP }], + [{ capability: USER_STATS_CAP, parentStream: "user" }], { connectionId: "github" } ); assert.equal(missing, null); @@ -266,7 +266,7 @@ test("child back-link is absent when the child field value is missing or empty", const empty = findParentBackLink( "user_stats", { id: "x", user_id: "" }, - [{ parentStream: "user", capability: USER_STATS_CAP }], + [{ capability: USER_STATS_CAP, parentStream: "user" }], { connectionId: "github" } ); assert.equal(empty, null); @@ -276,13 +276,13 @@ test("child back-link is absent when the child field value is missing or empty", const CHASE_TRANSACTIONS_MANIFEST_STREAM = { name: "transactions", - relationships: [{ name: "account", stream: "accounts", foreign_key: "account_id", cardinality: "has_one" }], + relationships: [{ cardinality: "has_one", foreign_key: "account_id", name: "account", stream: "accounts" }], }; test("child-declared has_one links to the parent record detail page", () => { const links = childHasOneBackLinksFromManifest( CHASE_TRANSACTIONS_MANIFEST_STREAM, - { id: "1212486749|2026042024323046109400600036029", account_id: "1212486749", amount: -1234 }, + { account_id: "1212486749", amount: -1234, id: "1212486749|2026042024323046109400600036029" }, { connectionId: "cin_029a67a16d8a252f6e3eb896" } ); assert.equal(links.length, 1); @@ -297,7 +297,7 @@ test("child-declared has_one percent-encodes connection, stream, and key value", const links = childHasOneBackLinksFromManifest( { name: "items", - relationships: [{ name: "order", stream: "open orders", foreign_key: "order id", cardinality: "has_one" }], + relationships: [{ cardinality: "has_one", foreign_key: "order id", name: "order", stream: "open orders" }], }, { "order id": "ref/42" }, { connectionId: "my conn" } @@ -311,7 +311,7 @@ test("child-declared has_many relationships are ignored by childHasOneBackLinksF const links = childHasOneBackLinksFromManifest( { name: "transactions", - relationships: [{ name: "tags", stream: "tags", foreign_key: "transaction_id", cardinality: "has_many" }], + relationships: [{ cardinality: "has_many", foreign_key: "transaction_id", name: "tags", stream: "tags" }], }, { id: "tx1", transaction_id: "tx1" }, { connectionId: "conn" } @@ -323,7 +323,7 @@ test("unrelated id-looking fields do not link when not covered by a declared has // account_id is NOT declared in this stream's relationships — must not produce a link. const links = childHasOneBackLinksFromManifest( { name: "transactions", relationships: [] }, - { id: "tx1", account_id: "1212486749" }, + { account_id: "1212486749", id: "tx1" }, { connectionId: "conn" } ); assert.deepEqual(links, []); @@ -341,7 +341,7 @@ test("child-declared has_one yields no link when foreign_key field is absent fro test("child-declared has_one yields no link when foreign_key value is empty", () => { const links = childHasOneBackLinksFromManifest( CHASE_TRANSACTIONS_MANIFEST_STREAM, - { id: "tx1", account_id: "" }, + { account_id: "", id: "tx1" }, { connectionId: "cin" } ); assert.deepEqual(links, []); @@ -371,10 +371,10 @@ test("childHasOneLinkFields ignores has_many and incomplete relations, and toler const fields = childHasOneLinkFields({ name: "transactions", relationships: [ - { name: "tags", stream: "tags", foreign_key: "transaction_id", cardinality: "has_many" }, - { name: "account", stream: "accounts", cardinality: "has_one" }, // missing foreign_key - { name: "owner", foreign_key: "owner_id", cardinality: "has_one" }, // missing stream - { name: "category", stream: "categories", foreign_key: "category_id", cardinality: "has_one" }, + { cardinality: "has_many", foreign_key: "transaction_id", name: "tags", stream: "tags" }, + { cardinality: "has_one", name: "account", stream: "accounts" }, // missing foreign_key + { cardinality: "has_one", foreign_key: "owner_id", name: "owner" }, // missing stream + { cardinality: "has_one", foreign_key: "category_id", name: "category", stream: "categories" }, ], }); assert.deepEqual([...fields], ["category_id"]); @@ -385,7 +385,7 @@ test("childHasOneLinkFields ignores has_many and incomplete relations, and toler test("childHasOneBackLinkForField links a declared has_one cell to the parent record", () => { const link = childHasOneBackLinkForField( CHASE_TRANSACTIONS_MANIFEST_STREAM, - { id: "tx1", account_id: "1212486749", amount: -1234 }, + { account_id: "1212486749", amount: -1234, id: "tx1" }, "account_id", { connectionId: "cin_chase" } ); @@ -398,7 +398,7 @@ test("childHasOneBackLinkForField links a declared has_one cell to the parent re test("childHasOneBackLinkForField resolves each field of a two-edges-to-same-parent stream independently", () => { // YNAB transactions: account_id and transfer_account_id are different columns // and resolve to DIFFERENT account records — the list page links each cell. - const record = { id: "t1", account_id: "acc-A", transfer_account_id: "acc-B" }; + const record = { account_id: "acc-A", id: "t1", transfer_account_id: "acc-B" }; const a = childHasOneBackLinkForField(YNAB_TRANSACTIONS_TWO_ACCOUNT_EDGES, record, "account_id", { connectionId: "cin_ynab", }); @@ -414,7 +414,7 @@ test("childHasOneBackLinkForField percent-encodes connection, parent stream, and const link = childHasOneBackLinkForField( { name: "items", - relationships: [{ name: "order", stream: "open orders", foreign_key: "order id", cardinality: "has_one" }], + relationships: [{ cardinality: "has_one", foreign_key: "order id", name: "order", stream: "open orders" }], }, { "order id": "ref/42" }, "order id", @@ -433,7 +433,7 @@ test("childHasOneBackLinkForField returns null for an undeclared field or empty/ ); // Declared field but empty value. assert.equal( - childHasOneBackLinkForField(CHASE_TRANSACTIONS_MANIFEST_STREAM, { id: "tx1", account_id: "" }, "account_id", { + childHasOneBackLinkForField(CHASE_TRANSACTIONS_MANIFEST_STREAM, { account_id: "", id: "tx1" }, "account_id", { connectionId: "cin", }), null @@ -455,15 +455,15 @@ const CHASE_STREAMS = [ { name: "accounts" }, { name: "transactions", - relationships: [{ name: "account", stream: "accounts", foreign_key: "account_id", cardinality: "has_one" }], + relationships: [{ cardinality: "has_one", foreign_key: "account_id", name: "account", stream: "accounts" }], }, ]; test("Chase accounts parent yields a transactions filtered-list link, never a detail URL", () => { const links = reverseChildListLinksFromManifest(CHASE_STREAMS, { connectionId: "cin_029a67a16d8a252f6e3eb896", - parentStream: "accounts", parentRecordKey: "1212486749", + parentStream: "accounts", }); assert.equal(links.length, 1); const link = links[0]; @@ -479,8 +479,8 @@ test("Chase accounts parent yields a transactions filtered-list link, never a de test("reverse link is a filtered list URL with a filter[…] query, never a detail segment", () => { const [link] = reverseChildListLinksFromManifest(CHASE_STREAMS, { connectionId: "chase", - parentStream: "accounts", parentRecordKey: "acc1", + parentStream: "accounts", }); assert.ok(link); // The path part ends at the child stream; the parent key is only in the query. @@ -496,15 +496,15 @@ test("a child-declared has_many produces no reverse link", () => { name: "tags", // has_many back to the parent — must NOT yield a reverse link by this rule. relationships: [ - { name: "transaction", stream: "transactions", foreign_key: "transaction_id", cardinality: "has_many" }, + { cardinality: "has_many", foreign_key: "transaction_id", name: "transaction", stream: "transactions" }, ], }, ]; assert.deepEqual( reverseChildListLinksFromManifest(streams, { connectionId: "conn", - parentStream: "transactions", parentRecordKey: "tx1", + parentStream: "transactions", }), [] ); @@ -516,8 +516,8 @@ test("a parent stream not targeted by any child has_one produces no reverse link // field exists elsewhere. const links = reverseChildListLinksFromManifest(CHASE_STREAMS, { connectionId: "chase", - parentStream: "merchants", parentRecordKey: "m1", + parentStream: "merchants", }); assert.deepEqual(links, []); }); @@ -525,13 +525,13 @@ test("a parent stream not targeted by any child has_one produces no reverse link test("a child has_one without a foreign_key produces no reverse link", () => { const streams = [ { name: "accounts" }, - { name: "transactions", relationships: [{ name: "account", stream: "accounts", cardinality: "has_one" }] }, + { name: "transactions", relationships: [{ cardinality: "has_one", name: "account", stream: "accounts" }] }, ]; assert.deepEqual( reverseChildListLinksFromManifest(streams, { connectionId: "chase", - parentStream: "accounts", parentRecordKey: "a1", + parentStream: "accounts", }), [] ); @@ -542,13 +542,13 @@ test("reverse link percent-encodes connection, child stream, filter field, and p { name: "open orders" }, { name: "line items", - relationships: [{ name: "order", stream: "open orders", foreign_key: "order id", cardinality: "has_one" }], + relationships: [{ cardinality: "has_one", foreign_key: "order id", name: "order", stream: "open orders" }], }, ]; const [link] = reverseChildListLinksFromManifest(streams, { connectionId: "my conn", - parentStream: "open orders", parentRecordKey: "ref/42", + parentStream: "open orders", }); assert.ok(link); assert.equal(link.href, "/sources/my%20conn/line%20items?filter[order%20id]=ref%2F42"); @@ -568,8 +568,8 @@ test("reverse links resolve via findManifestForConnectorId for URL-form and shor assert.ok(manifest, `manifest should resolve for ${id}`); const links = reverseChildListLinksFromManifest(manifest.streams, { connectionId: "cin_live", - parentStream: "accounts", parentRecordKey: "a1", + parentStream: "accounts", }); assert.equal(links.length, 1); assert.equal(links[0]?.href, "/sources/cin_live/transactions?filter[account_id]=a1"); @@ -582,13 +582,13 @@ test("reverse link deduplicates against a forward has_many target on the same ch const forwardLinks = buildRelatedLinks( [ { - name: "transactions", - stream: "transactions", - target_stream: "transactions", cardinality: "has_many", child_parent_key_field: "account_id", foreign_key: "account_id", granted: true, + name: "transactions", + stream: "transactions", + target_stream: "transactions", usable: true, }, ], @@ -603,7 +603,7 @@ test("reverse link deduplicates against a forward has_many target on the same ch const reverse = reverseChildListLinksFromManifest( CHASE_STREAMS, - { connectionId: "chase", parentStream: "accounts", parentRecordKey: "a1" }, + { connectionId: "chase", parentRecordKey: "a1", parentStream: "accounts" }, forwardKeys ); // Forward already covers (transactions, account_id) → reverse suppresses it. @@ -614,7 +614,7 @@ test("reverse link is kept when a forward has_many targets a different child str const forwardKeys = new Set([reverseChildListDedupKey("other_stream", "account_id")]); const reverse = reverseChildListLinksFromManifest( CHASE_STREAMS, - { connectionId: "chase", parentStream: "accounts", parentRecordKey: "a1" }, + { connectionId: "chase", parentRecordKey: "a1", parentStream: "accounts" }, forwardKeys ); assert.equal(reverse.length, 1); @@ -627,15 +627,15 @@ test("reverse link self-deduplicates a child stream declaring the same has_one t { name: "transactions", relationships: [ - { name: "account", stream: "accounts", foreign_key: "account_id", cardinality: "has_one" }, - { name: "owning_account", stream: "accounts", foreign_key: "account_id", cardinality: "has_one" }, + { cardinality: "has_one", foreign_key: "account_id", name: "account", stream: "accounts" }, + { cardinality: "has_one", foreign_key: "account_id", name: "owning_account", stream: "accounts" }, ], }, ]; const links = reverseChildListLinksFromManifest(streams, { connectionId: "chase", - parentStream: "accounts", parentRecordKey: "a1", + parentStream: "accounts", }); assert.equal(links.length, 1); }); @@ -644,20 +644,20 @@ test("reverseChildListLinksFromManifest returns empty for missing streams or arg assert.deepEqual( reverseChildListLinksFromManifest(undefined, { connectionId: "c", - parentStream: "accounts", parentRecordKey: "a1", + parentStream: "accounts", }), [] ); assert.deepEqual( - reverseChildListLinksFromManifest(CHASE_STREAMS, { connectionId: "c", parentStream: "", parentRecordKey: "a1" }), + reverseChildListLinksFromManifest(CHASE_STREAMS, { connectionId: "c", parentRecordKey: "a1", parentStream: "" }), [] ); assert.deepEqual( reverseChildListLinksFromManifest(CHASE_STREAMS, { connectionId: "c", - parentStream: "accounts", parentRecordKey: "", + parentStream: "accounts", }), [] ); @@ -704,7 +704,7 @@ test("reverseChildListEdgesFromManifest ignores a child-declared has_many", () = { name: "tags", relationships: [ - { name: "transaction", stream: "transactions", foreign_key: "transaction_id", cardinality: "has_many" }, + { cardinality: "has_many", foreign_key: "transaction_id", name: "transaction", stream: "transactions" }, ], }, ]; @@ -717,8 +717,8 @@ test("reverseChildListEdgesFromManifest self-dedups a child declaring the same h { name: "transactions", relationships: [ - { name: "account", stream: "accounts", foreign_key: "account_id", cardinality: "has_one" }, - { name: "owning_account", stream: "accounts", foreign_key: "account_id", cardinality: "has_one" }, + { cardinality: "has_one", foreign_key: "account_id", name: "account", stream: "accounts" }, + { cardinality: "has_one", foreign_key: "account_id", name: "owning_account", stream: "accounts" }, ], }, ]; @@ -735,20 +735,20 @@ test("reverseChildListEdgesFromManifest lists multiple distinct child streams an { name: "accounts" }, { name: "balances", - relationships: [{ name: "account", stream: "accounts", foreign_key: "account_id", cardinality: "has_one" }], + relationships: [{ cardinality: "has_one", foreign_key: "account_id", name: "account", stream: "accounts" }], }, { name: "statements", - relationships: [{ name: "account", stream: "accounts", foreign_key: "account_id", cardinality: "has_one" }], + relationships: [{ cardinality: "has_one", foreign_key: "account_id", name: "account", stream: "accounts" }], }, { name: "transactions", - relationships: [{ name: "account", stream: "accounts", foreign_key: "account_id", cardinality: "has_one" }], + relationships: [{ cardinality: "has_one", foreign_key: "account_id", name: "account", stream: "accounts" }], }, // A non-matching child (points at a different parent) must not appear. { name: "merchants", - relationships: [{ name: "category", stream: "categories", foreign_key: "category_id", cardinality: "has_one" }], + relationships: [{ cardinality: "has_one", foreign_key: "category_id", name: "category", stream: "categories" }], }, ]; assert.deepEqual(reverseChildListEdgesFromManifest(streams, "accounts"), [ @@ -770,8 +770,8 @@ test("a Chase accounts list renders a distinct filtered-transactions link per ro const perRow = rows.map((row) => reverseChildListLinksFromManifest(CHASE_STREAMS, { connectionId: "cin_live", - parentStream: "accounts", parentRecordKey: row.id, + parentStream: "accounts", }) ); assert.equal(perRow[0]?.length, 1); @@ -794,8 +794,8 @@ test("a list page for a childless stream yields no per-row reverse links", () => assert.equal(reverseChildListEdgesFromManifest(CHASE_STREAMS, "transactions").length, 0); const links = reverseChildListLinksFromManifest(CHASE_STREAMS, { connectionId: "cin_live", - parentStream: "transactions", parentRecordKey: "t1", + parentStream: "transactions", }); assert.deepEqual(links, []); }); @@ -810,15 +810,15 @@ test("a list page for a childless stream yields no per-row reverse links", () => const YNAB_TRANSACTIONS_TWO_ACCOUNT_EDGES = { name: "transactions", relationships: [ - { name: "account", stream: "accounts", foreign_key: "account_id", cardinality: "has_one" }, - { name: "transfer_account", stream: "accounts", foreign_key: "transfer_account_id", cardinality: "has_one" }, + { cardinality: "has_one", foreign_key: "account_id", name: "account", stream: "accounts" }, + { cardinality: "has_one", foreign_key: "transfer_account_id", name: "transfer_account", stream: "accounts" }, ], }; test("two child-declared has_one edges to the same parent stream via different fields both render", () => { const childLinks = childHasOneBackLinksFromManifest( YNAB_TRANSACTIONS_TWO_ACCOUNT_EDGES, - { id: "t1", account_id: "acc-A", transfer_account_id: "acc-B" }, + { account_id: "acc-A", id: "t1", transfer_account_id: "acc-B" }, { connectionId: "cin_ynab" } ); const merged = mergeParentBackLinks(null, childLinks); @@ -834,13 +834,13 @@ test("mergeParentBackLinks collapses the SAME edge discovered via both sources", // metadata source and child-declared source describe the same (accounts, // account_id) edge → one link, metadata-derived preferred. const metadata: ParentBackLink = { - parentStream: "accounts", childParentKeyField: "account_id", href: "/sources/cin/accounts/acc-A", + parentStream: "accounts", }; const childDeclared = childHasOneBackLinksFromManifest( CHASE_TRANSACTIONS_MANIFEST_STREAM, - { id: "t1", account_id: "acc-A" }, + { account_id: "acc-A", id: "t1" }, { connectionId: "cin" } ); const merged = mergeParentBackLinks(metadata, childDeclared); @@ -850,9 +850,9 @@ test("mergeParentBackLinks collapses the SAME edge discovered via both sources", test("mergeParentBackLinks keeps distinct parent streams and is order-stable", () => { const childLinks: ParentBackLink[] = [ - { parentStream: "accounts", childParentKeyField: "account_id", href: "/a" }, - { parentStream: "payees", childParentKeyField: "payee_id", href: "/p" }, - { parentStream: "categories", childParentKeyField: "category_id", href: "/c" }, + { childParentKeyField: "account_id", href: "/a", parentStream: "accounts" }, + { childParentKeyField: "payee_id", href: "/p", parentStream: "payees" }, + { childParentKeyField: "category_id", href: "/c", parentStream: "categories" }, ]; const merged = mergeParentBackLinks(null, childLinks); assert.deepEqual( diff --git a/apps/console/src/app/(console)/sources/lib/relationships.ts b/apps/console/src/app/(console)/sources/lib/relationships.ts index 2ba24a42b..e25aecb37 100644 --- a/apps/console/src/app/(console)/sources/lib/relationships.ts +++ b/apps/console/src/app/(console)/sources/lib/relationships.ts @@ -187,7 +187,7 @@ export function parentRelationsForChild( if (!childParentKeyField) { continue; } - out.push({ parentStream: parent.parentStream, capability }); + out.push({ capability, parentStream: parent.parentStream }); } } return out; @@ -234,11 +234,11 @@ export function buildRelatedLinks( const cardinality: "has_one" | "has_many" = cap.cardinality === "has_one" ? "has_one" : "has_many"; const link: RelatedLink = { - relation: cap.name, - targetStream, cardinality, childParentKeyField, navigable: false, + relation: cap.name, + targetStream, }; if (cap.usable !== true) { @@ -257,8 +257,8 @@ export function buildRelatedLinks( // Child list filtered by the parent's key. The parent key is NOT a child // record key, so this is the only correct target. link.href = filteredChildListHref({ - connectionId: args.connectionId, childStream: targetStream, + connectionId: args.connectionId, foreignKey: childParentKeyField, parentKey: args.parentRecordKey, }); @@ -309,8 +309,8 @@ export function childHasOneBackLinksFromManifest( } out.push({ childParentKeyField: rel.foreign_key, - parentStream: rel.stream, href: `${base}/${encodeURIComponent(rel.stream)}/${encodeURIComponent(value)}`, + parentStream: rel.stream, }); } return out; @@ -415,8 +415,8 @@ export function reverseChildListLinksFromManifest( childStream: childStream.name, foreignKey: rel.foreign_key, href: filteredChildListHref({ - connectionId: args.connectionId, childStream: childStream.name, + connectionId: args.connectionId, foreignKey: rel.foreign_key, parentKey: args.parentRecordKey, }), @@ -563,8 +563,8 @@ export function findParentBackLink( } return { childParentKeyField, - parentStream, href: `${base}/${encodeURIComponent(parentStream)}/${encodeURIComponent(value)}`, + parentStream, }; } return null; diff --git a/apps/console/src/app/(console)/sources/page.tsx b/apps/console/src/app/(console)/sources/page.tsx index c06943672..9053b7b44 100644 --- a/apps/console/src/app/(console)/sources/page.tsx +++ b/apps/console/src/app/(console)/sources/page.tsx @@ -138,9 +138,9 @@ function SourcesHeader({ error, message, notice }: { error?: string; message?: s

diff --git a/apps/console/src/app/(console)/sources/sources-demo-data.ts b/apps/console/src/app/(console)/sources/sources-demo-data.ts index 8b108ae7a..ebdb4e158 100644 --- a/apps/console/src/app/(console)/sources/sources-demo-data.ts +++ b/apps/console/src/app/(console)/sources/sources-demo-data.ts @@ -37,13 +37,13 @@ function health( extra: Partial = {} ): RefConnectionHealthSnapshot { return { - state, - reason_code: null, - last_success_at: "2026-06-12T08:00:00Z", - next_attempt_at: null, - badges: { stale: false, syncing: false }, axes: EMPTY_AXES, + badges: { stale: false, syncing: false }, + last_success_at: "2026-06-12T08:00:00Z", next_action: null, + next_attempt_at: null, + reason_code: null, + state, unknown_reasons: [], ...extra, }; @@ -66,56 +66,53 @@ function summary( partial: Pick & Partial ): RefConnectorSummary { return { + connection_health: health("healthy"), // The TYPE label (distinct from the owner's per-instance display_name), so // the demo exercises the real "type · account" list line shape. connector_display_name: typeLabel(partial.connector_id), connector_instance_id: partial.connection_id, freshness: {}, - manifest_version: "1.0.0", last_run: { - run_id: "run_demo_0001", - status: "succeeded", - started_at: "2026-06-12T08:00:00Z", + event_count: 42, + failure_reason: null, finished_at: "2026-06-12T08:01:00Z", first_at: "2026-04-01T00:00:00Z", last_at: "2026-06-12T08:01:00Z", - event_count: 42, - failure_reason: null, - }, - last_successful_run: { run_id: "run_demo_0001", - status: "succeeded", started_at: "2026-06-12T08:00:00Z", + status: "succeeded", + }, + last_successful_run: { + event_count: 42, + failure_reason: null, finished_at: "2026-06-12T08:01:00Z", first_at: "2026-04-01T00:00:00Z", last_at: "2026-06-12T08:01:00Z", - event_count: 42, - failure_reason: null, + run_id: "run_demo_0001", + started_at: "2026-06-12T08:00:00Z", + status: "succeeded", }, - schedule: null, + manifest_version: "1.0.0", next_action: null, - connection_health: health("healthy"), - streams: ["messages", "threads"], + schedule: null, stream_count: 2, + streams: ["messages", "threads"], total_records: 1234, ...partial, }; } const GMAIL = summary({ - connector_id: "gmail", + connection_health: health("healthy"), connection_id: "conn_gmail_personal_01", + connector_id: "gmail", display_name: "Gmail (personal)", - streams: ["messages", "threads", "labels", "attachments"], - stream_count: 4, - total_records: 48_201, - connection_health: health("healthy"), schedule: { - object: "schedule", - connector_id: "gmail", - trigger_kind: "scheduled", + active_run_id: null, automation_mode: "unattended", automation_summary: "Runs automatically every day.", + connector_id: "gmail", + created_at: "2026-04-01T00:00:00Z", effective_mode: "automatic", enabled: true, human_attention_needed: false, @@ -129,72 +126,75 @@ const GMAIL = summary({ minimum_interval_warning: null, next_due_at: "2026-06-13T08:00:00Z", notification_posture: "none", + object: "schedule", policy_warning: null, recommended_policy: null, scheduler_backoff: null, - active_run_id: null, - created_at: "2026-04-01T00:00:00Z", + trigger_kind: "scheduled", updated_at: "2026-06-12T08:01:00Z", }, + stream_count: 4, + streams: ["messages", "threads", "labels", "attachments"], + total_records: 48_201, }); const CHATGPT = summary({ - connector_id: "chatgpt", - connection_id: "conn_chatgpt_01", - display_name: "ChatGPT", - streams: ["conversations", "current_activity"], - stream_count: 2, - total_records: 5108, connection_health: health("needs_attention", { - reason_code: "owner_refresh_due", next_action: { - source: "structured", - reason_code: "owner_refresh_due", - owner_action: "act_elsewhere", action_target: "open_detail", attention_id: "att_demo_01", expires_at: null, - response_contract: "none", notification_state: "sent", + owner_action: "act_elsewhere", + reason_code: "owner_refresh_due", + response_contract: "none", + source: "structured", }, + reason_code: "owner_refresh_due", }), + connection_id: "conn_chatgpt_01", + connector_id: "chatgpt", + display_name: "ChatGPT", + stream_count: 2, + streams: ["conversations", "current_activity"], + total_records: 5108, }); const CHASE = summary({ - connector_id: "chase", + connection_health: health("blocked", { reason_code: "reauthorize_required" }), connection_id: "conn_chase_01", + connector_id: "chase", display_name: "Chase", - streams: ["statements", "current_activity"], stream_count: 2, + streams: ["statements", "current_activity"], total_records: 902, - connection_health: health("blocked", { reason_code: "reauthorize_required" }), }); const SPOTIFY_REVOKED = summary({ - connector_id: "spotify", + connection_health: health("idle"), connection_id: "conn_spotify_01", + connector_id: "spotify", display_name: "Spotify", - streams: ["plays", "playlists"], + revoked_at: "2026-06-01T00:00:00Z", + status: "revoked", stream_count: 2, + streams: ["plays", "playlists"], total_records: 12_044, - status: "revoked", - revoked_at: "2026-06-01T00:00:00Z", - connection_health: health("idle"), }); const AMAZON = summary({ - connector_id: "amazon", + connection_health: health("degraded", { reason_code: "partial_coverage" }), connection_id: "conn_amazon_01", + connector_id: "amazon", display_name: "Amazon", - streams: ["orders", "returns", "addresses"], stream_count: 3, + streams: ["orders", "returns", "addresses"], total_records: 311, - connection_health: health("degraded", { reason_code: "partial_coverage" }), }); export function buildSourcesDemoSummaries(scenario: SourcesDemoScenario): RefConnectorSummary[] { if (scenario === "healthy") { - return [GMAIL, summary({ connector_id: "github", connection_id: "conn_github_01", display_name: "GitHub" })]; + return [GMAIL, summary({ connection_id: "conn_github_01", connector_id: "github", display_name: "GitHub" })]; } if (scenario === "attention") { return [CHATGPT, CHASE, AMAZON]; diff --git a/apps/console/src/app/(console)/sources/sources-view-model.test.ts b/apps/console/src/app/(console)/sources/sources-view-model.test.ts index e53916f5c..e30a36784 100644 --- a/apps/console/src/app/(console)/sources/sources-view-model.test.ts +++ b/apps/console/src/app/(console)/sources/sources-view-model.test.ts @@ -57,33 +57,33 @@ const EMPTY_AXES = { function health(state: RefConnectionHealthSnapshot["state"]): RefConnectionHealthSnapshot { return { - state, - reason_code: null, - last_success_at: null, - next_attempt_at: null, - badges: { stale: false, syncing: false }, axes: EMPTY_AXES, + badges: { stale: false, syncing: false }, + last_success_at: null, next_action: null, + next_attempt_at: null, + reason_code: null, + state, unknown_reasons: [], }; } function summary(partial: Partial = {}): RefConnectorSummary { return { - connector_id: "gmail", + connection_health: health("healthy"), connection_id: "conn_1", + connector_display_name: "Gmail", + connector_id: "gmail", connector_instance_id: "conn_1", display_name: "Gmail", - connector_display_name: "Gmail", freshness: {}, - manifest_version: "1.0.0", last_run: null, last_successful_run: null, - schedule: null, + manifest_version: "1.0.0", next_action: null, - connection_health: health("healthy"), - streams: ["messages", "threads"], + schedule: null, stream_count: 2, + streams: ["messages", "threads"], total_records: 100, ...partial, }; @@ -129,11 +129,11 @@ function manualUploadManifest(connectorId = "whatsapp") { connector_id: connectorId, connector_key: connectorId, setup: { - modality: "manual_or_upload", manual_or_upload: { - import_dir_env_var: "WHATSAPP_EXPORT_DIR", accepted_file_extensions: [".zip"], + import_dir_env_var: "WHATSAPP_EXPORT_DIR", }, + modality: "manual_or_upload", }, }; } @@ -176,13 +176,12 @@ test("toSourceInstanceView reads status from rendered_verdict when present", () test("toSourceInstanceView derives local-device modality from persisted source_kind, not heartbeat presence", () => { const localWithoutHeartbeat = toSourceInstanceView( summary({ - source_kind: "local_device", local_device_progress: null, + source_kind: "local_device", }) ); const remoteWithHeartbeatShapedProgress = toSourceInstanceView( summary({ - source_kind: "account", local_device_progress: { last_heartbeat_at: "2026-06-03T11:59:00.000Z", last_heartbeat_status: "healthy", @@ -190,6 +189,7 @@ test("toSourceInstanceView derives local-device modality from persisted source_k records_pending: 0, source_count: 1, }, + source_kind: "account", }) ); assert.equal(localWithoutHeartbeat.isLocalDevicePush, true); @@ -251,13 +251,13 @@ test("toSourceInstanceView reads owner CTA from rendered_verdict required action test("toSourceInstanceView surfaces owner-runnable advisory action cues for source rows", () => { const view = toSourceInstanceView( summary({ - connector_id: "reddit", connector_display_name: "Reddit", + connector_id: "reddit", display_name: "Reddit", rendered_verdict: renderedVerdict({ channel: "advisory", - pill: { label: "Healthy", tone: "green" }, forward_statement: "Run a refresh when you want the latest saved posts.", + pill: { label: "Healthy", tone: "green" }, required_actions: [ { affects: [], @@ -365,8 +365,8 @@ test("toSourceInstanceView does not render maintainer or wait actions as owner C test("toSourceInstanceView renders calibrated live-journey verdict copy without inspection counts", () => { const chatgpt = toSourceInstanceView( summary({ - connector_id: "chatgpt", connector_display_name: "ChatGPT", + connector_id: "chatgpt", display_name: "ChatGPT", rendered_verdict: renderedVerdict({ annotations: [{ kind: "freshness", text: "Fresh today." }], @@ -389,8 +389,8 @@ test("toSourceInstanceView renders calibrated live-journey verdict copy without const amazon = toSourceInstanceView( summary({ - connector_id: "amazon", connector_display_name: "Amazon", + connector_id: "amazon", display_name: "Amazon", rendered_verdict: renderedVerdict({ annotations: [{ kind: "freshness", text: "Last refreshed 31 days ago." }], @@ -415,8 +415,8 @@ test("toSourceInstanceView renders calibrated live-journey verdict copy without const chase = toSourceInstanceView( summary({ - connector_id: "chase", connector_display_name: "Chase", + connector_id: "chase", display_name: "Chase", rendered_verdict: renderedVerdict({ annotations: [{ kind: "freshness", text: "Transactions stuck since Apr 22." }], @@ -507,8 +507,8 @@ test("toSourceInstanceView uses the same stream count for config and stream tabl const view = toSourceInstanceView( summary({ stream_count: 5, + stream_records: [{ last_updated: "2026-07-01T17:58:46.531Z", record_count: 133_848, stream: "messages" }], streams: ["conversations", "messages", "memories", "custom_gpts", "custom_instructions", "shared_conversations"], - stream_records: [{ stream: "messages", record_count: 133_848, last_updated: "2026-07-01T17:58:46.531Z" }], }) ); @@ -519,21 +519,21 @@ test("toSourceInstanceView uses the same stream count for config and stream tabl test("buildSourcesRuntimeAdvisory renders one global runtime fault and ignores healthy runtime", () => { assert.equal( buildSourcesRuntimeAdvisory({ + label: "Collection runtime ready", + message: null, object: "ref_runtime_status", ok: true, reason: null, - label: "Collection runtime ready", - message: null, }), null ); assert.deepEqual( buildSourcesRuntimeAdvisory({ + label: "Collection runtime unavailable", + message: null, object: "ref_runtime_status", ok: false, reason: "controller_unavailable", - label: "Collection runtime unavailable", - message: null, }), { headline: "Collection runtime unavailable", @@ -545,11 +545,11 @@ test("buildSourcesRuntimeAdvisory renders one global runtime fault and ignores h test("formatSchedule is honest about no schedule, paused, and policy-ineligible", () => { assert.equal(formatSchedule(null), "manual — no schedule"); const base: RefSchedule = { - object: "schedule", - connector_id: "gmail", - trigger_kind: "scheduled", + active_run_id: null, automation_mode: "unattended", automation_summary: "", + connector_id: "gmail", + created_at: "", effective_mode: "automatic", enabled: true, human_attention_needed: false, @@ -563,10 +563,10 @@ test("formatSchedule is honest about no schedule, paused, and policy-ineligible" minimum_interval_warning: null, next_due_at: null, notification_posture: "none", + object: "schedule", recommended_policy: null, scheduler_backoff: null, - active_run_id: null, - created_at: "", + trigger_kind: "scheduled", updated_at: "", }; assert.equal(formatSchedule(base), "every 1d · automatic"); @@ -626,23 +626,23 @@ test("toSourceInstanceView surfaces server-owned collection report facts per str test("toSourceInstanceView surfaces retained stream counts without conflating them with latest collection", () => { const view = toSourceInstanceView( summary({ - stream_records: [ - { stream: "messages", record_count: 42, last_updated: "2026-06-17T11:00:00.000Z" }, - { stream: "archived", record_count: 3, last_updated: "2026-06-16T11:00:00.000Z" }, - ], collection_report: [ { - stream: "messages", + checkpoint: "committed", collected: 8, considered: 10, - checkpoint: "committed", + coverage_condition: "retryable_gap", covered: 8, + forward_disposition: "resumable", pending_detail_gaps: 0, skipped: null, - coverage_condition: "retryable_gap", - forward_disposition: "resumable", + stream: "messages", }, ], + stream_records: [ + { last_updated: "2026-06-17T11:00:00.000Z", record_count: 42, stream: "messages" }, + { last_updated: "2026-06-16T11:00:00.000Z", record_count: 3, stream: "archived" }, + ], }) ); @@ -693,7 +693,7 @@ test("toSourceInstanceView drops blank stream names", () => { }); test("toSourceInstanceView surfaces a revoked instance with a struck status", () => { - const view = toSourceInstanceView(summary({ status: "revoked", revoked_at: "2026-06-01T00:00:00Z" })); + const view = toSourceInstanceView(summary({ revoked_at: "2026-06-01T00:00:00Z", status: "revoked" })); assert.equal(view.revoked, true); assert.equal(view.status.kind, "revoked"); }); @@ -701,8 +701,8 @@ test("toSourceInstanceView surfaces a revoked instance with a struck status", () test("toSourceInstanceView omits passport identity rows that duplicate the source title", () => { const view = toSourceInstanceView( summary({ - connector_id: "amazon", connector_display_name: "Amazon", + connector_id: "amazon", display_name: "Amazon - Personal", }) ); @@ -727,15 +727,15 @@ test("toSourceInstanceView omits passport identity rows that duplicate the sourc test("toSourceInstanceView keeps connector type when the source title does not identify it", () => { const view = toSourceInstanceView( summary({ - connector_id: "amazon", connector_display_name: "Amazon", + connector_id: "amazon", display_name: "Personal", }) ); assert.equal(view.displayName, "Personal"); assert.equal(view.listKind, "Amazon"); - assert.deepEqual(view.passportFields[0], { k: "type", value: "Amazon", mono: false }); + assert.deepEqual(view.passportFields[0], { k: "type", mono: false, value: "Amazon" }); }); test("toSourceInstanceView links the detail page, never a raw action target", () => { @@ -793,7 +793,7 @@ test("toSourceInstanceView: a revoked-and-draft row (should not arise in practic // the href does NOT independently re-check revoked. On real data these two // signals never disagree; if that guarantee ever breaks, this test is the // one that will start failing instead of silently drifting. - const view = toSourceInstanceView(draftSummary({ status: "revoked", revoked_at: "2026-07-10T00:00:00Z" })); + const view = toSourceInstanceView(draftSummary({ revoked_at: "2026-07-10T00:00:00Z", status: "revoked" })); assert.equal(view.revoked, true); assert.equal(view.status.kind, "revoked"); assert.equal(view.detailHref, "/connect/status/conn_1"); @@ -802,22 +802,22 @@ test("toSourceInstanceView: a revoked-and-draft row (should not arise in practic test("toSourcesView disambiguates duplicate unnamed connections without exposing ids", () => { const views = toSourcesView([ summary({ - connector_id: "amazon", + connection_id: "cin_a", connector_display_name: "Amazon", + connector_id: "amazon", display_name: "Amazon", - connection_id: "cin_a", }), summary({ - connector_id: "amazon", + connection_id: "cin_b", connector_display_name: "Amazon", + connector_id: "amazon", display_name: "Amazon", - connection_id: "cin_b", }), summary({ - connector_id: "amazon", + connection_id: "cin_named", connector_display_name: "Amazon", + connector_id: "amazon", display_name: "Amazon - Personal", - connection_id: "cin_named", }), ]); @@ -832,23 +832,23 @@ test("toSourcesView disambiguates duplicate unnamed connections without exposing test("duplicate source review flags same-type unnamed active sources without hiding them", () => { const views = toSourcesView([ summary({ - connector_id: "amazon", - connector_display_name: "Amazon", connection_id: "cin_named", + connector_display_name: "Amazon", + connector_id: "amazon", connector_instance_id: "cin_named", display_name: "Amazon - Personal", }), summary({ - connector_id: "amazon", - connector_display_name: "Amazon", connection_id: "cin_a", + connector_display_name: "Amazon", + connector_id: "amazon", connector_instance_id: "cin_a", display_name: "Amazon", }), summary({ - connector_id: "amazon", - connector_display_name: "Amazon", connection_id: "cin_b", + connector_display_name: "Amazon", + connector_id: "amazon", connector_instance_id: "cin_b", display_name: "Amazon", }), @@ -882,16 +882,16 @@ test("duplicate source review flags same-type unnamed active sources without hid test("duplicate source review ignores revoked fallback sources", () => { const views = toSourcesView([ summary({ - connector_id: "amazon", - connector_display_name: "Amazon", connection_id: "cin_active", + connector_display_name: "Amazon", + connector_id: "amazon", connector_instance_id: "cin_active", display_name: "Amazon", }), summary({ - connector_id: "amazon", - connector_display_name: "Amazon", connection_id: "cin_revoked", + connector_display_name: "Amazon", + connector_id: "amazon", connector_instance_id: "cin_revoked", display_name: "Amazon", revoked_at: "2026-06-17T00:00:00Z", @@ -905,37 +905,37 @@ test("duplicate source review ignores revoked fallback sources", () => { test("duplicate fallback collapse keeps named sources visible and groups 3+ unnamed active sources", () => { const views = toSourcesView([ summary({ - connector_id: "amazon", - connector_display_name: "Amazon", connection_id: "cin_named", + connector_display_name: "Amazon", + connector_id: "amazon", connector_instance_id: "cin_named", display_name: "Amazon - Personal", }), summary({ - connector_id: "amazon", - connector_display_name: "Amazon", connection_id: "cin_a", + connector_display_name: "Amazon", + connector_id: "amazon", connector_instance_id: "cin_a", display_name: "Amazon", }), summary({ - connector_id: "amazon", - connector_display_name: "Amazon", connection_id: "cin_b", + connector_display_name: "Amazon", + connector_id: "amazon", connector_instance_id: "cin_b", display_name: "Amazon", }), summary({ - connector_id: "amazon", - connector_display_name: "Amazon", connection_id: "cin_c", + connector_display_name: "Amazon", + connector_id: "amazon", connector_instance_id: "cin_c", display_name: "Amazon", }), summary({ - connector_id: "gmail", - connector_display_name: "Gmail", connection_id: "cin_gmail", + connector_display_name: "Gmail", + connector_id: "gmail", connector_instance_id: "cin_gmail", display_name: "Gmail", }), @@ -958,16 +958,16 @@ test("duplicate fallback collapse keeps named sources visible and groups 3+ unna test("duplicate fallback collapse leaves small duplicate sets visible", () => { const views = toSourcesView([ summary({ - connector_id: "amazon", - connector_display_name: "Amazon", connection_id: "cin_a", + connector_display_name: "Amazon", + connector_id: "amazon", connector_instance_id: "cin_a", display_name: "Amazon", }), summary({ - connector_id: "amazon", - connector_display_name: "Amazon", connection_id: "cin_b", + connector_display_name: "Amazon", + connector_id: "amazon", connector_instance_id: "cin_b", display_name: "Amazon", }), @@ -982,7 +982,7 @@ test("duplicate fallback collapse leaves small duplicate sets visible", () => { }); test("manual/upload sources link to importing another file into the same source", () => { - const view = toSourceInstanceView(summary({ connector_id: "whatsapp", connection_id: "cin_whatsapp_1" }), { + const view = toSourceInstanceView(summary({ connection_id: "cin_whatsapp_1", connector_id: "whatsapp" }), { manifests: [manualUploadManifest()], }); assert.equal(view.manualUploadHref, "/connect/manual-upload/whatsapp?connection_id=cin_whatsapp_1"); @@ -993,12 +993,12 @@ test("manual/upload sources link to importing another file into the same source" }); test("manual/upload source href is absent when the connector has no packaged import binding", () => { - const href = manualUploadHrefForSource(summary({ connector_id: "whatsapp", connection_id: "cin_whatsapp_1" }), [ + const href = manualUploadHrefForSource(summary({ connection_id: "cin_whatsapp_1", connector_id: "whatsapp" }), [ { connector_id: "whatsapp", setup: { - modality: "manual_or_upload", manual_or_upload: { accepted_file_extensions: [".zip"] }, + modality: "manual_or_upload", }, }, ]); diff --git a/apps/console/src/app/(console)/sources/sources-view-model.ts b/apps/console/src/app/(console)/sources/sources-view-model.ts index 6346a9484..ae8477a73 100644 --- a/apps/console/src/app/(console)/sources/sources-view-model.ts +++ b/apps/console/src/app/(console)/sources/sources-view-model.ts @@ -542,13 +542,6 @@ export function toSourceInstanceView( const facts = collectionFactsByStream.get(name) ?? null; const retained = streamRecordsByStream.get(name) ?? null; return { - name, - recordCount: retained ? retained.record_count : null, - // The index summary exposes no cursor or searchable flag per stream; - // render them as unknown rather than guessing. Collection-report facts - // are server-owned and safe to show here without another read. - cursor: null, - searchable: null, collection: facts ? { countsLabel: facts.countsLabel, @@ -564,18 +557,26 @@ export function toSourceInstanceView( tone: facts.tone, } : null, + // The index summary exposes no cursor or searchable flag per stream; + // render them as unknown rather than guessing. Collection-report facts + // are server-owned and safe to show here without another read. + cursor: null, exploreHref: exploreHrefFor(routeId, name), + name, + recordCount: retained ? retained.record_count : null, + searchable: null, }; }); const passportFields: SourcePassportField[] = [ - ...(listKind ? [{ k: "type", value: kind, mono: false } satisfies SourcePassportField] : []), - { k: "config", value: `${sourceStreamNames.length} streams`, mono: true }, + ...(listKind ? [{ k: "type", mono: false, value: kind } satisfies SourcePassportField] : []), + { k: "config", mono: true, value: `${sourceStreamNames.length} streams` }, { k: "auth", value: deriveAuthLine(primaryVerdictAction, isLocalDevicePush, manualUploadHref) }, - { k: "schedule", value: formatSchedule(summary.schedule), mono: true }, - { k: "last run", value: formatLastRun(summary.last_run), mono: true }, + { k: "schedule", mono: true, value: formatSchedule(summary.schedule) }, + { k: "last run", mono: true, value: formatLastRun(summary.last_run) }, { k: "records", + mono: true, // Sol fourth-verdict P1.3: the passport independently rendered the // raw number, bypassing `total_records_state` entirely — the second // of the two concrete authoritative-zero-rendering sites the verdict @@ -583,34 +584,33 @@ export function toSourceInstanceView( value: isTotalRecordsAuthoritative(summary.total_records_state) ? summary.total_records.toLocaleString() : formatTotalRecordsLabel(summary.total_records, summary.total_records_state, "records"), - mono: true, }, - { k: "added", value: summary.last_successful_run?.first_at ?? null, mono: true }, + { k: "added", mono: true, value: summary.last_successful_run?.first_at ?? null }, ]; return { - id: routeId, - connectorId, + accountLine, connectionId, + connectorId, connectorInstanceId, detailHref: sourceDetailHrefFor(routeId, summary), displayName, - kind, - listKind, - accountLine, - revoked, + id: routeId, isLocalDevicePush, isRunning, + kind, + listKind, manualUploadHref, needsOwnerLabel: hasFallbackLabel, - status, nextAction, ownerActionCue, + passportFields, primaryVerdictAction, + revoked, + status, streams, totalRecords: summary.total_records, totalRecordsState: summary.total_records_state, - passportFields, }; } diff --git a/apps/console/src/app/(console)/sources/sources-view.css b/apps/console/src/app/(console)/sources/sources-view.css index 63a66fde1..6ad064e53 100644 --- a/apps/console/src/app/(console)/sources/sources-view.css +++ b/apps/console/src/app/(console)/sources/sources-view.css @@ -22,33 +22,33 @@ .rr-s-list { display: flex; flex-direction: column; - border-top: 1px solid var(--border-strong); min-width: 0; + border-top: 1px solid var(--border-strong); } /* The mobile affordance is a (anchor) that must render identically to the desktop - + Back to syncs

diff --git a/apps/console/src/app/(console)/syncs/syncs-demo.ts b/apps/console/src/app/(console)/syncs/syncs-demo.ts index bb4a11ea5..4cd7272e6 100644 --- a/apps/console/src/app/(console)/syncs/syncs-demo.ts +++ b/apps/console/src/app/(console)/syncs/syncs-demo.ts @@ -27,11 +27,11 @@ const COOLING_RHYTHM: SyncRhythmTick[] = ["ok", "ok", "ok", "ok", "ok"]; function row(partial: Partial & Pick): SyncRow { return { - next: "—", - nextAt: null, - failed: false, collectedThisRun: null, coverageCondition: null, + failed: false, + next: "—", + nextAt: null, streamSkipped: false, ...partial, }; @@ -57,11 +57,6 @@ const BLOCKED_HEALTH: RefConnectionHealthSnapshot = { const COOLING_HEALTH: RefConnectionHealthSnapshot = { axes: { attention: "none", coverage: "partial", freshness: "fresh", outbox: "idle" }, badges: { stale: false, syncing: false }, - last_success_at: "2026-06-13T04:10:00Z", - next_action: null, - next_attempt_at: "2026-06-13T09:00:00Z", - reason_code: "source_pressure", - state: "cooling_off", detail_gap_backlog: { max_attempt_count: 5, next_attempt_at: "2026-06-13T09:00:00Z", @@ -69,6 +64,11 @@ const COOLING_HEALTH: RefConnectionHealthSnapshot = { pending_is_floor: true, recovered: 4200, }, + last_success_at: "2026-06-13T04:10:00Z", + next_action: null, + next_attempt_at: "2026-06-13T09:00:00Z", + reason_code: "source_pressure", + state: "cooling_off", unknown_reasons: [], } as RefConnectionHealthSnapshot; @@ -82,136 +82,136 @@ function demoCard(input: { if (!summary) { throw new Error(`demo health for ${input.name} did not produce a failure summary`); } - return { name: input.name, connectionId: input.connectionId, connectorId: input.connectorId, summary, work: null }; + return { connectionId: input.connectionId, connectorId: input.connectorId, name: input.name, summary, work: null }; } const FIRST_MERIDIAN = demoCard({ - name: "First Meridian — checking", connectionId: "cin_fm_206b11", connectorId: "first_meridian", health: BLOCKED_HEALTH, + name: "First Meridian — checking", }); const CHATGPT = demoCard({ - name: "ChatGPT — personal", connectionId: "cin_cg_91a0fe", connectorId: "chatgpt", health: COOLING_HEALTH, + name: "ChatGPT — personal", }); export const DEMO_SYNCS_MODEL: SyncsViewModel = { - band: { onSchedule: 6, needYourHand: 1, needsReview: 2, allClear: false }, + band: { allClear: false, needsReview: 2, needYourHand: 1, onSchedule: 6 }, duplicateGroups: [], failureCards: [FIRST_MERIDIAN, CHATGPT], - pendingSetupCards: [], groups: [ { - name: "Northstar HR", - connectionId: "cin_nh_e3391c", activeRunId: null, + connectionId: "cin_nh_e3391c", connectorId: "northstar_hr", health: "ok", + lastRunAt: "2026-06-13T06:00:00Z", lastRunDelta: "+2 records", lastRunDuration: "18 s", - lastRunAt: "2026-06-13T06:00:00Z", lastRunRhythm: OK_RHYTHM, + name: "Northstar HR", streams: [ row({ - stream: "pay_statements", + browseHref: "/explore?connection=cin_nh_e3391c&stream=pay_statements", cadence: "with payroll", collectedThisRun: 2, coverageCondition: "complete", next: "Jun 14 · 06:00Z", nextAt: "2026-06-14T06:00:00Z", - browseHref: "/explore?connection=cin_nh_e3391c&stream=pay_statements", + stream: "pay_statements", }), row({ - stream: "employment", + browseHref: "/explore?connection=cin_nh_e3391c&stream=employment", cadence: "daily", collectedThisRun: 0, coverageCondition: "complete", next: "Jun 14 · 06:00Z", nextAt: "2026-06-14T06:00:00Z", - browseHref: "/explore?connection=cin_nh_e3391c&stream=employment", + stream: "employment", }), ], totalStreamCount: 2, }, { - name: "ChatGPT — personal", - connectionId: "cin_cg_91a0fe", activeRunId: null, + connectionId: "cin_cg_91a0fe", connectorId: "chatgpt", health: "failing", + lastRunAt: "2026-06-13T04:10:00Z", lastRunDelta: "+34 records", lastRunDuration: "41 s", - lastRunAt: "2026-06-13T04:10:00Z", lastRunRhythm: COOLING_RHYTHM, + name: "ChatGPT — personal", streams: [ row({ - stream: "conversations", + browseHref: "/explore?connection=cin_cg_91a0fe&stream=conversations", cadence: "daily", collectedThisRun: 34, coverageCondition: "partial", next: "2026-06-13T09:00:00Z", nextAt: "2026-06-13T09:00:00Z", - browseHref: "/explore?connection=cin_cg_91a0fe&stream=conversations", + stream: "conversations", }), ], totalStreamCount: 1, }, { - name: "First Meridian — checking", - connectionId: "cin_fm_206b11", activeRunId: null, + connectionId: "cin_fm_206b11", connectorId: "first_meridian", health: "failing", + lastRunAt: "2026-06-11T05:00:00Z", lastRunDelta: "sync failed", lastRunDuration: "2 s", - lastRunAt: "2026-06-11T05:00:00Z", lastRunRhythm: FAIL_RHYTHM, + name: "First Meridian — checking", streams: [ row({ - stream: "transactions", + browseHref: "/explore?connection=cin_fm_206b11&stream=transactions", cadence: "daily", failed: true, next: "held", - browseHref: "/explore?connection=cin_fm_206b11&stream=transactions", + stream: "transactions", }), row({ - stream: "balances", + browseHref: "/explore?connection=cin_fm_206b11&stream=balances", cadence: "daily", failed: true, next: "held", - browseHref: "/explore?connection=cin_fm_206b11&stream=balances", + stream: "balances", }), ], totalStreamCount: 2, }, { - name: "Gmail — personal", - connectionId: "cin_gm_410c2b", activeRunId: null, + connectionId: "cin_gm_410c2b", connectorId: "gmail", health: "ok", + lastRunAt: "2026-06-13T05:00:00Z", lastRunDelta: "+38 records", lastRunDuration: "6 s", - lastRunAt: "2026-06-13T05:00:00Z", lastRunRhythm: OK_RHYTHM, + name: "Gmail — personal", streams: [ row({ - stream: "messages", + browseHref: "/explore?connection=cin_gm_410c2b&stream=messages", cadence: "every 15 min", collectedThisRun: 38, coverageCondition: "complete", next: "2026-06-13T05:45:00Z", nextAt: "2026-06-13T05:45:00Z", - browseHref: "/explore?connection=cin_gm_410c2b&stream=messages", + stream: "messages", }), ], totalStreamCount: 1, }, ], + pendingSetupCards: [], totalGroupCount: 4, totalReviewCardCount: 2, totalStreamCount: 6, diff --git a/apps/console/src/app/(console)/syncs/syncs-model.test.ts b/apps/console/src/app/(console)/syncs/syncs-model.test.ts index 701769e5b..e48815df5 100644 --- a/apps/console/src/app/(console)/syncs/syncs-model.test.ts +++ b/apps/console/src/app/(console)/syncs/syncs-model.test.ts @@ -185,14 +185,14 @@ function schedule(overrides: Partial = {}): RefSchedule { test("source-pressure cooldown produces a WAIT card, never a reconnect prompt", () => { const coolingHealth = health({ - state: "cooling_off", - reason_code: "source_pressure", - next_attempt_at: "2026-06-13T09:00:00Z", axes: { attention: "none", coverage: "partial", freshness: "fresh", outbox: "idle" }, + next_attempt_at: "2026-06-13T09:00:00Z", + reason_code: "source_pressure", + state: "cooling_off", }); const model = buildSyncsViewModel({ connectors: [connector({ connection_health: coolingHealth, display_name: "ChatGPT — personal" })], - runs: [run({ status: "succeeded", event_count: 34 })], + runs: [run({ event_count: 34, status: "succeeded" })], }); assert.equal(model.failureCards.length, 1, "a cooling connection still gets an honest card"); @@ -214,9 +214,6 @@ test("a blocked connection with a source-pressure backlog still gets the WAIT ca // A `blocked` raw state that carries a scheduled next attempt + pending backlog // is a deferral, not a terminal stop — the guard must suppress the reconnect. const blockedButThrottled = health({ - state: "blocked", - reason_code: "blocked_threshold", - next_attempt_at: "2026-06-13T09:00:00Z", detail_gap_backlog: { max_attempt_count: 5, next_attempt_at: "2026-06-13T09:00:00Z", @@ -224,6 +221,9 @@ test("a blocked connection with a source-pressure backlog still gets the WAIT ca pending_is_floor: true, recovered: 100, }, + next_attempt_at: "2026-06-13T09:00:00Z", + reason_code: "blocked_threshold", + state: "blocked", }); const model = buildSyncsViewModel({ connectors: [connector({ connection_health: blockedButThrottled })], @@ -234,9 +234,9 @@ test("a blocked connection with a source-pressure backlog still gets the WAIT ca test("a genuine blocked connection (no backlog, no next attempt) DOES prompt reconnect", () => { const reallyBlocked = health({ - state: "blocked", - reason_code: "credentials_expired", next_attempt_at: null, + reason_code: "credentials_expired", + state: "blocked", }); const model = buildSyncsViewModel({ connectors: [connector({ connection_health: reallyBlocked })], @@ -625,15 +625,15 @@ test("describeCadence humanizes the schedule interval", () => { assert.equal(describeCadence(schedule({ interval_seconds: 900 })), "every 15 min"); assert.equal(describeCadence(schedule({ interval_seconds: 86_400 })), "daily"); assert.equal(describeCadence(schedule({ effective_mode: "manual" })), "manual"); - assert.equal(describeCadence(schedule({ enabled: false, effective_mode: "paused" })), "paused"); + assert.equal(describeCadence(schedule({ effective_mode: "paused", enabled: false })), "paused"); assert.equal(describeCadence(null), "on demand"); }); test("describeDelta reads records, no change, and failure honestly", () => { - assert.equal(describeDelta({ failed: false, eventCount: 38 }), "+38 records"); - assert.equal(describeDelta({ failed: false, eventCount: 1 }), "+1 record"); - assert.equal(describeDelta({ failed: false, eventCount: 0 }), "no change"); - assert.equal(describeDelta({ failed: true, eventCount: 5 }), "sync failed"); + assert.equal(describeDelta({ eventCount: 38, failed: false }), "+38 records"); + assert.equal(describeDelta({ eventCount: 1, failed: false }), "+1 record"); + assert.equal(describeDelta({ eventCount: 0, failed: false }), "no change"); + assert.equal(describeDelta({ eventCount: 5, failed: true }), "sync failed"); }); test("describeDuration formats sub-minute and minute spans", () => { @@ -643,7 +643,7 @@ test("describeDuration formats sub-minute and minute spans", () => { }); test("a failing connection holds its next and marks rows failed", () => { - const failHealth = health({ state: "blocked", reason_code: "credentials_expired" }); + const failHealth = health({ reason_code: "credentials_expired", state: "blocked" }); const model = buildSyncsViewModel({ connectors: [connector({ connection_health: failHealth, schedule: schedule() })], runs: [run({ connection_id: "cin_test", status: "failed" })], @@ -1289,9 +1289,6 @@ test("syncs cross-surface: an inactive queued recovery card is passive progress, const model = buildSyncsViewModel({ connectors: [ connector({ - connection_id: "cin_amazon", - connector_id: "amazon", - display_name: "Amazon", connection_health: health({ axes: { attention: "none", coverage: "deferred", freshness: "fresh", outbox: "idle" }, badges: { stale: false, syncing: false }, @@ -1307,6 +1304,9 @@ test("syncs cross-surface: an inactive queued recovery card is passive progress, }, state: "degraded", }), + connection_id: "cin_amazon", + connector_id: "amazon", + display_name: "Amazon", rendered_verdict: deferredRecoveryVerdict, streams: ["orders"], }), @@ -1375,14 +1375,14 @@ test("per-stream collectedThisRun is populated from collection_report when prese const model = buildSyncsViewModel({ connectors: [ connector({ + collection_report: [ + collectionEntry({ collected: 120, coverage_condition: "complete", stream: "commits" }), + collectionEntry({ collected: 8, coverage_condition: "partial", stream: "pull_requests" }), + ], connection_id: "cin_gh", connector_id: "github", display_name: "GitHub", streams: ["commits", "pull_requests"], - collection_report: [ - collectionEntry({ stream: "commits", collected: 120, coverage_condition: "complete" }), - collectionEntry({ stream: "pull_requests", collected: 8, coverage_condition: "partial" }), - ], }), ], runs: [], @@ -1406,14 +1406,14 @@ test("two streams with different collection_report entries show DIFFERENT values const model = buildSyncsViewModel({ connectors: [ connector({ + collection_report: [ + collectionEntry({ collected: 42, stream: "messages" }), + collectionEntry({ collected: 0, stream: "attachments" }), + ], connection_id: "cin_multi", connector_id: "gmail", display_name: "Gmail", streams: ["messages", "attachments"], - collection_report: [ - collectionEntry({ stream: "messages", collected: 42 }), - collectionEntry({ stream: "attachments", collected: 0 }), - ], }), ], runs: [run({ connection_id: "cin_multi", event_count: 42, status: "succeeded" })], @@ -1442,9 +1442,9 @@ test("connection-level last-run facts live on the group, not on each stream row" display_name: "Chase", last_run: connectorRun({ event_count: 99, - status: "succeeded", first_at: "2026-06-13T04:00:00Z", last_at: "2026-06-13T04:00:06Z", + status: "succeeded", }), streams: ["transactions", "balances"], }), @@ -1514,14 +1514,14 @@ test("streamSkipped is true only when collection_report entry has a skip", () => const model = buildSyncsViewModel({ connectors: [ connector({ + collection_report: [ + collectionEntry({ collected: 0, skipped: { reason: "rate_limited" }, stream: "transactions" }), + collectionEntry({ collected: 5, stream: "balances" }), + ], connection_id: "cin_skip", connector_id: "plaid", display_name: "Plaid", streams: ["transactions", "balances"], - collection_report: [ - collectionEntry({ stream: "transactions", collected: 0, skipped: { reason: "rate_limited" } }), - collectionEntry({ stream: "balances", collected: 5 }), - ], }), ], runs: [], @@ -1542,22 +1542,22 @@ test("a stream with a real collected count keeps its per-stream truth when the c const model = buildSyncsViewModel({ connectors: [ connector({ + collection_report: [ + collectionEntry({ collected: 500, stream: "messages" }), + // labels has no report entry, so it falls back to connection failure. + ], connection_id: "cin_partial", - connector_instance_id: "cin_partial", connector_id: "gmail", + connector_instance_id: "cin_partial", display_name: "Gmail", streams: ["messages", "labels"], - collection_report: [ - collectionEntry({ stream: "messages", collected: 500 }), - // labels has no report entry, so it falls back to connection failure. - ], }), ], runs: [ run({ connection_id: "cin_partial", - connector_instance_id: "cin_partial", connector_id: "gmail", + connector_instance_id: "cin_partial", event_count: 500, status: "failed", }), diff --git a/apps/console/src/app/(console)/syncs/syncs-model.ts b/apps/console/src/app/(console)/syncs/syncs-model.ts index 5c0138656..5c4630880 100644 --- a/apps/console/src/app/(console)/syncs/syncs-model.ts +++ b/apps/console/src/app/(console)/syncs/syncs-model.ts @@ -484,7 +484,7 @@ function buildSyncRows(input: { const cadence = describeCadence(schedule); const lastRun = connectionRuns.find((r) => isTerminalRunStatus(r.status)) ?? connectionRuns[0] ?? null; const lastFailed = lastRun ? FAILED_RUN_STATUSES.has(lastRun.status) : false; - const { next, nextAt } = describeNext({ schedule, failing }); + const { next, nextAt } = describeNext({ failing, schedule }); // Index collection_report by stream name for O(1) per-row lookup. const reportByStream: Map = indexCollectionReportByStream( @@ -495,24 +495,24 @@ function buildSyncRows(input: { const rows = streams.map((stream): SyncRow => { const reportEntry = reportByStream.get(stream) ?? null; return { - stream, + browseHref: browseStreamHref(connector.connection_id, stream), cadence, - next, - nextAt, + // Per-stream facts from collection_report. Null when absent (honest + // empty state for pre-Tranche-C references). + collectedThisRun: reportEntry !== null && Number.isFinite(reportEntry.collected) ? reportEntry.collected : null, + coverageCondition: reportEntry === null ? null : reportEntry.coverage_condition, // `failed` is the connection-level last-run outcome, used only as a // fallback for streams that have NO per-stream report. When a stream // has its own collection_report entry, that per-stream truth governs // its display, so the connection-level failure must not override it. failed: lastFailed && reportEntry === null, - browseHref: browseStreamHref(connector.connection_id, stream), - // Per-stream facts from collection_report. Null when absent (honest - // empty state for pre-Tranche-C references). - collectedThisRun: reportEntry !== null && Number.isFinite(reportEntry.collected) ? reportEntry.collected : null, - coverageCondition: reportEntry === null ? null : reportEntry.coverage_condition, + next, + nextAt, + stream, streamSkipped: reportEntry !== null && reportEntry.skipped !== null, }; }); - return { rows, lastFailed, lastRun }; + return { lastFailed, lastRun, rows }; } /** @@ -539,10 +539,10 @@ function buildHealthBand(input: { const needYourHand = sourceAttentionHeadline({ ...EMPTY_SOURCE_WORK_GROUPS, needsOwner }).needsYou; const needsReview = input.failureCards.length; return { - onSchedule, - needYourHand, - needsReview, allClear: needsReview === 0, + needsReview, + needYourHand, + onSchedule, }; } @@ -641,9 +641,9 @@ function collapseDuplicateFallbackProjections(projections: readonly SyncProjecti /** Maps a SyncProjection to the FailureCard shape used in the view-model. */ function toFailureCard(projection: SyncProjection): FailureCard { return { - name: projection.connector.display_name, connectionId: projection.connector.connection_id, connectorId: projection.connector.connector_id, + name: projection.connector.display_name, summary: projection.summary as FailureSummary, work: projection.work, }; @@ -659,9 +659,9 @@ function toFailureCard(projection: SyncProjection): FailureCard { function toPendingSetupCard(connector: RefConnectorSummary): PendingSetupCard { const routeId = connector.connection_id ?? connector.connector_instance_id ?? connector.connector_id; return { - continueHref: `/connect/status/${encodeURIComponent(routeId)}`, connectionId: connector.connection_id, connectorId: connector.connector_id, + continueHref: `/connect/status/${encodeURIComponent(routeId)}`, name: connector.display_name, }; } @@ -697,37 +697,37 @@ function projectSyncProjection(input: { const renderedHealth = connector.rendered_verdict ? renderedStatusGroupHealth(actionability.renderedStatus) : null; const failing = (renderedHealth ?? connectionHealth(summary)) === "failing"; const connectionRuns = connectionRunHistory({ connector, runs }); - const { rows, lastFailed, lastRun } = buildSyncRows({ connector, connectionRuns, failing }); + const { rows, lastFailed, lastRun } = buildSyncRows({ connectionRuns, connector, failing }); const lastAt = lastRun?.last_at ?? connector.last_run?.last_at ?? connector.last_successful_run?.last_at ?? null; const lastAtMs = lastAt ? Date.parse(lastAt) : 0; const eventCount = lastRun ? lastRun.event_count : null; - const lastRunDelta = lastRun === null ? null : describeDelta({ failed: lastFailed, eventCount }); + const lastRunDelta = lastRun === null ? null : describeDelta({ eventCount, failed: lastFailed }); const lastRunDuration = describeDuration(lastRun?.first_at ?? null, lastRun?.last_at ?? null); const lastRunRhythm = deriveConnectionRhythm(connectionRuns); return { connector, failing, - lastAtMs: Number.isNaN(lastAtMs) ? 0 : lastAtMs, - summary, - work, group: { activeRunId: connector.schedule?.active_run_id ?? (connector.last_run && isActiveConnectorRunSummaryStatus(connector.last_run.status) ? connector.last_run.run_id : null), - name: connector.display_name, connectionId: connector.connection_id, connectorId: connector.connector_id, health: failing ? "failing" : "ok", + lastRunAt: lastAt, lastRunDelta, lastRunDuration, - lastRunAt: lastAt, lastRunRhythm, + name: connector.display_name, streams: rows, totalStreamCount: rows.length, }, + lastAtMs: Number.isNaN(lastAtMs) ? 0 : lastAtMs, + summary, + work, }; } @@ -784,7 +784,7 @@ export function buildSyncsViewModel(input: { // duplicate groups that were collapsed away are surfaced separately through // the duplicate-group panel, so counting them here would tell the owner to // "review the cards below" when no such card is visible. - const band = buildHealthBand({ groups: allGroups, failureCards, pendingSetupWork, projections: ordered }); + const band = buildHealthBand({ failureCards, groups: allGroups, pendingSetupWork, projections: ordered }); return { band, duplicateGroups, diff --git a/apps/console/src/app/(console)/syncs/syncs-view.tsx b/apps/console/src/app/(console)/syncs/syncs-view.tsx index c9fb11dfa..a23646a9a 100644 --- a/apps/console/src/app/(console)/syncs/syncs-view.tsx +++ b/apps/console/src/app/(console)/syncs/syncs-view.tsx @@ -52,14 +52,14 @@ type FailureSection = (typeof FAILURE_SECTION_ORDER)[number]; // category copy. Only "other" is local to this surface. const FAILURE_SECTION_COPY: Record = { needsOwner: SOURCE_WORK_GROUP_COPY.needsOwner, - review: SOURCE_WORK_GROUP_COPY.review, - systemIssue: SOURCE_WORK_GROUP_COPY.systemIssue, - working: SOURCE_WORK_GROUP_COPY.working, notMeasured: SOURCE_WORK_GROUP_COPY.notMeasured, other: { label: "Other source work", note: "Worth a look when you have a moment.", }, + review: SOURCE_WORK_GROUP_COPY.review, + systemIssue: SOURCE_WORK_GROUP_COPY.systemIssue, + working: SOURCE_WORK_GROUP_COPY.working, }; // ─── Health stat band ───────────────────────────────────────────────────────── diff --git a/apps/console/src/app/api/stream-debug/route.test.ts b/apps/console/src/app/api/stream-debug/route.test.ts index 092f3fa66..d33c54209 100644 --- a/apps/console/src/app/api/stream-debug/route.test.ts +++ b/apps/console/src/app/api/stream-debug/route.test.ts @@ -25,13 +25,13 @@ test("stream debug route appends sanitized events to date-grouped JSONL", async body: JSON.stringify({ events: [ { - name: "neko.clipboard_local_to_remote", clipboardChangeEventAvailable: true, + name: "neko.clipboard_local_to_remote", password: "do-not-store-password", - rawClipboardText: "do-not-store-clipboard", proxyUrl: "https://token:do-not-store@neko.internal/session", - transportError: "upstream disconnected at wss://neko.internal/socket?token=do-not-store", + rawClipboardText: "do-not-store-clipboard", safeLengthBucket: "17-64", + transportError: "upstream disconnected at wss://neko.internal/socket?token=do-not-store", }, ], }), @@ -45,7 +45,7 @@ test("stream debug route appends sanitized events to date-grouped JSONL", async ); assert.equal(response.status, 200); - assert.deepEqual(await response.json(), { ok: true, accepted: 1 }); + assert.deepEqual(await response.json(), { accepted: 1, ok: true }); assert.equal( logs.some((line) => line.includes("pdpp_stream_debug")), true diff --git a/apps/console/src/app/api/stream-debug/route.ts b/apps/console/src/app/api/stream-debug/route.ts index 077f9d9d2..97caf37c9 100644 --- a/apps/console/src/app/api/stream-debug/route.ts +++ b/apps/console/src/app/api/stream-debug/route.ts @@ -133,7 +133,7 @@ export async function POST(request: Request) { ? ((payload as { events: unknown[] }).events.slice(0, MAX_EVENTS) as unknown[]) : []; if (events.length === 0) { - return NextResponse.json({ ok: true, accepted: 0 }); + return NextResponse.json({ accepted: 0, ok: true }); } const sanitized = sanitizeValue({ @@ -154,5 +154,5 @@ export async function POST(request: Request) { ) ); } - return NextResponse.json({ ok: true, accepted: events.length }); + return NextResponse.json({ accepted: events.length, ok: true }); } diff --git a/apps/console/src/app/apple-icon.tsx b/apps/console/src/app/apple-icon.tsx index 44be4185a..52c0fe283 100644 --- a/apps/console/src/app/apple-icon.tsx +++ b/apps/console/src/app/apple-icon.tsx @@ -4,7 +4,7 @@ import { LAUNCH_COLORS } from "@pdpp/brand/launch-colors"; import { ImageResponse } from "next/og"; -export const size = { width: 180, height: 180 }; +export const size = { height: 180, width: 180 }; export const contentType = "image/png"; // Apple touch icon for the iOS home screen — the console's two-rectangle PDPP @@ -21,12 +21,12 @@ export default function AppleIcon() { return new ImageResponse(
diff --git a/apps/console/src/app/design-system/page.tsx b/apps/console/src/app/design-system/page.tsx index 9b9042599..4f9c64501 100644 --- a/apps/console/src/app/design-system/page.tsx +++ b/apps/console/src/app/design-system/page.tsx @@ -65,8 +65,8 @@ import { import type { Metadata } from "next"; export const metadata: Metadata = { + robots: { follow: false, index: false }, title: "Ink Carbon — Design showcase", - robots: { index: false, follow: false }, }; // ─── Specimen panel ─────────────────────────────────────────────── @@ -76,15 +76,15 @@ function Section({ title, children }: { title: string; children: React.ReactNode
{title} @@ -100,12 +100,12 @@ function ThemePanel({ theme, label }: { theme: "dark" | "light"; label: string } className={theme === "dark" ? "dark" : undefined} data-theme={theme} style={{ - flex: 1, - minWidth: 0, background: "var(--background)", color: "var(--foreground)", - padding: "2rem", + flex: 1, fontFamily: "var(--font-sans)", + minWidth: 0, + padding: "2rem", }} >
@@ -129,14 +129,14 @@ function ThemePanel({ theme, label }: { theme: "dark" | "light"; label: string } {/* ─── Endorsements (status badges) ─── */}
-
+
-
+
@@ -145,7 +145,7 @@ function ThemePanel({ theme, label }: { theme: "dark" | "light"; label: string } {/* ─── Tags ─── */}
-
+
statements transactions identity @@ -156,13 +156,13 @@ function ThemePanel({ theme, label }: { theme: "dark" | "light"; label: string } {/* ─── Buttons ─── */}
-
+
Default Approve 3 streams Cancel Revoke
-
+
Default sm Approve @@ -400,20 +400,20 @@ export default function InkCarbonShowcase() { return (
{/* Page header */}
@@ -421,7 +421,7 @@ export default function InkCarbonShowcase() { Phase 1 · /design-system @@ -430,7 +430,7 @@ export default function InkCarbonShowcase() { {/* Two-column: dark | light */}
-
+
diff --git a/apps/console/src/app/globals.css b/apps/console/src/app/globals.css index 5d9ef96d5..35a9501e4 100644 --- a/apps/console/src/app/globals.css +++ b/apps/console/src/app/globals.css @@ -220,8 +220,8 @@ @layer base { body { - @apply bg-background text-foreground; font-family: var(--font-sans); + @apply bg-background text-foreground; } html, body { @@ -248,8 +248,8 @@ html { } html > body[data-scroll-locked] { - margin-right: 0 !important; --removed-body-scroll-bar-size: 0 !important; + margin-right: 0 !important; } /* @@ -292,10 +292,10 @@ html:has(.pdpp-stream-dialog) { .pdpp-stream-frame { --pdpp-stream-video-matte: oklch(0.08 0.005 260); inline-size: 100%; - block-size: 100%; min-inline-size: 0; - min-block-size: 0; max-width: 100%; + block-size: 100%; + min-block-size: 0; max-height: 100%; background: var(--pdpp-stream-video-matte); } @@ -332,8 +332,8 @@ html:has(.pdpp-stream-dialog) { * which per-route CSS chunk order Next.js produces. */ .pdpp-dialog-backdrop.pdpp-stream-dialog-backdrop { - background: transparent; overflow: clip; + background: transparent; } .pdpp-dialog.pdpp-stream-dialog { @@ -344,28 +344,28 @@ html:has(.pdpp-stream-dialog) { inset-inline-start: 0; top: 0; left: 0; - transform: none; z-index: 50; display: grid; grid-template-rows: 1fr; + gap: 0; inline-size: 100%; - block-size: 100vh; - block-size: 100svh; max-inline-size: 100%; width: 100%; + max-width: 100%; + block-size: 100vh; + block-size: 100svh; height: 100vh; height: 100svh; - max-width: 100%; + padding: 0; margin: 0; overflow: hidden; - translate: 0 0; - scale: 1; + background: var(--pdpp-stream-video-matte); border: 0; border-radius: 0; - background: var(--pdpp-stream-video-matte); - padding: 0; box-shadow: none; - gap: 0; + transform: none; + scale: 1; + translate: 0 0; } .pdpp-dialog.pdpp-stream-dialog:is([data-starting-style], [data-ending-style]) { @@ -375,52 +375,52 @@ html:has(.pdpp-stream-dialog) { .pdpp-stream-corner-controls { --pdpp-stream-control-offset: 0.75rem; --pdpp-stream-control-gap: 0.5rem; - pointer-events: none; position: absolute; inset-block-start: var(--pdpp-stream-control-offset); inset-inline-end: var(--pdpp-stream-control-offset); z-index: 10; display: flex; flex-direction: column; - align-items: flex-end; gap: var(--pdpp-stream-control-gap); + align-items: flex-end; + pointer-events: none; } .pdpp-stream-control-row { display: flex; - align-items: center; gap: var(--pdpp-stream-control-gap); + align-items: center; } @media (max-width: 40rem), (pointer: coarse) { .pdpp-stream-corner-controls { inset-block-start: auto; - inset-inline-end: 50%; inset-block-end: calc( env(safe-area-inset-bottom) + var(--pdpp-stream-control-offset) ); - translate: 50% 0; + inset-inline-end: 50%; align-items: center; + translate: 50% 0; } } .pdpp-stream-control-button { --pdpp-stream-control-size: 2.75rem; - pointer-events: auto; display: inline-flex; - inline-size: var(--pdpp-stream-control-size); - block-size: var(--pdpp-stream-control-size); align-items: center; justify-content: center; - border-radius: 999px; - background: color-mix(in oklch, var(--background) 90%, transparent); + inline-size: var(--pdpp-stream-control-size); + block-size: var(--pdpp-stream-control-size); color: var(--foreground); - box-shadow: 0 1px 2px color-mix(in oklch, var(--foreground) 12%, transparent); + pointer-events: auto; outline: none; + background: color-mix(in oklch, var(--background) 90%, transparent); + border: 1px solid color-mix(in oklch, var(--border) 70%, transparent); + border-radius: 999px; + box-shadow: 0 1px 2px color-mix(in oklch, var(--foreground) 12%, transparent); backdrop-filter: blur(8px); transition: background-color 150ms ease; - border: 1px solid color-mix(in oklch, var(--border) 70%, transparent); } .pdpp-stream-control-button:hover { @@ -433,9 +433,9 @@ html:has(.pdpp-stream-dialog) { } .pdpp-stream-keyboard-affordance { + gap: 0.4rem; inline-size: auto; min-inline-size: var(--pdpp-stream-control-size); - gap: 0.4rem; padding-inline: 0.75rem; } @@ -447,17 +447,17 @@ html:has(.pdpp-stream-dialog) { } .pdpp-stream-location-label { - pointer-events: auto; display: flex; - max-inline-size: calc(100vw - 2rem); - align-items: baseline; gap: 0.25rem; - border-radius: 999px; - background: color-mix(in oklch, var(--background) 90%, transparent); + align-items: baseline; + max-inline-size: calc(100vw - 2rem); padding: 0.25rem 0.75rem; + pointer-events: auto; + background: color-mix(in oklch, var(--background) 90%, transparent); + border: 1px solid color-mix(in oklch, var(--border) 70%, transparent); + border-radius: 999px; box-shadow: 0 1px 2px color-mix(in oklch, var(--foreground) 12%, transparent); backdrop-filter: blur(8px); - border: 1px solid color-mix(in oklch, var(--border) 70%, transparent); } @media (min-width: 40rem) { @@ -468,19 +468,19 @@ html:has(.pdpp-stream-dialog) { .pdpp-stream-toast-zone { --pdpp-stream-toast-offset: 0.75rem; - pointer-events: none; position: absolute; - inset-inline: 0; inset-block-end: calc( env(safe-area-inset-bottom) + var(--pdpp-stream-toast-offset) ); + inset-inline: 0; z-index: 10; display: flex; - max-inline-size: 24rem; justify-content: center; - margin-inline: auto; + max-inline-size: 24rem; padding-inline: 1rem; + margin-inline: auto; + pointer-events: none; } .pdpp-stream-toast-zone[data-slot="popup"] { @@ -500,19 +500,19 @@ html:has(.pdpp-stream-dialog) { the destructive choice is never tucked under the step panel it is guarding. */ .pdpp-stream-toast-zone[data-slot="close-confirm"] { --pdpp-stream-toast-offset: 8.5rem; - max-inline-size: min(34rem, 100vw); z-index: 11; + max-inline-size: min(34rem, 100vw); } .pdpp-stream-toast-bubble { - pointer-events: auto; - border-radius: 0.375rem; - background: color-mix(in oklch, var(--background) 95%, transparent); padding: 0.5rem 0.75rem; color: var(--foreground); + pointer-events: auto; + background: color-mix(in oklch, var(--background) 95%, transparent); + border: 1px solid color-mix(in oklch, var(--border) 70%, transparent); + border-radius: 0.375rem; box-shadow: 0 1px 2px color-mix(in oklch, var(--foreground) 12%, transparent); backdrop-filter: blur(8px); - border: 1px solid color-mix(in oklch, var(--border) 70%, transparent); } .pdpp-stream-clipboard-textarea[data-masked="true"] { @@ -543,8 +543,8 @@ html:has(.pdpp-stream-dialog) { } [data-pdpp-neko-client] { - background: var(--pdpp-stream-video-matte); overflow: hidden; + background: var(--pdpp-stream-video-matte); } /* Prevent the host browser (e.g. Brave on Android) from hijacking @@ -611,8 +611,8 @@ html:has(.pdpp-stream-dialog) { display: flex !important; width: 100% !important; height: 100% !important; - margin: 0 !important; padding: 0 !important; + margin: 0 !important; background: var(--pdpp-stream-video-matte) !important; } @@ -630,8 +630,8 @@ html:has(.pdpp-stream-dialog) { [data-pdpp-neko-client] .emotes { inset: 0 !important; width: 100% !important; - height: 100% !important; max-width: none !important; + height: 100% !important; max-height: none !important; margin: 0 !important; } @@ -658,29 +658,29 @@ html:has(.pdpp-stream-dialog) { margin-top: var(--docs-prose-spacing); } .docs-prose h1 { + margin-top: 1.75rem; font-size: 1.6rem; font-weight: 600; letter-spacing: -0.01em; - margin-top: 1.75rem; } .docs-prose h2 { + padding-top: 0.5rem; + margin-top: 1.75rem; font-size: 1.25rem; font-weight: 600; letter-spacing: -0.005em; - margin-top: 1.75rem; - padding-top: 0.5rem; border-top: 1px solid var(--border); } .docs-prose h3 { + margin-top: 1.25rem; font-size: 1.05rem; font-weight: 600; - margin-top: 1.25rem; } .docs-prose h4, .docs-prose h5, .docs-prose h6 { - font-weight: 600; margin-top: 1rem; + font-weight: 600; } .docs-prose p { color: var(--foreground); @@ -694,12 +694,12 @@ html:has(.pdpp-stream-dialog) { text-decoration-thickness: 2px; } .docs-prose ul { - list-style: disc; padding-left: 1.25rem; + list-style: disc; } .docs-prose ol { - list-style: decimal; padding-left: 1.25rem; + list-style: decimal; } .docs-prose li + li { margin-top: 0.25rem; @@ -708,34 +708,34 @@ html:has(.pdpp-stream-dialog) { margin: 0; } .docs-prose code { + padding: 0.1em 0.35em; font-family: var(--font-mono); font-size: 0.85em; background: var(--muted); - padding: 0.1em 0.35em; border-radius: 0.3em; } .docs-prose pre { - background: var(--card); - border: 1px solid var(--border); - border-radius: 0.5rem; padding: 0.875rem 1rem; overflow-x: auto; font-size: 0.85em; + background: var(--card); + border: 1px solid var(--border); + border-radius: 0.5rem; } .docs-prose pre code { - background: transparent; padding: 0; + background: transparent; border-radius: 0; } .docs-prose blockquote { - border-left: 2px solid var(--border); padding: 0.25rem 0 0.25rem 0.875rem; color: var(--muted-foreground); + border-left: 2px solid var(--border); } .docs-prose hr { + margin: 2rem 0; border: 0; border-top: 1px solid var(--border); - margin: 2rem 0; } .docs-prose table { width: 100%; @@ -744,9 +744,9 @@ html:has(.pdpp-stream-dialog) { } .docs-prose th, .docs-prose td { - border-bottom: 1px solid var(--border); padding: 0.45rem 0.6rem; text-align: left; + border-bottom: 1px solid var(--border); } .docs-prose th { font-weight: 600; diff --git a/apps/console/src/app/icon.svg b/apps/console/src/app/icon.svg index cb3fac5eb..02bf4172d 100644 --- a/apps/console/src/app/icon.svg +++ b/apps/console/src/app/icon.svg @@ -1,6 +1,6 @@ - - - - - + + + + + diff --git a/apps/console/src/app/layout.tsx b/apps/console/src/app/layout.tsx index 01eacce56..2c1c58bf6 100644 --- a/apps/console/src/app/layout.tsx +++ b/apps/console/src/app/layout.tsx @@ -18,24 +18,14 @@ import "./globals.css"; // range and italic support. The CSS variable --ink-carbon-sans is injected // on and @pdpp/brand/ink-carbon.css picks it up via --font-sans override. const schibstedGrotesk = Schibsted_Grotesk({ - subsets: ["latin"], - weight: ["400", "500", "600", "700", "800", "900"], + display: "swap", style: ["normal", "italic"], + subsets: ["latin"], variable: "--ink-carbon-sans", - display: "swap", + weight: ["400", "500", "600", "700", "800", "900"], }); export const metadata: Metadata = { - title: "PDPP — Personal Data Portability Protocol", - description: - "An authorization and disclosure protocol for personal data. You decide what to share, with whom, for how long, for what purpose.", - metadataBase: new URL("https://pdpp.dev"), - icons: { - icon: [ - { url: "/icon.svg", type: "image/svg+xml" }, - { url: "/brand/pdpp-favicon.svg", type: "image/svg+xml" }, - ], - }, // iOS home-screen launch. statusBarStyle "default" lets iOS pick the bar // treatment per system appearance (light bar on light, dark bar on dark) // instead of forcing one — so the launch reads correctly under both themes, @@ -46,30 +36,40 @@ export const metadata: Metadata = { statusBarStyle: "default", title: "PDPP", }, + description: + "An authorization and disclosure protocol for personal data. You decide what to share, with whom, for how long, for what purpose.", + icons: { + icon: [ + { type: "image/svg+xml", url: "/icon.svg" }, + { type: "image/svg+xml", url: "/brand/pdpp-favicon.svg" }, + ], + }, + metadataBase: new URL("https://pdpp.dev"), openGraph: { - title: "PDPP — Personal Data Portability Protocol", description: "An authorization and disclosure protocol for personal data. You decide what to share, with whom, for how long, for what purpose.", + title: "PDPP — Personal Data Portability Protocol", type: "website", }, + title: "PDPP — Personal Data Portability Protocol", twitter: { card: "summary_large_image", - title: "PDPP — Personal Data Portability Protocol", description: "An authorization and disclosure protocol for personal data.", + title: "PDPP — Personal Data Portability Protocol", }, }; export const viewport = { - width: "device-width", initialScale: 1, // Theme-following chrome color: the browser/PWA picks the entry matching the // OS scheme, so the splash/chrome never flashes the wrong color before the // app paints. Both colors are sourced from LAUNCH_COLORS (the single source // of truth derived from the `--background` tokens) — no drifting hex here. themeColor: [ - { media: "(prefers-color-scheme: dark)", color: LAUNCH_COLORS.dark }, - { media: "(prefers-color-scheme: light)", color: LAUNCH_COLORS.light }, + { color: LAUNCH_COLORS.dark, media: "(prefers-color-scheme: dark)" }, + { color: LAUNCH_COLORS.light, media: "(prefers-color-scheme: light)" }, ], + width: "device-width", }; export default async function RootLayout({ children }: { children: React.ReactNode }) { diff --git a/apps/console/src/app/manifest.ts b/apps/console/src/app/manifest.ts index c2e3a7173..026f0b082 100644 --- a/apps/console/src/app/manifest.ts +++ b/apps/console/src/app/manifest.ts @@ -12,21 +12,21 @@ import type { MetadataRoute } from "next"; // truth) — manifests cannot carry oklch() or CSS vars, hence the imported hex. export default function manifest(): MetadataRoute.Manifest { return { - name: "PDPP Owner Console", - short_name: "PDPP", + background_color: LAUNCH_COLORS.light, description: "Owner console for your PDPP reference instance — connections, runs, grants, and the record explorer.", - start_url: "/", - scope: "/", display: "standalone", - background_color: LAUNCH_COLORS.light, - theme_color: LAUNCH_COLORS.light, icons: [ { - src: "/brand/pdpp-favicon.svg", + purpose: "any", sizes: "any", + src: "/brand/pdpp-favicon.svg", type: "image/svg+xml", - purpose: "any", }, ], + name: "PDPP Owner Console", + scope: "/", + short_name: "PDPP", + start_url: "/", + theme_color: LAUNCH_COLORS.light, }; } diff --git a/apps/console/src/app/reference-proxy.ts b/apps/console/src/app/reference-proxy.ts index 06891321a..e2d7e338c 100644 --- a/apps/console/src/app/reference-proxy.ts +++ b/apps/console/src/app/reference-proxy.ts @@ -94,8 +94,8 @@ export async function proxyReferenceRequest( { error: { code: "reference_unreachable", - message: `Cannot reach PDPP ${target.toUpperCase()} service.`, detail: error instanceof Error ? error.message : String(error), + message: `Cannot reach PDPP ${target.toUpperCase()} service.`, }, }, { status: 502 } diff --git a/apps/console/src/app/well-known/skills/[...path]/route.ts b/apps/console/src/app/well-known/skills/[...path]/route.ts index 7f545d03a..762885ff0 100644 --- a/apps/console/src/app/well-known/skills/[...path]/route.ts +++ b/apps/console/src/app/well-known/skills/[...path]/route.ts @@ -26,9 +26,9 @@ function notFound(): Response { return jsonResponse( { error: { - type: "not_found_error", code: "not_found", message: "Skill file not found", + type: "not_found_error", }, }, { status: 404 } diff --git a/apps/console/src/app/well-known/skills/route.test.ts b/apps/console/src/app/well-known/skills/route.test.ts index 94364461c..abea603a5 100644 --- a/apps/console/src/app/well-known/skills/route.test.ts +++ b/apps/console/src/app/well-known/skills/route.test.ts @@ -94,9 +94,9 @@ test("console .well-known skills route serves only allowlisted files", async () assert.match(traversal.headers.get("content-type") ?? "", APPLICATION_JSON); assert.deepEqual(await jsonOf(traversal), { error: { - type: "not_found_error", code: "not_found", message: "Skill file not found", + type: "not_found_error", }, }); }); diff --git a/apps/console/src/lib/agent-skills/catalog.ts b/apps/console/src/lib/agent-skills/catalog.ts index 1e20f6238..f42f92fd4 100644 --- a/apps/console/src/lib/agent-skills/catalog.ts +++ b/apps/console/src/lib/agent-skills/catalog.ts @@ -38,50 +38,50 @@ interface AgentSkillDefinition { const SKILLS: readonly AgentSkillDefinition[] = [ { - name: DATA_ACCESS_SKILL_NAME, - description: DATA_ACCESS_SKILL_DESCRIPTION, canonical_source: "docs/agent-skills", - recommended_install: "npx skills add -g when supported; otherwise fetch files from this catalog", + description: DATA_ACCESS_SKILL_DESCRIPTION, files: [ { - routePath: `${DATA_ACCESS_SKILL_NAME}/SKILL.md`, - repoRelativePath: `${DATA_ACCESS_SKILL_BASE_REPO_PATH}/SKILL.md`, mediaType: "text/markdown; charset=utf-8", + repoRelativePath: `${DATA_ACCESS_SKILL_BASE_REPO_PATH}/SKILL.md`, + routePath: `${DATA_ACCESS_SKILL_NAME}/SKILL.md`, }, { - routePath: `${DATA_ACCESS_SKILL_NAME}/references/grant-design.md`, - repoRelativePath: `${DATA_ACCESS_SKILL_BASE_REPO_PATH}/references/grant-design.md`, mediaType: "text/markdown; charset=utf-8", + repoRelativePath: `${DATA_ACCESS_SKILL_BASE_REPO_PATH}/references/grant-design.md`, + routePath: `${DATA_ACCESS_SKILL_NAME}/references/grant-design.md`, }, { - routePath: `${DATA_ACCESS_SKILL_NAME}/references/query-cookbook.md`, - repoRelativePath: `${DATA_ACCESS_SKILL_BASE_REPO_PATH}/references/query-cookbook.md`, mediaType: "text/markdown; charset=utf-8", + repoRelativePath: `${DATA_ACCESS_SKILL_BASE_REPO_PATH}/references/query-cookbook.md`, + routePath: `${DATA_ACCESS_SKILL_NAME}/references/query-cookbook.md`, }, { - routePath: `${DATA_ACCESS_SKILL_NAME}/references/security.md`, - repoRelativePath: `${DATA_ACCESS_SKILL_BASE_REPO_PATH}/references/security.md`, mediaType: "text/markdown; charset=utf-8", + repoRelativePath: `${DATA_ACCESS_SKILL_BASE_REPO_PATH}/references/security.md`, + routePath: `${DATA_ACCESS_SKILL_NAME}/references/security.md`, }, { - routePath: `${DATA_ACCESS_SKILL_NAME}/references/troubleshooting.md`, - repoRelativePath: `${DATA_ACCESS_SKILL_BASE_REPO_PATH}/references/troubleshooting.md`, mediaType: "text/markdown; charset=utf-8", + repoRelativePath: `${DATA_ACCESS_SKILL_BASE_REPO_PATH}/references/troubleshooting.md`, + routePath: `${DATA_ACCESS_SKILL_NAME}/references/troubleshooting.md`, }, ], + name: DATA_ACCESS_SKILL_NAME, + recommended_install: "npx skills add -g when supported; otherwise fetch files from this catalog", }, { - name: OWNER_AGENT_SKILL_NAME, - description: OWNER_AGENT_SKILL_DESCRIPTION, canonical_source: "docs/agent-skills", - recommended_install: "npx skills add -g when supported; otherwise fetch files from this catalog", + description: OWNER_AGENT_SKILL_DESCRIPTION, files: [ { - routePath: OWNER_AGENT_SKILL_ROUTE_PATH, - repoRelativePath: `${OWNER_AGENT_SKILL_BASE_REPO_PATH}/SKILL.md`, mediaType: "text/markdown; charset=utf-8", + repoRelativePath: `${OWNER_AGENT_SKILL_BASE_REPO_PATH}/SKILL.md`, + routePath: OWNER_AGENT_SKILL_ROUTE_PATH, }, ], + name: OWNER_AGENT_SKILL_NAME, + recommended_install: "npx skills add -g when supported; otherwise fetch files from this catalog", }, ]; @@ -170,10 +170,10 @@ export async function buildAgentSkillCatalog(origin: string): Promise => { const bytes = await readRepoFile(file.repoRelativePath); return { + bytes: bytes.byteLength, + media_type: file.mediaType, path: file.routePath, repo_path: file.repoRelativePath, - media_type: file.mediaType, - bytes: bytes.byteLength, sha256: sha256(bytes), url: catalogUrl(origin, file.routePath), }; @@ -185,8 +185,8 @@ export async function buildAgentSkillCatalog(origin: string): Promise { test("substituteCommandTemplate resolves all three non-secret placeholders", () => { const resolved = substituteCommandTemplate( "npx -y @pdpp/local-collector run --base-url --connector ", - { providerUrl: "https://pdpp.example.com", connectorId: "claude-code", connectionId: "cin_x" } + { connectionId: "cin_x", connectorId: "claude-code", providerUrl: "https://pdpp.example.com" } ); assert.equal( resolved, @@ -212,7 +212,7 @@ test("substituteCommandTemplate resolves all three non-secret placeholders", () test("substituteCommandTemplate resolves the connection-id placeholder", () => { const resolved = substituteCommandTemplate( "npx -y @pdpp/local-collector retry-dead-letters --connection-id ", - { providerUrl: null, connectorId: null, connectionId: "cin_laptop" } + { connectionId: "cin_laptop", connectorId: null, providerUrl: null } ); assert.equal(resolved, "npx -y @pdpp/local-collector retry-dead-letters --connection-id cin_laptop"); }); @@ -220,7 +220,7 @@ test("substituteCommandTemplate resolves the connection-id placeholder", () => { test("substituteCommandTemplate resolves the source-instance-id placeholder", () => { const resolved = substituteCommandTemplate( "npx -y @pdpp/local-collector recover --source-instance-id --apply", - { providerUrl: null, connectorId: null, connectionId: "cin_laptop", sourceInstanceId: "dsrc_laptop" } + { connectionId: "cin_laptop", connectorId: null, providerUrl: null, sourceInstanceId: "dsrc_laptop" } ); assert.equal(resolved, "npx -y @pdpp/local-collector recover --source-instance-id dsrc_laptop --apply"); }); @@ -231,25 +231,25 @@ test("substituteCommandTemplate FAILS CLOSED when a placeholder is unresolved", // explicit "unavailable" state instead of a broken command. assert.equal( substituteCommandTemplate("... --connection-id ", { - providerUrl: null, - connectorId: null, connectionId: null, + connectorId: null, + providerUrl: null, }), null ); assert.equal( substituteCommandTemplate("run --base-url --connector ", { - providerUrl: "https://x", - connectorId: null, connectionId: "cin_x", + connectorId: null, + providerUrl: "https://x", }), null ); assert.equal( substituteCommandTemplate("recover --source-instance-id ", { - providerUrl: null, - connectorId: null, connectionId: "cin_x", + connectorId: null, + providerUrl: null, sourceInstanceId: null, }), null diff --git a/apps/site/next.config.mjs b/apps/site/next.config.mjs index 561cf664a..04e7e8b07 100644 --- a/apps/site/next.config.mjs +++ b/apps/site/next.config.mjs @@ -24,13 +24,21 @@ const buildWorkers = parseBuildWorkers(process.env.PDPP_WEB_BUILD_WORKERS); /** @type {import('next').NextConfig} */ const nextConfig = { ...(allowedDevOrigins.length > 0 ? { allowedDevOrigins } : {}), + experimental: { + // The default is host CPU count minus one (23 on the owner workstation), + // which repeatedly trips native SIGSEGV / SIGTRAP during production + // builds on Next 16.2.x. Keep the canonical build stable by default while + // still allowing CI/operators to raise it intentionally. + cpus: buildWorkers, + }, output: "standalone", - outputFileTracingRoot: path.join(__dirname, "../.."), // Runtime file reads outside the bundled output need explicit tracing // includes so Next copies them into the standalone deploy. Without these, // /reference/coverage, the well-known agent-skill catalog, and /llms-full.txt // 500 on Vercel because the markdown they read is absent. outputFileTracingIncludes: { + "/llms-full.txt": ["../../docs/agent-skills/**/*.md", "../../openspec/README.md", "../../pnpm-workspace.yaml"], + "/llms.txt": ["../../docs/agent-skills/**/*.md", "../../openspec/README.md", "../../pnpm-workspace.yaml"], // resolveRepoRoot() looks for a directory containing both // pnpm-workspace.yaml and openspec/, so a stub openspec marker is needed // even for routes that only read docs/agent-skills. @@ -39,100 +47,87 @@ const nextConfig = { "../../openspec/README.md", "../../pnpm-workspace.yaml", ], - "/llms-full.txt": ["../../docs/agent-skills/**/*.md", "../../openspec/README.md", "../../pnpm-workspace.yaml"], - "/llms.txt": ["../../docs/agent-skills/**/*.md", "../../openspec/README.md", "../../pnpm-workspace.yaml"], }, + outputFileTracingRoot: path.join(__dirname, "../.."), reactStrictMode: true, - experimental: { - // The default is host CPU count minus one (23 on the owner workstation), - // which repeatedly trips native SIGSEGV / SIGTRAP during production - // builds on Next 16.2.x. Keep the canonical build stable by default while - // still allowing CI/operators to raise it intentionally. - cpus: buildWorkers, - }, - // Transpile the reference-implementation workspace package so Next can - // consume its TypeScript sources directly once shim pairs (.js + .d.ts) - // collapse into single .ts exports. Without this, Next's bundler would - // reject .ts entries from a node_modules-resolved workspace package. - transpilePackages: ["pdpp-reference-implementation", "@pdpp/brand", "@pdpp/operator-ui"], async redirects() { return [ { - source: "/spec-core", destination: "/docs/spec-core", permanent: true, + source: "/spec-core", }, { - source: "/spec-collection-profile", destination: "/docs/spec-collection-profile", permanent: true, + source: "/spec-collection-profile", }, { - source: "/spec-architecture", destination: "/docs/spec-architecture", permanent: true, + source: "/spec-architecture", }, { - source: "/spec-auth-design", destination: "/docs/spec-auth-design", permanent: true, + source: "/spec-auth-design", }, { - source: "/spec-change-tracking", destination: "/docs/spec-change-tracking", permanent: true, + source: "/spec-change-tracking", }, { - source: "/spec-connector-ecosystem", destination: "/docs/spec-connector-ecosystem", permanent: true, + source: "/spec-connector-ecosystem", }, { - source: "/spec-data-query-api", destination: "/docs/spec-data-query-api", permanent: true, + source: "/spec-data-query-api", }, { - source: "/spec-deferred", destination: "/docs/spec-deferred", permanent: true, + source: "/spec-deferred", }, { - source: "/spec-dti-alignment", destination: "/docs/spec-dti-alignment", permanent: true, + source: "/spec-dti-alignment", }, { - source: "/spec-e2e-examples", destination: "/docs/reference-implementation-examples", permanent: true, + source: "/spec-e2e-examples", }, { - source: "/spec-reference-implementation-examples", destination: "/docs/reference-implementation-examples", permanent: true, + source: "/spec-reference-implementation-examples", }, { - source: "/e2e", destination: "/docs/reference-implementation", permanent: true, + source: "/e2e", }, { - source: "/e2e/:path*", destination: "/docs/reference-implementation", permanent: true, + source: "/e2e/:path*", }, { - source: "/reference-implementation", destination: "/docs/reference-implementation", permanent: true, + source: "/reference-implementation", }, // Sandbox IA parity: sandbox/records/timeline redirects to sandbox/explore // now that the sandbox Explore canvas exists. { - source: "/sandbox/records/timeline", destination: "/sandbox/explore", permanent: false, + source: "/sandbox/records/timeline", }, // NOTE: old owner-console redirects were dropped when the operator // console moved to `apps/console`. The public site owns no live @@ -142,25 +137,25 @@ const nextConfig = { async rewrites() { return [ { - source: "/docs.mdx", destination: "/llms.mdx/docs", + source: "/docs.mdx", }, { - source: "/docs/:path*.mdx", destination: "/llms.mdx/docs/:path*", + source: "/docs/:path*.mdx", }, // Root agent skill discovery. Keep handlers under a filesystem-safe // internal path and expose the standards-shaped public .well-known URL. { - source: "/.well-known/skills/:path*", destination: "/well-known/skills/:path*", + source: "/.well-known/skills/:path*", }, // Agents that probe the well-known namespace first should still land on // the canonical agent-readable entrypoint, so there is no single // universal URL to guess. Compact alias, not duplicate content. { - source: "/.well-known/llms.txt", destination: "/llms.txt", + source: "/.well-known/llms.txt", }, // NOTE: the top-level `/.well-known/oauth-authorization-server` and // `/.well-known/oauth-protected-resource` rewrites were dropped with the @@ -170,23 +165,28 @@ const nextConfig = { // Sandbox demo well-known metadata uses the same internal-path adapter: // handlers live under `well-known/**`; public URLs stay .well-known. { - source: "/sandbox/.well-known/oauth-authorization-server", destination: "/sandbox/well-known/oauth-authorization-server", + source: "/sandbox/.well-known/oauth-authorization-server", }, { - source: "/sandbox/.well-known/oauth-protected-resource", destination: "/sandbox/well-known/oauth-protected-resource", + source: "/sandbox/.well-known/oauth-protected-resource", }, // Sandbox demo reference-only inspection routes. Public path uses // the underscore convention from the live reference (`/_ref/`); the // handlers live under `ref/**` so Next's "private folder" rule does // not exclude them from routing. { - source: "/sandbox/_ref/:path*", destination: "/sandbox/ref/:path*", + source: "/sandbox/_ref/:path*", }, ]; }, + // Transpile the reference-implementation workspace package so Next can + // consume its TypeScript sources directly once shim pairs (.js + .d.ts) + // collapse into single .ts exports. Without this, Next's bundler would + // reject .ts entries from a node_modules-resolved workspace package. + transpilePackages: ["pdpp-reference-implementation", "@pdpp/brand", "@pdpp/operator-ui"], webpack(config) { config.resolve.alias = { ...config.resolve.alias, diff --git a/apps/site/source.config.ts b/apps/site/source.config.ts index 9b83995ea..5ba4de6ab 100644 --- a/apps/site/source.config.ts +++ b/apps/site/source.config.ts @@ -1,17 +1,17 @@ // Copyright The PDP-Connect Contributors // SPDX-License-Identifier: Apache-2.0 -import { defineConfig, defineDocs } from "fumadocs-mdx/config"; import { metaSchema, pageSchema } from "fumadocs-core/source/schema"; -import { remarkLegacyHeadingIds } from "@/lib/remark-legacy-heading-ids"; +import { defineConfig, defineDocs } from "fumadocs-mdx/config"; +import { remarkLegacyHeadingIds } from "@/lib/remark-legacy-heading-ids.ts"; export const docs = defineDocs({ dir: "content/docs", docs: { - schema: pageSchema, postprocess: { includeProcessedMarkdown: true, }, + schema: pageSchema, }, meta: { schema: metaSchema, diff --git a/apps/site/src/app/apple-icon.tsx b/apps/site/src/app/apple-icon.tsx index 023f8e8d6..d915776c6 100644 --- a/apps/site/src/app/apple-icon.tsx +++ b/apps/site/src/app/apple-icon.tsx @@ -4,7 +4,7 @@ import { LAUNCH_COLORS } from "@pdpp/brand/launch-colors"; import { ImageResponse } from "next/og"; -export const size = { width: 180, height: 180 }; +export const size = { height: 180, width: 180 }; export const contentType = "image/png"; // Apple touch icon — the full split-P on the LIGHT launch background. @@ -20,12 +20,12 @@ export default function AppleIcon() { return new ImageResponse(
diff --git a/apps/site/src/app/design/page.tsx b/apps/site/src/app/design/page.tsx index af67f4f9b..221def549 100644 --- a/apps/site/src/app/design/page.tsx +++ b/apps/site/src/app/design/page.tsx @@ -102,8 +102,8 @@ export default function DesignSystemPage() {
@@ -125,9 +125,9 @@ export default function DesignSystemPage() { className="rounded px-2 py-0.5 font-mono text-[10px]" key={t} style={{ - color: "var(--muted-foreground)", - border: "1px solid var(--border)", backgroundColor: "var(--muted)", + border: "1px solid var(--border)", + color: "var(--muted-foreground)", }} > {t} @@ -146,7 +146,7 @@ export default function DesignSystemPage() { {/* Bottom-left quadrant — sticky nav */}
scrollTo(id)} style={{ - fontSize: "0.8125rem", + backgroundColor: active === id ? "var(--muted)" : "transparent", color: active === id ? "var(--foreground)" : "var(--muted-foreground)", + fontSize: "0.8125rem", fontWeight: active === id ? 500 : 400, - backgroundColor: active === id ? "var(--muted)" : "transparent", }} type="button" > @@ -211,9 +211,9 @@ function MobileNav({ active, scrollTo }: { active: string; scrollTo: (id: string
@@ -223,8 +223,8 @@ function MobileNav({ active, scrollTo }: { active: string; scrollTo: (id: string key={id} onClick={() => scrollTo(id)} style={{ - color: active === id ? "var(--foreground)" : "var(--muted-foreground)", borderBottom: active === id ? "2px solid var(--foreground)" : "2px solid transparent", + color: active === id ? "var(--foreground)" : "var(--muted-foreground)", marginBottom: "-1px", }} type="button" @@ -243,7 +243,7 @@ function SectionWrap({ id, children }: { id: string; children: React.ReactNode }
{children}
@@ -259,7 +259,7 @@ function SectionHeader({ title, description }: { title: string; description?: st {description && (

{description}

@@ -326,7 +326,7 @@ function BrandSection() { {/* Construction + rules */}
@@ -369,7 +369,7 @@ function BrandSection() { PDPP @@ -385,12 +385,12 @@ function BrandSection() {
PDPP v0.1 @@ -420,7 +420,7 @@ function BrandSection() {
@@ -431,7 +431,7 @@ function BrandSection() { PDPP @@ -474,8 +474,8 @@ function MarkPlate({ surface, caption, meta }: { surface: "light" | "dark"; capt
- +
@@ -865,7 +865,7 @@ function TypographySection() {
Geist Sans — interface copy
-
+
@@ -890,57 +890,57 @@ function TypographySection() { { className: "pdpp-display-lg", label: "display-lg", - spec: "60/600/-0.03em", sample: "Personal Data", + spec: "60/600/-0.03em", usage: "Splash heroes — the landing page only", }, { className: "pdpp-display", label: "display", - spec: "40/600/-0.025em", sample: "Design System", + spec: "40/600/-0.025em", usage: "Page heroes — docs, design system", }, { className: "pdpp-heading", label: "heading", - spec: "20/600/-0.01em", sample: "Grant request", + spec: "20/600/-0.01em", usage: "Section headers", }, { className: "pdpp-title", label: "title", - spec: "14/600", sample: "Longview", + spec: "14/600", usage: "Card titles, entity names", }, { className: "pdpp-body-lg", label: "body-lg", - spec: "18/400", sample: "An authorization and disclosure protocol.", + spec: "18/400", usage: "Hero lead copy", }, { className: "pdpp-body", label: "body", - spec: "14/400", sample: "Comparing salary, equity, benefits, and tax tradeoffs.", + spec: "14/400", usage: "Descriptions, prose", }, { className: "pdpp-label", label: "label", - spec: "12/500", sample: "What they can access", + spec: "12/500", usage: "Field labels, section labels", }, { className: "pdpp-caption", label: "caption", - spec: "12/400", sample: "No live scraping required.", + spec: "12/400", usage: "Helper text, footnotes", }, ].map(({ className, label, spec, sample, usage }) => ( @@ -950,15 +950,15 @@ function TypographySection() { .{className} - {[ { - step: "heading", - utility: "text-pdpp-heading font-semibold tracking-[-0.01em]", className: "pdpp-heading", sample: "Grant request", spec: "20/600", + step: "heading", + utility: "text-pdpp-heading font-semibold tracking-[-0.01em]", }, { - step: "title", - utility: "text-pdpp-title font-semibold", className: "pdpp-title", sample: "Longview", spec: "14/600", + step: "title", + utility: "text-pdpp-title font-semibold", }, { - step: "body-lg", - utility: "text-pdpp-body-lg", className: "pdpp-body-lg", sample: "An authorization and disclosure protocol.", spec: "18/400", + step: "body-lg", + utility: "text-pdpp-body-lg", }, { - step: "body", - utility: "text-pdpp-body", className: "pdpp-body", sample: "Comparing salary, equity, benefits, and tax tradeoffs.", spec: "14/400", + step: "body", + utility: "text-pdpp-body", }, { - step: "label", - utility: "text-pdpp-label font-medium", className: "pdpp-label", sample: "What they can access", spec: "12/500", + step: "label", + utility: "text-pdpp-label font-medium", }, { - step: "caption", - utility: "text-pdpp-caption", className: "pdpp-caption", sample: "Helper copy.", spec: "12/400", + step: "caption", + utility: "text-pdpp-caption", }, ].map(({ step, className, utility, sample, spec }) => ( @@ -1064,7 +1064,7 @@ function TypographySection() { {step} - -
+ {sample} @@ -1016,46 +1016,46 @@ function TypographySection() {
+ .{className} + {utility} @@ -1117,8 +1117,8 @@ function TypographySection() {
{["Role", "Specimen"].map((h) => ( @@ -1129,30 +1129,30 @@ function TypographySection() {
{[ { + color: "var(--foreground)", label: "id", sample: "grt_8f3a2b1c4d5e6f7a8b9c0d1e2f3a4b5c", usage: "Grant IDs, resource identifiers", - color: "var(--foreground)", }, { + color: "var(--foreground)", label: "code", sample: "pay_statements · summary · continuous", usage: "Stream names, field names, enum values", - color: "var(--foreground)", }, { + color: "var(--edu-fg)", label: "spec-ref", sample: "§4.2 Selection Request · §6.1 Stream Metadata", usage: "Protocol spec citations only", - color: "var(--edu-fg)", }, ].map(({ label, sample, color }) => (
@@ -1190,10 +1190,10 @@ function SpacingSection() {
- +
@@ -1230,11 +1230,11 @@ function SpacingSection() { {[ - { pattern: "px-4 py-2", value: "16px 8px", usage: "Panel headers, toolbar rows" }, - { pattern: "p-4 / p-5", value: "16px / 20px", usage: "Card content, form sections" }, - { pattern: "gap-2 / gap-3", value: "8px / 12px", usage: "Tight list items, inline groups" }, - { pattern: "gap-6 / gap-8", value: "24px / 32px", usage: "Section-level spacing" }, - { pattern: "px-6 py-8", value: "24px 32px", usage: "Stage / centered empty states" }, + { pattern: "px-4 py-2", usage: "Panel headers, toolbar rows", value: "16px 8px" }, + { pattern: "p-4 / p-5", usage: "Card content, form sections", value: "16px / 20px" }, + { pattern: "gap-2 / gap-3", usage: "Tight list items, inline groups", value: "8px / 12px" }, + { pattern: "gap-6 / gap-8", usage: "Section-level spacing", value: "24px / 32px" }, + { pattern: "px-6 py-8", usage: "Stage / centered empty states", value: "24px 32px" }, ].map(({ pattern, value, usage }) => (
@@ -1269,24 +1269,24 @@ function SpacingSection() { // ─── 04 Elevation ───────────────────────────────────────────────────────────── const ELEVATION_LEVELS = [ - { level: 0, label: "Flat", shadow: "none", desc: "Page surface, rows, default panels" }, + { desc: "Page surface, rows, default panels", label: "Flat", level: 0, shadow: "none" }, { - level: 1, + desc: "Cards, form fields", label: "Raised", + level: 1, shadow: "0 1px 2px rgba(0,0,0,0.04), 0 1px 3px rgba(0,0,0,0.02)", - desc: "Cards, form fields", }, { - level: 2, + desc: "Dropdowns, command palette", label: "Float", + level: 2, shadow: "0 4px 6px rgba(0,0,0,0.04), 0 10px 15px rgba(0,0,0,0.08)", - desc: "Dropdowns, command palette", }, { - level: 3, + desc: "Modals, overlays", label: "Modal", + level: 3, shadow: "0 10px 15px rgba(0,0,0,0.05), 0 20px 25px rgba(0,0,0,0.10), 0 0 0 1px rgba(0,0,0,0.05)", - desc: "Modals, overlays", }, ]; @@ -1359,20 +1359,20 @@ function MotionSection() { Semantic aliases
{[ - { token: "--motion-enter", composes: "300ms ease-enter", usage: "Modals, drawers, toasts arriving" }, + { composes: "300ms ease-enter", token: "--motion-enter", usage: "Modals, drawers, toasts arriving" }, { - token: "--motion-exit", composes: "100ms ease-exit", + token: "--motion-exit", usage: "Any element leaving — exits are faster than enters", }, { - token: "--motion-state", composes: "200ms ease-standard", + token: "--motion-state", usage: "Button hover, checkbox, toggle, tab switch", }, { - token: "--motion-feedback", composes: "100ms ease-spring", + token: "--motion-feedback", usage: "Success flash, error indication, confirm action", }, ].map(({ token, composes, usage }) => ( @@ -1411,8 +1411,8 @@ function MotionSection() {
{`@media (prefers-reduced-motion: reduce) {
@@ -1445,10 +1445,10 @@ function MotionSection() {
 function DurationDemo() {
   const [playing, setPlaying] = useState(null);
   const tiers = [
-    { name: "fast", ms: 100, cssVar: "--duration-fast" },
-    { name: "base", ms: 200, cssVar: "--duration-base" },
-    { name: "moderate", ms: 300, cssVar: "--duration-moderate" },
-    { name: "slow", ms: 500, cssVar: "--duration-slow" },
+    { cssVar: "--duration-fast", ms: 100, name: "fast" },
+    { cssVar: "--duration-base", ms: 200, name: "base" },
+    { cssVar: "--duration-moderate", ms: 300, name: "moderate" },
+    { cssVar: "--duration-slow", ms: 500, name: "slow" },
   ];
 
   const play = (name: string, ms: number) => {
@@ -1474,11 +1474,11 @@ function DurationDemo() {
             
@@ -1494,10 +1494,10 @@ function DurationDemo() { function EasingDemo() { const [active, setActive] = useState(null); const curves = [ - { name: "enter", cssVar: "--ease-enter", desc: "Decelerate — arrivals" }, - { name: "exit", cssVar: "--ease-exit", desc: "Accelerate — departures" }, - { name: "standard", cssVar: "--ease-standard", desc: "Full arc — state changes" }, - { name: "spring", cssVar: "--ease-spring", desc: "Overshoot — feedback" }, + { cssVar: "--ease-enter", desc: "Decelerate — arrivals", name: "enter" }, + { cssVar: "--ease-exit", desc: "Accelerate — departures", name: "exit" }, + { cssVar: "--ease-standard", desc: "Full arc — state changes", name: "standard" }, + { cssVar: "--ease-spring", desc: "Overshoot — feedback", name: "spring" }, ]; const play = (name: string) => { @@ -1517,10 +1517,10 @@ function EasingDemo() {
@@ -1557,10 +1557,10 @@ function StaggerDemo() { border: "1px solid var(--border)", opacity: visible ? 1 : 0, transform: visible ? "translateY(0)" : "translateY(6px)", - transitionProperty: phase === "playing" ? "opacity, transform" : "none", + transitionDelay: phase === "playing" ? `${i * 50}ms` : "0ms", transitionDuration: "var(--duration-moderate)", + transitionProperty: phase === "playing" ? "opacity, transform" : "none", transitionTimingFunction: "var(--ease-enter)", - transitionDelay: phase === "playing" ? `${i * 50}ms` : "0ms", }} > @@ -1587,7 +1587,7 @@ function SkeletonDemo() {
@@ -1672,11 +1672,11 @@ function SurfacesSection() { {/* Human row */}
Alex Rivera
@@ -1687,11 +1687,11 @@ function SurfacesSection() { {/* Protocol row */}
@@ -1699,7 +1699,7 @@ function SurfacesSection() {
expires 24h · PDPP v0.1.0
@@ -1711,18 +1711,18 @@ function SurfacesSection() { {[ { attr: "--human", - value: "oklch(0.52 0.09 45)", desc: "2px left border on human surfaces (identity, ownership, consent)", + value: "oklch(0.52 0.09 45)", }, { attr: "--human-wash", - value: "oklch(0.52 0.09 45 / 0.07)", desc: "Gradient wash tint — linear-gradient to right, fades to transparent", + value: "oklch(0.52 0.09 45 / 0.07)", }, { attr: "--primary", - value: "oklch(0.580 0.172 253.7)", desc: "2px left border on protocol surfaces (tokens, grants, spec data)", + value: "oklch(0.580 0.172 253.7)", }, ].map(({ attr, value, desc }) => (
Alex Rivera
@@ -1777,11 +1777,11 @@ function SurfacesSection() { {/* Protocol row inside the card */}
@@ -1789,7 +1789,7 @@ function SurfacesSection() {
expires 24h · PDPP v0.1.0
@@ -2048,7 +2048,7 @@ function DashboardPrimitivesSection() {

= { - recommended: { - color: "var(--primary)", - backgroundColor: "color-mix(in oklab, var(--primary) 8%, var(--surface-tint))", - border: "1px solid color-mix(in oklab, var(--primary) 18%, var(--border))", - label: "Recommended", - }, alternative: { - color: "var(--foreground)", backgroundColor: "color-mix(in oklab, var(--human-wash) 82%, var(--surface-tint))", border: "1px solid color-mix(in oklab, var(--human) 18%, var(--border))", + color: "var(--foreground)", label: "Alternative", }, discard: { - color: "var(--muted-foreground)", backgroundColor: "var(--muted)", border: "1px solid var(--border)", + color: "var(--muted-foreground)", label: "Discard", }, + recommended: { + backgroundColor: "color-mix(in oklab, var(--primary) 8%, var(--surface-tint))", + border: "1px solid color-mix(in oklab, var(--primary) 18%, var(--border))", + color: "var(--primary)", + label: "Recommended", + }, }; function LongviewCandidatePreview({ candidate }: { candidate: LongviewLogoCandidate }) { @@ -2934,9 +2934,9 @@ function ExampleWorldsSection() { className="rounded-full px-2.5 py-1.5 text-xs" key={name} style={{ - color: "var(--foreground)", backgroundColor: "color-mix(in oklab, var(--muted) 44%, var(--surface-tint))", border: "1px solid var(--border)", + color: "var(--foreground)", }} > {name} @@ -3038,9 +3038,9 @@ function ExampleWorldsSection() { {world.anchorStream.name} @@ -3048,9 +3048,9 @@ function ExampleWorldsSection() { {world.anchorStream.cadence} @@ -3078,9 +3078,9 @@ function ExampleWorldsSection() { className="rounded-full px-2.5 py-1.5 text-xs" key={source} style={{ - color: "var(--foreground)", backgroundColor: "color-mix(in oklab, var(--muted) 42%, var(--surface-tint))", border: "1px solid var(--border)", + color: "var(--foreground)", }} > {source} @@ -3102,9 +3102,9 @@ function ExampleWorldsSection() { className="rounded-full px-2.5 py-1.5 text-xs" key={item} style={{ - color: "var(--foreground)", backgroundColor: "color-mix(in oklab, var(--background) 75%, var(--human-wash))", border: "1px solid color-mix(in oklab, var(--human) 18%, var(--border))", + color: "var(--foreground)", }} > {item} @@ -3236,9 +3236,9 @@ function ExampleWorldsSection() { className="rounded-full px-2.5 py-1 text-xs" key={field} style={{ - color: "var(--foreground)", backgroundColor: "color-mix(in oklab, var(--primary) 10%, var(--surface-tint))", border: "1px solid color-mix(in oklab, var(--primary) 16%, var(--border))", + color: "var(--foreground)", }} > {field} @@ -3260,9 +3260,9 @@ function ExampleWorldsSection() { className="rounded-full px-2.5 py-1 text-xs" key={field} style={{ - color: "var(--muted-foreground)", backgroundColor: "var(--muted)", border: "1px solid var(--border)", + color: "var(--muted-foreground)", }} > {field} @@ -3304,14 +3304,14 @@ function DocsSection() { Docs shell — masthead, sidebar, TOC
@@ -3337,10 +3337,10 @@ function DocsSection() {
{field} @@ -3543,8 +3543,8 @@ function DocsSection() {
@@ -3674,178 +3674,178 @@ function SpecimenSwitcher({ const CONSENT_SPECIMENS: { label: string; axes: string; data: ConsentCardProps }[] = [ { - // Axes: 1=continuous, 2=research, 4=delete, 5=present, 6=date, 7=mixed, 13=verified, 15=present, 16=present, 17=multiple - label: "Planning (baseline)", axes: "continuous, planning, verified, retention:delete, expiry, optional stream, commitments", data: { - requester: { name: "Longview", monogram: "LV", verified: true }, - purpose: - "Longview is requesting compensation records to compare salary, equity, benefits, and tax tradeoffs before a career move.", + accessMode: "continuous", commitments: [ "Analysis stays inside this planning workspace", "No employer outreach or document sharing without separate approval", ], + optional: { + consequenceOff: "Turned off. The rest of the grant is unaffected.", + consequenceOn: "Improves plan comparison and exposes coverage tradeoffs.", + detail: "Plan name, coverage tier, and employer contribution. No dependent details, claims, or provider notes.", + key: "benefits_enrollments", + label: "Benefits enrollments", + }, + purpose: + "Longview is requesting compensation records to compare salary, equity, benefits, and tax tradeoffs before a career move.", + requester: { monogram: "LV", name: "Longview", verified: true }, streams: [ { - key: "pay_statements", - label: "Pay statements", detail: "Employer, pay period, gross pay, and net pay. No bank account details, home address, or tax ID fragments.", + key: "pay_statements", + label: "Pay statements", }, { - key: "equity_grants", - label: "Equity grants", detail: "Grant type, quantity, vesting start, and vesting schedule. No brokerage account numbers or beneficiary details.", + key: "equity_grants", + label: "Equity grants", }, ], - optional: { - key: "benefits_enrollments", - label: "Benefits enrollments", - detail: "Plan name, coverage tier, and employer contribution. No dependent details, claims, or provider notes.", - consequenceOn: "Improves plan comparison and exposes coverage tradeoffs.", - consequenceOff: "Turned off. The rest of the grant is unaffected.", - }, - accessMode: "continuous", - technical: { clientId: "longview_planning_v1", purposeCode: "planning", grantExpires: "Apr 15, 2027" }, + technical: { clientId: "longview_planning_v1", grantExpires: "Apr 15, 2027", purposeCode: "planning" }, }, + // Axes: 1=continuous, 2=research, 4=delete, 5=present, 6=date, 7=mixed, 13=verified, 15=present, 16=present, 17=multiple + label: "Planning (baseline)", }, { - // Axes: 1=single_use, 2=personalization, 6=null(no expiry), 7=all required, 8=time_range, 15=absent, 17=single stream - label: "Single use, no expiry", axes: "single_use, personalization, no expiry, no optional, time_range, no commitments, single stream", data: { - requester: { name: "Concert Finder", monogram: "CF", verified: true }, - purpose: "Concert Finder wants your top artists to recommend upcoming shows near you.", + accessMode: "single_use", commitments: [], + purpose: "Concert Finder wants your top artists to recommend upcoming shows near you.", + requester: { monogram: "CF", name: "Concert Finder", verified: true }, streams: [ { - key: "top_artists", - label: "Your top artists", detail: "Artist names, genres, and popularity scores from the last 6 months. No listening timestamps or play counts.", + key: "top_artists", + label: "Your top artists", }, ], - accessMode: "single_use", - technical: { clientId: "concert_finder", purposeCode: "personalization", grantExpires: "No expiry" }, + technical: { clientId: "concert_finder", grantExpires: "No expiry", purposeCode: "personalization" }, }, + // Axes: 1=single_use, 2=personalization, 6=null(no expiry), 7=all required, 8=time_range, 15=absent, 17=single stream + label: "Single use, no expiry", }, { - // Axes: 2=ai_training(#3), 4=anonymize, 12=present, 13=unverified, 14=logo suppressed - label: "AI training, unverified", axes: "ai_training (explicit consent), unverified client, retention:anonymize, continuous", data: { - requester: { name: "DataCo ML Pipeline", monogram: "DC", verified: false }, - purpose: "DataCo ML Pipeline wants to use your social media data to train recommendation models.", + accessMode: "continuous", commitments: ["Model weights only, raw data not retained"], + purpose: "DataCo ML Pipeline wants to use your social media data to train recommendation models.", + requester: { monogram: "DC", name: "DataCo ML Pipeline", verified: false }, streams: [ { + detail: "Post captions, dates, and engagement metrics. No private messages or stories.", key: "posts", label: "Your posts", - detail: "Post captions, dates, and engagement metrics. No private messages or stories.", }, - { key: "following", label: "Who you follow", detail: "Account IDs and usernames. No DMs or profile details." }, + { detail: "Account IDs and usernames. No DMs or profile details.", key: "following", label: "Who you follow" }, ], - accessMode: "continuous", - technical: { clientId: "dataco_ml_v2", purposeCode: "ai_training", grantExpires: "Jan 1, 2028" }, + technical: { clientId: "dataco_ml_v2", grantExpires: "Jan 1, 2028", purposeCode: "ai_training" }, }, + // Axes: 2=ai_training(#3), 4=anonymize, 12=present, 13=unverified, 14=logo suppressed + label: "AI training, unverified", }, { - // Axes: 2=export, 4=absent(no retention), 5=absent, 15=absent, 16=absent(no purpose_description) - label: "Self-export, minimal", axes: "export, single_use, no retention, no commitments, no purpose_description fallback", data: { - requester: { name: "PDPP Export Tool", monogram: "PE", verified: true }, - purpose: "Export your data for personal use.", + accessMode: "single_use", commitments: [], + purpose: "Export your data for personal use.", + requester: { monogram: "PE", name: "PDPP Export Tool", verified: true }, streams: [ { + detail: "Complete following list with account IDs and usernames.", key: "following", label: "Who you follow", - detail: "Complete following list with account IDs and usernames.", }, { + detail: "All post data including captions, dates, media types, and locations.", key: "posts", label: "Your posts", - detail: "All post data including captions, dates, media types, and locations.", }, { + detail: "Full ad targeting profile with categories, sources, and confidence scores.", key: "ad_targeting", label: "Ad interest categories", - detail: "Full ad targeting profile with categories, sources, and confidence scores.", }, ], - accessMode: "single_use", - technical: { clientId: "pdpp_export", purposeCode: "export", grantExpires: "24 hours" }, + technical: { clientId: "pdpp_export", grantExpires: "24 hours", purposeCode: "export" }, }, + // Axes: 2=export, 4=absent(no retention), 5=absent, 15=absent, 16=absent(no purpose_description) + label: "Self-export, minimal", }, { - // Axes: 2=agent_context, 1=continuous, 6=null, 17=single, 12=absent(client_display missing, fall back to client_id) - label: "AI agent, no display", axes: "agent_context, continuous, no expiry, no client_display (client_id fallback)", data: { - requester: { name: "agt_personal_v3", monogram: "AG", verified: false }, - purpose: "Requesting ongoing access to provide personalized context to your AI agent.", + accessMode: "continuous", commitments: ["Data processed locally, never sent to external servers"], + purpose: "Requesting ongoing access to provide personalized context to your AI agent.", + requester: { monogram: "AG", name: "agt_personal_v3", verified: false }, streams: [ { + detail: "Message content, timestamps, and participants. Includes DMs.", key: "messages", label: "Your messages", - detail: "Message content, timestamps, and participants. Includes DMs.", }, ], - accessMode: "continuous", - technical: { clientId: "agt_personal_v3", purposeCode: "agent_context", grantExpires: "No expiry" }, + technical: { clientId: "agt_personal_v3", grantExpires: "No expiry", purposeCode: "agent_context" }, }, + // Axes: 2=agent_context, 1=continuous, 6=null, 17=single, 12=absent(client_display missing, fall back to client_id) + label: "AI agent, no display", }, { - // Axes: 2=analytics, 8=since+until, 18=profile used - label: "Analytics, scoped time window", axes: "analytics, single_use, time_range with since+until, profile-based", data: { - requester: { name: "Sleep Insights", monogram: "SI", verified: true }, - purpose: "Sleep Insights wants to analyze your sleep data from Q1 2026 to identify patterns.", + accessMode: "single_use", commitments: ["Analysis results shared back with you", "Raw data deleted after analysis completes"], + purpose: "Sleep Insights wants to analyze your sleep data from Q1 2026 to identify patterns.", + requester: { monogram: "SI", name: "Sleep Insights", verified: true }, streams: [ { + detail: "Sleep duration, scores, and stage breakdowns for Jan-Mar 2026. No heart rate or HRV data.", key: "sleep_sessions", label: "Sleep sessions", - detail: "Sleep duration, scores, and stage breakdowns for Jan-Mar 2026. No heart rate or HRV data.", }, ], - accessMode: "single_use", - technical: { clientId: "sleep_insights_v1", purposeCode: "analytics", grantExpires: "7 days" }, + technical: { clientId: "sleep_insights_v1", grantExpires: "7 days", purposeCode: "analytics" }, }, + // Axes: 2=analytics, 8=since+until, 18=profile used + label: "Analytics, scoped time window", }, { - // Axis 17 (multiple connections): a grant covering two Gmail accounts of - // the same connector type. The per-connection sub-rows are built through - // `buildConsentCardConnections`, so the never-renamed connection renders - // an owner-meaningful `Gmail · account 2` default rather than its raw - // storage placeholder. Proves the no-placeholder consent invariant - // (openspec/changes/expose-connection-identity-on-public-read). - label: "Multi-connection (same connector)", axes: "continuous, agent_context, two connections of one connector, owner-meaningful default label", data: { - requester: { name: "Inbox Assistant", monogram: "IA", verified: true }, - purpose: "Inbox Assistant wants to triage messages across both of your connected Gmail accounts.", + accessMode: "continuous", commitments: ["Drafts stay local until you send them"], + purpose: "Inbox Assistant wants to triage messages across both of your connected Gmail accounts.", + requester: { monogram: "IA", name: "Inbox Assistant", verified: true }, streams: [ { - key: "messages", - label: "Your messages", - detail: "Subject, sender, and snippet across the granted accounts. No attachments.", connections: buildConsentCardConnections("gmail", [ { connectionId: "cin_gmail_personal", displayName: "Personal Gmail" }, // No owner label yet — mapper derives `Gmail · account 2`, never // the registry URL it carries in storage. { connectionId: "cin_gmail_work", displayName: "https://registry.pdpp.org/connectors/gmail" }, ]), + detail: "Subject, sender, and snippet across the granted accounts. No attachments.", + key: "messages", + label: "Your messages", }, ], - accessMode: "continuous", - technical: { clientId: "inbox_assistant_v1", purposeCode: "agent_context", grantExpires: "No expiry" }, + technical: { clientId: "inbox_assistant_v1", grantExpires: "No expiry", purposeCode: "agent_context" }, }, + // Axis 17 (multiple connections): a grant covering two Gmail accounts of + // the same connector type. The per-connection sub-rows are built through + // `buildConsentCardConnections`, so the never-renamed connection renders + // an owner-meaningful `Gmail · account 2` default rather than its raw + // storage placeholder. Proves the no-placeholder consent invariant + // (openspec/changes/expose-connection-identity-on-public-read). + label: "Multi-connection (same connector)", }, ]; @@ -3854,97 +3854,97 @@ const CONSENT_SPECIMENS: { label: string; axes: string; data: ConsentCardProps } const GRANT_SPECIMENS: { label: string; axes: string; data: GrantInspectorProps }[] = [ { - label: "Active, continuous", axes: "active, continuous, retention:delete, view + fields, time_range", data: { + accessMode: "continuous", + client: { clientId: "longview_planning_v1", name: "Longview" }, + expiresAt: "Apr 15, 2027", grantId: "grt_8f3a2b1c", issuedAt: "Apr 6, 2026", - status: "active", - client: { clientId: "longview_planning_v1", name: "Longview" }, purposeCode: "planning", purposeDescription: "Career-move compensation planning", - accessMode: "continuous", - expiresAt: "Apr 15, 2027", retention: { duration: "90 days", onExpiry: "delete" }, + status: "active", streams: [ { - name: "pay_statements", - label: "Pay statements", detail: "Employer, pay period, gross pay, and net pay. No bank account details, home address, or tax ID fragments.", - view: "summary", fields: ["employer", "pay_period", "gross_pay", "net_pay"], + label: "Pay statements", + name: "pay_statements", timeRange: { since: "Jan 1, 2025" }, + view: "summary", }, { - name: "equity_grants", - label: "Equity grants", detail: "Grant type, quantity, vesting start, and vesting schedule. No brokerage account numbers or beneficiary details.", - view: "vesting_summary", fields: ["grant_type", "quantity", "vesting_start", "vesting_schedule"], + label: "Equity grants", + name: "equity_grants", + view: "vesting_summary", }, ], }, + label: "Active, continuous", }, { - label: "Expired", axes: "expired, single_use, no retention, no view (all fields)", data: { + accessMode: "single_use", + client: { clientId: "concert_finder", name: "Concert Finder" }, + expiresAt: "Mar 2, 2026", grantId: "grt_a1b2c3d4", issuedAt: "Mar 1, 2026", - status: "expired", - client: { clientId: "concert_finder", name: "Concert Finder" }, purposeCode: "personalization", purposeDescription: "Concert recommendations", - accessMode: "single_use", - expiresAt: "Mar 2, 2026", + status: "expired", streams: [ - { name: "top_artists", label: "Your top artists", detail: "Artist names, genres, and popularity scores." }, + { detail: "Artist names, genres, and popularity scores.", label: "Your top artists", name: "top_artists" }, ], }, + label: "Expired", }, { - label: "Revoked", axes: "revoked, continuous, retention:anonymize, no expiry", data: { + accessMode: "continuous", + client: { clientId: "dataco_ml_v2", name: "DataCo ML Pipeline" }, + expiresAt: null, grantId: "grt_rev0ked1", issuedAt: "Jan 15, 2026", - status: "revoked", - client: { clientId: "dataco_ml_v2", name: "DataCo ML Pipeline" }, purposeCode: "ai_training", purposeDescription: "Recommendation model training", - accessMode: "continuous", - expiresAt: null, retention: { duration: "6 months", onExpiry: "anonymize" }, + status: "revoked", streams: [ - { name: "posts", label: "Your posts", fields: ["id", "caption", "taken_at", "media_type"] }, - { name: "following_accounts", label: "Who you follow", fields: ["id", "username"] }, + { fields: ["id", "caption", "taken_at", "media_type"], label: "Your posts", name: "posts" }, + { fields: ["id", "username"], label: "Who you follow", name: "following_accounts" }, ], }, + label: "Revoked", }, { - label: "Single use, all fields", axes: "active, single_use, no fields (all authorized), time_range since+until", data: { + accessMode: "single_use", + client: { clientId: "sleep_insights_v1", name: "Sleep Insights" }, + expiresAt: "Apr 8, 2026", grantId: "grt_sleep001", issuedAt: "Apr 1, 2026", - status: "active", - client: { clientId: "sleep_insights_v1", name: "Sleep Insights" }, purposeCode: "analytics", purposeDescription: "Q1 2026 sleep pattern analysis", - accessMode: "single_use", - expiresAt: "Apr 8, 2026", retention: { duration: "30 days", onExpiry: "delete" }, + status: "active", streams: [ { - name: "sleep_sessions", - label: "Sleep sessions", detail: "Sleep duration, scores, and stage breakdowns.", + label: "Sleep sessions", + name: "sleep_sessions", timeRange: { since: "Jan 1, 2026", until: "Apr 1, 2026" }, }, ], }, + label: "Single use, all fields", }, ]; @@ -3953,88 +3953,88 @@ const GRANT_SPECIMENS: { label: string; axes: string; data: GrantInspectorProps const INVENTORY_SPECIMENS: { label: string; axes: string; data: StreamInventoryProps }[] = [ { - label: "Instagram (populated)", axes: "mutable_state + append_only, all synced, nonzero counts", data: { connectorName: "Instagram", connectorVersion: "1.2.0", streams: [ { - name: "following_accounts", - label: "Who you follow", detail: "Usernames and account IDs of accounts you follow. No DMs, profile details, or follower lists.", - semantics: "mutable_state", - recordCount: 106, + label: "Who you follow", lastSynced: "Apr 6, 2026", + name: "following_accounts", + recordCount: 106, + semantics: "mutable_state", }, { - name: "posts", - label: "Your posts", detail: "Post captions, dates, and media types. No comments, likes, or private messages.", - semantics: "append_only", - recordCount: 22, + label: "Your posts", lastSynced: "Apr 6, 2026", + name: "posts", + recordCount: 22, + semantics: "append_only", }, { - name: "ad_targeting", - label: "Ad interest categories", detail: "Ad categories, sources, and confidence scores. No browsing history or purchase data.", - semantics: "mutable_state", - recordCount: 47, + label: "Ad interest categories", lastSynced: "Apr 6, 2026", + name: "ad_targeting", + recordCount: 47, + semantics: "mutable_state", }, ], }, + label: "Instagram (populated)", }, { - label: "Spotify (fresh)", axes: "append_only dominant, one never synced, zero count stream", data: { connectorName: "Spotify", connectorVersion: "2.0.0", streams: [ { - name: "top_artists", - label: "Your top artists", detail: "Artist names, genres, and popularity scores. No listening timestamps or play counts.", - semantics: "mutable_state", - recordCount: 48, + label: "Your top artists", lastSynced: "Apr 5, 2026", + name: "top_artists", + recordCount: 48, + semantics: "mutable_state", }, { - name: "play_events", - label: "Play history", detail: "Track plays with timestamps and durations. No skip or repeat data.", - semantics: "append_only", - recordCount: 1243, + label: "Play history", lastSynced: "Apr 5, 2026", + name: "play_events", + recordCount: 1243, + semantics: "append_only", }, { - name: "saved_tracks", - label: "Saved tracks", detail: "Tracks in your library with save dates.", - semantics: "mutable_state", + label: "Saved tracks", + name: "saved_tracks", recordCount: 0, + semantics: "mutable_state", }, ], }, + label: "Spotify (fresh)", }, { - label: "Oura (single stream)", axes: "single stream, append_only only, never synced", data: { connectorName: "Oura Ring", connectorVersion: "1.0.0", streams: [ { - name: "sleep_sessions", - label: "Sleep sessions", detail: "Sleep duration, scores, and stage breakdowns. No heart rate or HRV data.", - semantics: "append_only", + label: "Sleep sessions", + name: "sleep_sessions", recordCount: 0, + semantics: "append_only", }, ], }, + label: "Oura (single stream)", }, ]; @@ -4044,16 +4044,14 @@ const INVENTORY_SPECIMENS: { label: string; axes: string; data: StreamInventoryP const CONNECTOR_SPECIMENS: { label: string; axes: string; data: ConnectorCardProps }[] = [ { - label: "Instagram (full)", axes: "multiple streams, mixed semantics, fields+time_range, views, no profiles", data: { connectorId: "https://registry.pdpp.org/connectors/instagram", displayName: "Instagram", - version: "1.2.0", streams: [ { - name: "following_accounts", label: "Who you follow", + name: "following_accounts", semantics: "mutable_state", supportsFields: true, supportsResources: false, @@ -4061,8 +4059,8 @@ const CONNECTOR_SPECIMENS: { label: string; axes: string; data: ConnectorCardPro viewCount: 2, }, { - name: "posts", label: "Your posts", + name: "posts", semantics: "append_only", supportsFields: true, supportsResources: false, @@ -4070,8 +4068,8 @@ const CONNECTOR_SPECIMENS: { label: string; axes: string; data: ConnectorCardPro viewCount: 2, }, { - name: "ad_targeting", label: "Ad interest categories", + name: "ad_targeting", semantics: "mutable_state", supportsFields: true, supportsResources: false, @@ -4079,19 +4077,23 @@ const CONNECTOR_SPECIMENS: { label: string; axes: string; data: ConnectorCardPro viewCount: 1, }, ], + version: "1.2.0", }, + label: "Instagram (full)", }, { - label: "Spotify (with profiles)", axes: "profiles present, resources supported, many views", data: { connectorId: "https://registry.pdpp.org/connectors/spotify", displayName: "Spotify", - version: "2.0.0", + profiles: [ + { id: "listening_history", label: "Listening history", streamCount: 2 }, + { id: "library", label: "Full library", streamCount: 3 }, + ], streams: [ { - name: "top_artists", label: "Your top artists", + name: "top_artists", semantics: "mutable_state", supportsFields: true, supportsResources: false, @@ -4099,8 +4101,8 @@ const CONNECTOR_SPECIMENS: { label: string; axes: string; data: ConnectorCardPro viewCount: 2, }, { - name: "play_events", label: "Play history", + name: "play_events", semantics: "append_only", supportsFields: true, supportsResources: false, @@ -4108,8 +4110,8 @@ const CONNECTOR_SPECIMENS: { label: string; axes: string; data: ConnectorCardPro viewCount: 0, }, { - name: "saved_tracks", label: "Saved tracks", + name: "saved_tracks", semantics: "mutable_state", supportsFields: true, supportsResources: true, @@ -4117,8 +4119,8 @@ const CONNECTOR_SPECIMENS: { label: string; axes: string; data: ConnectorCardPro viewCount: 1, }, { - name: "playlists", label: "Playlists", + name: "playlists", semantics: "mutable_state", supportsFields: true, supportsResources: true, @@ -4126,23 +4128,19 @@ const CONNECTOR_SPECIMENS: { label: string; axes: string; data: ConnectorCardPro viewCount: 2, }, ], - profiles: [ - { id: "listening_history", label: "Listening history", streamCount: 2 }, - { id: "library", label: "Full library", streamCount: 3 }, - ], + version: "2.0.0", }, + label: "Spotify (with profiles)", }, { - label: "Oura (minimal)", axes: "single stream, no views, no profiles, no resources, append_only only", data: { connectorId: "https://registry.pdpp.org/connectors/oura", displayName: "Oura Ring", - version: "1.0.0", streams: [ { - name: "sleep_sessions", label: "Sleep sessions", + name: "sleep_sessions", semantics: "append_only", supportsFields: true, supportsResources: false, @@ -4150,15 +4148,15 @@ const CONNECTOR_SPECIMENS: { label: string; axes: string; data: ConnectorCardPro viewCount: 0, }, ], + version: "1.0.0", }, + label: "Oura (minimal)", }, { - label: "Gmail (no labels)", axes: "no display.label (falls back to stream name), mixed capabilities", data: { connectorId: "https://registry.pdpp.org/connectors/gmail", displayName: "Gmail", - version: "1.0.0", streams: [ { name: "messages", @@ -4185,7 +4183,9 @@ const CONNECTOR_SPECIMENS: { label: string; axes: string; data: ConnectorCardPro viewCount: 0, }, ], + version: "1.0.0", }, + label: "Gmail (no labels)", }, ]; @@ -4194,48 +4194,48 @@ const CONNECTOR_SPECIMENS: { label: string; axes: string; data: ConnectorCardPro const CITATION_SPECIMENS: { label: string; axes: string; data: { citations: SpecCitationProps[] } }[] = [ { - label: "Single citation", axes: "single, no href", - data: { citations: [{ section: "5", label: "Selection Request" }] }, + data: { citations: [{ label: "Selection Request", section: "5" }] }, + label: "Single citation", }, { - label: "Citation with link", axes: "single, with href", - data: { citations: [{ section: "6", label: "Grant", href: "/spec-core#grant" }] }, + data: { citations: [{ href: "/spec-core#grant", label: "Grant", section: "6" }] }, + label: "Citation with link", }, { - label: "Citation group", axes: "multiple citations, mixed href", data: { citations: [ - { section: "5", label: "Selection Request", href: "/spec-core#selection-request" }, - { section: "6", label: "Grant" }, - { section: "7", label: "Manifest Format", href: "/spec-core#manifest-format" }, + { href: "/spec-core#selection-request", label: "Selection Request", section: "5" }, + { label: "Grant", section: "6" }, + { href: "/spec-core#manifest-format", label: "Manifest Format", section: "7" }, ], }, + label: "Citation group", }, { - label: "Deep section refs", axes: "subsection numbers, Appendix", data: { citations: [ - { section: "5.1", label: "Client Display" }, - { section: "5.2", label: "Client Claims" }, - { section: "7.1", label: "Stream Display" }, - { section: "A", label: "Purpose Codes" }, + { label: "Client Display", section: "5.1" }, + { label: "Client Claims", section: "5.2" }, + { label: "Stream Display", section: "7.1" }, + { label: "Purpose Codes", section: "A" }, ], }, + label: "Deep section refs", }, { - label: "Consent flow citations", axes: "realistic grouping for consent card context", data: { citations: [ - { section: "5", label: "Selection Request" }, - { section: "6", label: "Grant" }, - { section: "A", label: "Purpose Codes" }, + { label: "Selection Request", section: "5" }, + { label: "Grant", section: "6" }, + { label: "Purpose Codes", section: "A" }, ], }, + label: "Consent flow citations", }, ]; @@ -4255,7 +4255,7 @@ function StatusSection() {
@@ -4279,33 +4279,33 @@ function StatusSection() { {[ { - tokenVal: "--border", label: "Offline / idle", token: "--border", + tokenVal: "--border", usage: "Uninitiated, not started", }, { - tokenVal: "--primary", label: "Active / in progress", token: "--primary", + tokenVal: "--primary", usage: "Running, loading, processing", }, { - tokenVal: "--success", label: "Success / complete", token: "--success", + tokenVal: "--success", usage: "Done, granted, synced", }, { - tokenVal: "--warning", label: "Warning / pending", token: "--warning", + tokenVal: "--warning", usage: "Pending review, attention needed", }, { - tokenVal: "--destructive", label: "Error", token: "--destructive", + tokenVal: "--destructive", usage: "Failed, rejected, revoked", }, ].map(({ tokenVal, label, token, usage }) => ( @@ -4339,10 +4339,10 @@ function StatusSection() { diff --git a/apps/site/src/app/docs/[[...slug]]/page.tsx b/apps/site/src/app/docs/[[...slug]]/page.tsx index 9461072a9..abefa76a0 100644 --- a/apps/site/src/app/docs/[[...slug]]/page.tsx +++ b/apps/site/src/app/docs/[[...slug]]/page.tsx @@ -71,10 +71,10 @@ export async function generateMetadata({ params }: DocsPageProps): Promise diff --git a/apps/site/src/app/globals.css b/apps/site/src/app/globals.css index 8672ef167..f41d9ed48 100644 --- a/apps/site/src/app/globals.css +++ b/apps/site/src/app/globals.css @@ -165,8 +165,8 @@ @layer base { body { - @apply bg-background text-foreground; font-family: var(--font-sans); + @apply bg-background text-foreground; } html, body { @@ -179,17 +179,17 @@ html { } html > body[data-scroll-locked] { - margin-right: 0 !important; --removed-body-scroll-bar-size: 0 !important; + margin-right: 0 !important; } .pdpp-stream-frame { --pdpp-stream-video-matte: oklch(0.08 0.005 260); inline-size: 100%; - block-size: 100%; min-inline-size: 0; - min-block-size: 0; max-width: 100%; + block-size: 100%; + min-block-size: 0; max-height: 100%; background: var(--pdpp-stream-video-matte); } @@ -207,24 +207,24 @@ html > body[data-scroll-locked] { z-index: 50; display: grid; grid-template-rows: 1fr; + gap: 0; inline-size: 100vw; - block-size: 100vh; - block-size: 100svh; max-inline-size: none; width: 100vw; + max-width: none; + block-size: 100vh; + block-size: 100svh; height: 100vh; height: 100svh; - max-width: none; + padding: 0; margin: 0; overflow: hidden; - translate: 0 0; - scale: 1; + background: var(--pdpp-stream-video-matte); border: 0; border-radius: 0; - background: var(--pdpp-stream-video-matte); - padding: 0; box-shadow: none; - gap: 0; + scale: 1; + translate: 0 0; } .pdpp-stream-dialog:is([data-starting-style], [data-ending-style]) { @@ -242,52 +242,52 @@ html > body[data-scroll-locked] { .pdpp-stream-corner-controls { --pdpp-stream-control-offset: 0.75rem; --pdpp-stream-control-gap: 0.5rem; - pointer-events: none; position: absolute; inset-block-start: var(--pdpp-stream-control-offset); inset-inline-end: var(--pdpp-stream-control-offset); z-index: 10; display: flex; flex-direction: column; - align-items: flex-end; gap: var(--pdpp-stream-control-gap); + align-items: flex-end; + pointer-events: none; } .pdpp-stream-control-row { display: flex; - align-items: center; gap: var(--pdpp-stream-control-gap); + align-items: center; } @media (max-width: 40rem), (pointer: coarse) { .pdpp-stream-corner-controls { inset-block-start: auto; - inset-inline-end: 50%; inset-block-end: calc( env(safe-area-inset-bottom) + var(--pdpp-stream-control-offset) ); - translate: 50% 0; + inset-inline-end: 50%; align-items: center; + translate: 50% 0; } } .pdpp-stream-control-button { --pdpp-stream-control-size: 2.75rem; - pointer-events: auto; display: inline-flex; - inline-size: var(--pdpp-stream-control-size); - block-size: var(--pdpp-stream-control-size); align-items: center; justify-content: center; - border-radius: 999px; - background: color-mix(in oklch, var(--background) 90%, transparent); + inline-size: var(--pdpp-stream-control-size); + block-size: var(--pdpp-stream-control-size); color: var(--foreground); - box-shadow: 0 1px 2px color-mix(in oklch, var(--foreground) 12%, transparent); + pointer-events: auto; outline: none; + background: color-mix(in oklch, var(--background) 90%, transparent); + border: 1px solid color-mix(in oklch, var(--border) 70%, transparent); + border-radius: 999px; + box-shadow: 0 1px 2px color-mix(in oklch, var(--foreground) 12%, transparent); backdrop-filter: blur(8px); transition: background-color 150ms ease; - border: 1px solid color-mix(in oklch, var(--border) 70%, transparent); } .pdpp-stream-control-button:hover { @@ -300,17 +300,17 @@ html > body[data-scroll-locked] { } .pdpp-stream-location-label { - pointer-events: auto; display: flex; - max-inline-size: calc(100vw - 2rem); - align-items: baseline; gap: 0.25rem; - border-radius: 999px; - background: color-mix(in oklch, var(--background) 90%, transparent); + align-items: baseline; + max-inline-size: calc(100vw - 2rem); padding: 0.25rem 0.75rem; + pointer-events: auto; + background: color-mix(in oklch, var(--background) 90%, transparent); + border: 1px solid color-mix(in oklch, var(--border) 70%, transparent); + border-radius: 999px; box-shadow: 0 1px 2px color-mix(in oklch, var(--foreground) 12%, transparent); backdrop-filter: blur(8px); - border: 1px solid color-mix(in oklch, var(--border) 70%, transparent); } @media (min-width: 40rem) { @@ -321,19 +321,19 @@ html > body[data-scroll-locked] { .pdpp-stream-toast-zone { --pdpp-stream-toast-offset: 0.75rem; - pointer-events: none; position: absolute; - inset-inline: 0; inset-block-end: calc( env(safe-area-inset-bottom) + var(--pdpp-stream-toast-offset) ); + inset-inline: 0; z-index: 10; display: flex; - max-inline-size: 24rem; justify-content: center; - margin-inline: auto; + max-inline-size: 24rem; padding-inline: 1rem; + margin-inline: auto; + pointer-events: none; } .pdpp-stream-toast-zone[data-slot="popup"] { @@ -350,14 +350,14 @@ html > body[data-scroll-locked] { } .pdpp-stream-toast-bubble { - pointer-events: auto; - border-radius: 0.375rem; - background: color-mix(in oklch, var(--background) 95%, transparent); padding: 0.5rem 0.75rem; color: var(--foreground); + pointer-events: auto; + background: color-mix(in oklch, var(--background) 95%, transparent); + border: 1px solid color-mix(in oklch, var(--border) 70%, transparent); + border-radius: 0.375rem; box-shadow: 0 1px 2px color-mix(in oklch, var(--foreground) 12%, transparent); backdrop-filter: blur(8px); - border: 1px solid color-mix(in oklch, var(--border) 70%, transparent); } .pdpp-stream-clipboard-textarea[data-masked="true"] { @@ -365,8 +365,8 @@ html > body[data-scroll-locked] { } [data-pdpp-neko-client] { - background: var(--pdpp-stream-video-matte); overflow: hidden; + background: var(--pdpp-stream-video-matte); } /* Prevent the host browser (e.g. Brave on Android) from hijacking @@ -433,8 +433,8 @@ html > body[data-scroll-locked] { display: flex !important; width: 100% !important; height: 100% !important; - margin: 0 !important; padding: 0 !important; + margin: 0 !important; background: var(--pdpp-stream-video-matte) !important; } @@ -452,8 +452,8 @@ html > body[data-scroll-locked] { [data-pdpp-neko-client] .emotes { inset: 0 !important; width: 100% !important; - height: 100% !important; max-width: none !important; + height: 100% !important; max-height: none !important; margin: 0 !important; } @@ -480,29 +480,29 @@ html > body[data-scroll-locked] { margin-top: var(--docs-prose-spacing); } .docs-prose h1 { + margin-top: 1.75rem; font-size: 1.6rem; font-weight: 600; letter-spacing: -0.01em; - margin-top: 1.75rem; } .docs-prose h2 { + padding-top: 0.5rem; + margin-top: 1.75rem; font-size: 1.25rem; font-weight: 600; letter-spacing: -0.005em; - margin-top: 1.75rem; - padding-top: 0.5rem; border-top: 1px solid var(--border); } .docs-prose h3 { + margin-top: 1.25rem; font-size: 1.05rem; font-weight: 600; - margin-top: 1.25rem; } .docs-prose h4, .docs-prose h5, .docs-prose h6 { - font-weight: 600; margin-top: 1rem; + font-weight: 600; } .docs-prose p { color: var(--foreground); @@ -516,12 +516,12 @@ html > body[data-scroll-locked] { text-decoration-thickness: 2px; } .docs-prose ul { - list-style: disc; padding-left: 1.25rem; + list-style: disc; } .docs-prose ol { - list-style: decimal; padding-left: 1.25rem; + list-style: decimal; } .docs-prose li + li { margin-top: 0.25rem; @@ -530,34 +530,34 @@ html > body[data-scroll-locked] { margin: 0; } .docs-prose code { + padding: 0.1em 0.35em; font-family: var(--font-mono); font-size: 0.85em; background: var(--muted); - padding: 0.1em 0.35em; border-radius: 0.3em; } .docs-prose pre { - background: var(--card); - border: 1px solid var(--border); - border-radius: 0.5rem; padding: 0.875rem 1rem; overflow-x: auto; font-size: 0.85em; + background: var(--card); + border: 1px solid var(--border); + border-radius: 0.5rem; } .docs-prose pre code { - background: transparent; padding: 0; + background: transparent; border-radius: 0; } .docs-prose blockquote { - border-left: 2px solid var(--border); padding: 0.25rem 0 0.25rem 0.875rem; color: var(--muted-foreground); + border-left: 2px solid var(--border); } .docs-prose hr { + margin: 2rem 0; border: 0; border-top: 1px solid var(--border); - margin: 2rem 0; } .docs-prose table { width: 100%; @@ -566,9 +566,9 @@ html > body[data-scroll-locked] { } .docs-prose th, .docs-prose td { - border-bottom: 1px solid var(--border); padding: 0.45rem 0.6rem; text-align: left; + border-bottom: 1px solid var(--border); } .docs-prose th { font-weight: 600; diff --git a/apps/site/src/app/icon.svg b/apps/site/src/app/icon.svg index cb3fac5eb..02bf4172d 100644 --- a/apps/site/src/app/icon.svg +++ b/apps/site/src/app/icon.svg @@ -1,6 +1,6 @@ - - - - - + + + + + diff --git a/apps/site/src/app/layout.tsx b/apps/site/src/app/layout.tsx index 0f7f2a6b8..3445784ce 100644 --- a/apps/site/src/app/layout.tsx +++ b/apps/site/src/app/layout.tsx @@ -11,40 +11,40 @@ import { TooltipProvider } from "@/components/ui/tooltip.tsx"; import "./globals.css"; export const metadata: Metadata = { - title: "PDPP — Personal Data Portability Protocol", description: "An authorization and disclosure protocol for personal data. You decide what to share, with whom, for how long, for what purpose.", - metadataBase: new URL("https://pdpp.dev"), // Mirror the console's favicon declaration so pdpp.dev serves the // edge-to-edge two-rectangle mark instead of auto-detecting the // rounded-plate icon.svg (whose cream plate reads as white padding // at favicon scale). /brand/pdpp-favicon.svg is the padding-free mark. icons: { - icon: [{ url: "/brand/pdpp-favicon.svg", type: "image/svg+xml" }], + icon: [{ type: "image/svg+xml", url: "/brand/pdpp-favicon.svg" }], }, + metadataBase: new URL("https://pdpp.dev"), openGraph: { - title: "PDPP — Personal Data Portability Protocol", description: "An authorization and disclosure protocol for personal data. You decide what to share, with whom, for how long, for what purpose.", + title: "PDPP — Personal Data Portability Protocol", type: "website", }, + title: "PDPP — Personal Data Portability Protocol", twitter: { card: "summary_large_image", - title: "PDPP — Personal Data Portability Protocol", description: "An authorization and disclosure protocol for personal data.", + title: "PDPP — Personal Data Portability Protocol", }, }; export const viewport = { - width: "device-width", initialScale: 1, // Theme-following chrome color, sourced from LAUNCH_COLORS (the single source // of truth derived from the `--background` tokens). The browser picks the // entry matching the OS scheme, so the chrome never flashes the wrong color. themeColor: [ - { media: "(prefers-color-scheme: dark)", color: LAUNCH_COLORS.dark }, - { media: "(prefers-color-scheme: light)", color: LAUNCH_COLORS.light }, + { color: LAUNCH_COLORS.dark, media: "(prefers-color-scheme: dark)" }, + { color: LAUNCH_COLORS.light, media: "(prefers-color-scheme: light)" }, ], + width: "device-width", }; export default async function RootLayout({ children }: { children: React.ReactNode }) { diff --git a/apps/site/src/app/manifest.ts b/apps/site/src/app/manifest.ts index 0e064b80e..dae0bc76b 100644 --- a/apps/site/src/app/manifest.ts +++ b/apps/site/src/app/manifest.ts @@ -6,18 +6,13 @@ import type { MetadataRoute } from "next"; export default function manifest(): MetadataRoute.Manifest { return { - name: "PDPP", - short_name: "PDPP", - description: - "Personal Data Polyfill Project — protocol docs, the reference-implementation explainer, and a mock-backed sandbox.", - start_url: "/", - scope: "/", - display: "standalone", // Honest LIGHT first-paint color from LAUNCH_COLORS (the `:root --background` // token). Was a drifting #f8f6f0 that didn't match the computed token // (#fcfcfa); browsers override the splash to dark on a dark OS via CSS. background_color: LAUNCH_COLORS.light, - theme_color: LAUNCH_COLORS.light, + description: + "Personal Data Polyfill Project — protocol docs, the reference-implementation explainer, and a mock-backed sandbox.", + display: "standalone", // Only the App Router-generated /icon.svg (from src/app/icon.svg) is a real // asset on the public site. The previous PNG entries pointed at // public/*.png files that do not exist and are excluded by the root @@ -26,11 +21,16 @@ export default function manifest(): MetadataRoute.Manifest { // belong in the manifest icon list. icons: [ { - src: "/icon.svg", + purpose: "any", sizes: "any", + src: "/icon.svg", type: "image/svg+xml", - purpose: "any", }, ], + name: "PDPP", + scope: "/", + short_name: "PDPP", + start_url: "/", + theme_color: LAUNCH_COLORS.light, }; } diff --git a/apps/site/src/app/opengraph-image.tsx b/apps/site/src/app/opengraph-image.tsx index 61cdc1f94..65150a1e0 100644 --- a/apps/site/src/app/opengraph-image.tsx +++ b/apps/site/src/app/opengraph-image.tsx @@ -5,7 +5,7 @@ import { ImageResponse } from "next/og"; export const alt = "PDPP — Personal Data Portability Protocol. Clients request named records and fields; every response stays inside the grant."; -export const size = { width: 1200, height: 630 }; +export const size = { height: 630, width: 1200 }; export const contentType = "image/png"; // Social card for the public site. Reused as both the Open Graph and Twitter @@ -29,15 +29,15 @@ function Chip({ label, color, struck }: { label: string; color: string; struck?: return (
{label} @@ -49,19 +49,19 @@ export default function OpengraphImage() { return new ImageResponse(
{/* Wordmark */} -
+
-
PDPP
-
+
PDPP
+
v0.1.0 · Open reference
@@ -79,25 +79,25 @@ export default function OpengraphImage() { {/* Headline */}
Granular access to personal data
-
+
Clients request named records and fields. Every response stays inside the grant.
{/* Field-projection promise — eight fields enter, four come back */} -
-
8 fields
+
+
8 fields
-
→ 4 returned
+
→ 4 returned
, { ...size } diff --git a/apps/site/src/app/page.tsx b/apps/site/src/app/page.tsx index d0bdc6d3a..47d39b511 100644 --- a/apps/site/src/app/page.tsx +++ b/apps/site/src/app/page.tsx @@ -18,10 +18,10 @@ export default function Home() { actions={
- + Read the docs - + See the flow
@@ -41,8 +41,8 @@ export default function Home() { className="rounded px-2 py-0.5 font-mono text-xs" style={{ backgroundColor: "var(--primary-wash)", - color: "var(--primary)", border: "1px solid var(--authorship-protocol-border)", + color: "var(--primary)", }} > PDPP diff --git a/apps/site/src/app/palette/page.tsx b/apps/site/src/app/palette/page.tsx index a1aa258ce..a9c323e33 100644 --- a/apps/site/src/app/palette/page.tsx +++ b/apps/site/src/app/palette/page.tsx @@ -10,67 +10,67 @@ const CANDIDATES = [ { - name: "Rose — soft", - description: "Lower chroma, lighter. Refined.", border: "oklch(0.75 0.06 15)", - wash: "oklch(0.75 0.06 15 / 0.07)", + description: "Lower chroma, lighter. Refined.", + name: "Rose — soft", swatch: "oklch(0.75 0.06 15)", + wash: "oklch(0.75 0.06 15 / 0.07)", }, { - name: "Rose", - description: "Original. Explicit, human.", border: "oklch(0.72 0.08 20)", - wash: "oklch(0.72 0.08 20 / 0.07)", + description: "Original. Explicit, human.", + name: "Rose", swatch: "oklch(0.72 0.08 20)", + wash: "oklch(0.72 0.08 20 / 0.07)", }, { - name: "Terracotta", - description: "Rose toward orange. Earthier.", border: "oklch(0.65 0.10 35)", - wash: "oklch(0.65 0.10 35 / 0.07)", + description: "Rose toward orange. Earthier.", + name: "Terracotta", swatch: "oklch(0.65 0.10 35)", + wash: "oklch(0.65 0.10 35 / 0.07)", }, { - name: "Brick", - description: "Deep muted rose. Serious.", border: "oklch(0.55 0.09 25)", - wash: "oklch(0.55 0.09 25 / 0.07)", + description: "Deep muted rose. Serious.", + name: "Brick", swatch: "oklch(0.55 0.09 25)", + wash: "oklch(0.55 0.09 25 / 0.07)", }, { - name: "Copper — light", - description: "Brighter, more orange. Near amber.", border: "oklch(0.70 0.12 55)", - wash: "oklch(0.70 0.12 55 / 0.07)", + description: "Brighter, more orange. Near amber.", + name: "Copper — light", swatch: "oklch(0.70 0.12 55)", + wash: "oklch(0.70 0.12 55 / 0.07)", }, { - name: "Copper", - description: "Original. Warm but precise.", border: "oklch(0.62 0.10 50)", - wash: "oklch(0.62 0.10 50 / 0.07)", + description: "Original. Warm but precise.", + name: "Copper", swatch: "oklch(0.62 0.10 50)", + wash: "oklch(0.62 0.10 50 / 0.07)", }, { - name: "Copper — deep", - description: "Darker, richer. More bronze.", border: "oklch(0.52 0.09 45)", - wash: "oklch(0.52 0.09 45 / 0.07)", + description: "Darker, richer. More bronze.", + name: "Copper — deep", swatch: "oklch(0.52 0.09 45)", + wash: "oklch(0.52 0.09 45 / 0.07)", }, { - name: "Copper — red", - description: "Red-copper. More oxide, aged.", border: "oklch(0.58 0.11 35)", - wash: "oklch(0.58 0.11 35 / 0.07)", + description: "Red-copper. More oxide, aged.", + name: "Copper — red", swatch: "oklch(0.58 0.11 35)", + wash: "oklch(0.58 0.11 35 / 0.07)", }, { - name: "Vana Stone", - description: "Earthy neutral. Already in Vana.", border: "oklch(0.60 0.04 80)", - wash: "oklch(0.60 0.04 80 / 0.07)", + description: "Earthy neutral. Already in Vana.", + name: "Vana Stone", swatch: "oklch(0.60 0.04 80)", + wash: "oklch(0.60 0.04 80 / 0.07)", }, ]; @@ -82,15 +82,15 @@ export default function PalettePage() { style={{ backgroundColor: "var(--background)", color: "var(--foreground)", + fontFamily: "var(--font-sans)", minHeight: "100vh", padding: "48px 64px", - fontFamily: "var(--font-sans)", }} >

Warm tone candidates

-

+

Each candidate shown in three contexts: as a left border rule, as a background wash, and paired with the protocol blue to test the human/protocol duality.

@@ -99,37 +99,37 @@ export default function PalettePage() { {/* Test 1: 2px border on white — does it survive at actual pixel weight? */}
Test 1 — 2px border on white
-
+
The hardest context. Most colors disappear or go muddy at 2px.
{CANDIDATES.map(({ name, border, swatch }) => ( -
+
Design System
- + {name}
@@ -142,58 +142,58 @@ export default function PalettePage() {
Test 2 — alongside protocol blue
-
+
Human row (warm) above protocol row (blue). Do they read as intentionally different or accidentally clashing?
{CANDIDATES.map(({ name, border, wash, swatch }) => ( -
+
-
+
Alex Rivera
-
+
instagram.com/alex · owner
grt_8f3a2b1c · single_use · §4.2
@@ -203,11 +203,11 @@ export default function PalettePage() {
{name} @@ -220,10 +220,10 @@ export default function PalettePage() { {/* Test 3: Consent card — the most important surface */}
Test 3 — consent card
-
+
The primary blue Allow button on a warm-washed card. Does it feel trustworthy or confused?
-
+
{CANDIDATES.map(({ name, border, wash, swatch }) => (
Grant request
@@ -248,10 +248,10 @@ export default function PalettePage() {
Access to your Instagram social graph. @@ -259,13 +259,13 @@ export default function PalettePage() {
-
+
- {name} + {name}
))}
-
-

+

+

Protocol blue for reference:{" "} oklch(0.580 0.172 253.7) · #187adc   Selected:{" "} Copper — deep → --human diff --git a/apps/site/src/app/reference/layout.tsx b/apps/site/src/app/reference/layout.tsx index c2c2ca6eb..6124a5d12 100644 --- a/apps/site/src/app/reference/layout.tsx +++ b/apps/site/src/app/reference/layout.tsx @@ -10,8 +10,8 @@ export default function ReferenceLayout({ children }: { children: ReactNode }) {

diff --git a/apps/site/src/app/reference/page.tsx b/apps/site/src/app/reference/page.tsx index ce134a633..c8b8f7d74 100644 --- a/apps/site/src/app/reference/page.tsx +++ b/apps/site/src/app/reference/page.tsx @@ -18,9 +18,9 @@ const RAILWAY_DEPLOY_URL = "https://railway.com/new/template/pdpp-core-template-source?utm_medium=integration&utm_source=button&utm_campaign=pdpp-core"; export const metadata: Metadata = { - title: "Reference Implementation - PDPP", description: "A public explainer for the forkable PDPP reference implementation: purpose, architecture, trust boundaries, and local/self-hosted operation.", + title: "Reference Implementation - PDPP", }; async function getRequestOrigin(): Promise { @@ -34,28 +34,28 @@ async function getRequestOrigin(): Promise { const architectureLayers = [ { + body: "Normative protocol semantics, extension docs, grant shapes, query behavior, and intentionally deferred scope.", label: "Protocol authority", route: "/docs", title: "PDPP docs", - body: "Normative protocol semantics, extension docs, grant shapes, query behavior, and intentionally deferred scope.", }, { + body: "Current code, tests, dashboards, example clients, and operator diagnostics for one implementation of PDPP.", label: "Reference authority", route: "/reference", title: "Forkable implementation", - body: "Current code, tests, dashboards, example clients, and operator diagnostics for one implementation of PDPP.", }, { + body: "A stateful control plane for a running local or self-hosted instance. It is not a public hosted demo.", label: "Live instance", route: "/", title: "Operator dashboard", - body: "A stateful control plane for a running local or self-hosted instance. It is not a public hosted demo.", }, { + body: "A mock-adapter-backed reference instance with deterministic data. Browse connectors, streams, records, grants, runs, traces, and call sandbox-prefixed AS/RS-shaped APIs.", label: "Reference sandbox", route: "/sandbox", title: "Mock-adapter reference instance", - body: "A mock-adapter-backed reference instance with deterministic data. Browse connectors, streams, records, grants, runs, traces, and call sandbox-prefixed AS/RS-shaped APIs.", }, ] as const; @@ -68,40 +68,40 @@ const trustBoundaries = [ const referenceLinks = [ { + body: "Falsifiable status rows for protocol flows, retrieval extensions, collection profiles, reference diagnostics, sandbox, and deferred scope.", + href: "/reference/coverage", label: "Coverage", title: "Public coverage matrix", - href: "/reference/coverage", - body: "Falsifiable status rows for protocol flows, retrieval extensions, collection profiles, reference diagnostics, sandbox, and deferred scope.", }, { + body: "Browse a public PDPP reference surface backed by deterministic mock adapters. Inspect records, grants, runs, and traces, then call sandbox-prefixed AS/RS-shaped APIs (/sandbox/v1/**, /sandbox/_ref/**, /sandbox/.well-known/**).", + href: "/sandbox", label: "Sandbox", title: "Mock reference demo instance", - href: "/sandbox", - body: "Browse a public PDPP reference surface backed by deterministic mock adapters. Inspect records, grants, runs, and traces, then call sandbox-prefixed AS/RS-shaped APIs (/sandbox/v1/**, /sandbox/_ref/**, /sandbox/.well-known/**).", }, { + body: "Browse the monorepo, issues, tests, Docker files, and reference package.", + href: GITHUB_REPO, label: "Repository", title: "GitHub source", - href: GITHUB_REPO, - body: "Browse the monorepo, issues, tests, Docker files, and reference package.", }, { + body: "Repo overview, dev commands, Docker image posture, and top-level project map.", + href: GITHUB_ROOT_README, label: "Start here", title: "Root README", - href: GITHUB_ROOT_README, - body: "Repo overview, dev commands, Docker image posture, and top-level project map.", }, { + body: "Local stack, direct AS/RS mode, Docker Compose, owner auth, and generated artifacts.", + href: GITHUB_REFERENCE_README, label: "Run/deploy", title: "Reference README", - href: GITHUB_REFERENCE_README, - body: "Local stack, direct AS/RS mode, Docker Compose, owner auth, and generated artifacts.", }, { + body: "Protocol-facing architecture notes. Treat repo package topology as reference behavior unless specified by docs.", + href: "/docs/spec-architecture", label: "Architecture", title: "Architecture docs", - href: "/docs/spec-architecture", - body: "Protocol-facing architecture notes. Treat repo package topology as reference behavior unless specified by docs.", }, ] as const; @@ -129,10 +129,10 @@ export default async function ReferencePage() { docs remain the normative authority.

- + Clone and run - + Read protocol docs
@@ -326,18 +326,18 @@ printf 'Origin: https://%s.fly.dev\\nOwner password: %s\\n' "$APP" "$OWNER_PASSW ))}
diff --git a/apps/site/src/app/sandbox/_demo/builders.test.ts b/apps/site/src/app/sandbox/_demo/builders.test.ts index 40b1d5aae..7fa29ceb0 100644 --- a/apps/site/src/app/sandbox/_demo/builders.test.ts +++ b/apps/site/src/app/sandbox/_demo/builders.test.ts @@ -31,7 +31,7 @@ test("paginate returns deterministic envelopes with cursor and total", () => { assert.equal(page1.has_more, true); assert.equal(page1.next_cursor, "3"); assert.equal(page1.total, 10); - const page2 = paginate(rows, { limit: 3, cursor: page1.next_cursor }); + const page2 = paginate(rows, { cursor: page1.next_cursor, limit: 3 }); assert.deepEqual(page2.data, [4, 5, 6]); const last = paginate(rows, { limit: 100 }); assert.equal(last.has_more, false); @@ -62,7 +62,7 @@ test("streams list and detail return matching shapes", () => { test("records list returns null for missing stream and pages correctly", () => { assert.equal(buildRecordsList({ stream: "does_not_exist" }), null); - const page = buildRecordsList({ stream: "pay_statements", limit: 1 }); + const page = buildRecordsList({ limit: 1, stream: "pay_statements" }); assert.ok(page); assert.equal(page?.data.length, 1); assert.ok(page?.has_more); diff --git a/apps/site/src/app/sandbox/_demo/builders.ts b/apps/site/src/app/sandbox/_demo/builders.ts index 23232bcdd..2fd264fca 100644 --- a/apps/site/src/app/sandbox/_demo/builders.ts +++ b/apps/site/src/app/sandbox/_demo/builders.ts @@ -92,13 +92,13 @@ export function paginate(rows: readonly T[], opts: { limit?: number; cursor?: const next = start + limit; const hasMore = next < rows.length; return { - object: "list", data: slice as T[], has_more: hasMore, - next_cursor: hasMore ? encodeCursor(next) : null, - total: rows.length, is_demo: true, + next_cursor: hasMore ? encodeCursor(next) : null, notice: DEMO_NOTICE.notice, + object: "list", + total: rows.length, }; } @@ -122,23 +122,23 @@ export interface SchemaResponse { export function buildSchemaResponse(issuer = DEMO_ISSUER): SchemaResponse { return { - object: "schema_graph", - is_demo: true, - notice: DEMO_NOTICE.notice, - issuer, connectors: DEMO_CONNECTORS.map((connector) => ({ connector_id: connector.connector_id, display_name: connector.display_name, streams: DEMO_STREAMS.filter((stream) => stream.connector_id === connector.connector_id).map((stream) => ({ - stream: stream.key, - label: stream.label, fields: stream.fields.map((field) => ({ name: field.name, - type: field.type, semantic_class: field.semantic_class, + type: field.type, })), + label: stream.label, + stream: stream.key, })), })), + is_demo: true, + issuer, + notice: DEMO_NOTICE.notice, + object: "schema_graph", }; } @@ -167,12 +167,12 @@ export function buildStreamsList(opts: { ? DEMO_STREAMS.filter((stream) => stream.connector_id === opts.connector_id) : DEMO_STREAMS; const summaries = filtered.map((stream) => ({ - object: "stream_summary", connector_id: stream.connector_id, description: stream.description, field_count: stream.fields.length, label: stream.label, latest_record_time: stream.latest_record_time, + object: "stream_summary", record_count: streamRecordCount(stream.key), retention_label: stream.retention_label, stream: stream.key, @@ -200,19 +200,19 @@ export function buildStreamDetail(streamKey: string): StreamDetail | null { return null; } return { - object: "stream", - is_demo: true, - notice: DEMO_NOTICE.notice, connector_id: stream.connector_id, description: stream.description, fields: stream.fields.map((field) => ({ + description: field.description, name: field.name, - type: field.type, semantic_class: field.semantic_class, - description: field.description, + type: field.type, })), + is_demo: true, label: stream.label, latest_record_time: stream.latest_record_time, + notice: DEMO_NOTICE.notice, + object: "stream", record_count: streamRecordCount(stream.key), retention_label: stream.retention_label, stream: stream.key, @@ -266,9 +266,9 @@ export function buildRecordsList(opts: { }); const sorted = sortRecordsNewestFirst(matching); const summaries = sorted.map((record) => ({ - object: "record", connector_id: record.connector_id, ingested_at: record.ingested_at, + object: "record", preview: recordPreview(record), record_id: record.record_id, record_time: record.record_time, @@ -295,12 +295,12 @@ export function buildRecordDetail(streamKey: string, recordId: string): RecordDe return null; } return { - object: "record_detail", - is_demo: true, - notice: DEMO_NOTICE.notice, connector_id: record.connector_id, fields: record.fields, ingested_at: record.ingested_at, + is_demo: true, + notice: DEMO_NOTICE.notice, + object: "record_detail", record_id: record.record_id, record_time: record.record_time, stream: record.stream, @@ -360,15 +360,15 @@ export function buildSearchResponse(query: string): SearchResponse { const trimmed = query.trim(); if (trimmed.length === 0) { return { - object: "list", + data: [], + has_more: false, is_demo: true, + next_cursor: null, notice: DEMO_NOTICE.notice, - url: "/sandbox/v1/search", + object: "list", query: "", total: 0, - has_more: false, - next_cursor: null, - data: [], + url: "/sandbox/v1/search", }; } const lower = trimmed.toLowerCase(); @@ -392,15 +392,15 @@ export function buildSearchResponse(query: string): SearchResponse { return 0; }); return { - object: "list", + data: hits, + has_more: false, is_demo: true, + next_cursor: null, notice: DEMO_NOTICE.notice, - url: "/sandbox/v1/search", + object: "list", query: trimmed, total: hits.length, - has_more: false, - next_cursor: null, - data: hits, + url: "/sandbox/v1/search", }; } @@ -423,17 +423,17 @@ function buildSearchHit(record: DemoRecord, query: { lower: string; trimmed: str return null; } return { - object: "search_result", connector_id: record.connector_id, emitted_at: record.ingested_at, matched_fields: matchedFields, + object: "search_result", record_key: record.record_id, record_time: record.record_time, record_url: `/sandbox/v1/streams/${encodeURIComponent(record.stream)}/records/${encodeURIComponent(record.record_id)}`, score: { kind: "sandbox_lexical_rank", - value: matchedFields.length + bestMatch.score, order: "higher_is_better", + value: matchedFields.length + bestMatch.score, value_semantics: "demo_only", }, snippet: { field: bestMatch.field, text: bestMatch.snippet }, @@ -469,16 +469,16 @@ function grantToSummary(grant: DemoGrantDef): GrantSummary { failure = "grant_revoked"; } return { - object: "grant_summary", - grant_id: grant.grant_id, client_id: grant.client_id, connector_id: grant.connector_id, - stream: grant.stream, - status: grant.status, + event_count: grant.events.length, failure_reason: failure, first_at: grant.first_at, + grant_id: grant.grant_id, last_at: grant.last_at, - event_count: grant.events.length, + object: "grant_summary", + status: grant.status, + stream: grant.stream, trace_id: grant.trace_id, }; } @@ -518,14 +518,14 @@ export function buildGrantTimeline(grantId: string): TimelineEnvelope | null { return null; } return { - object: "timeline", + event_count: grant.events.length, + events: [...grant.events], is_demo: true, notice: DEMO_NOTICE.notice, - subject_type: "grant", + object: "timeline", subject_id: grant.grant_id, + subject_type: "grant", trace_id: grant.trace_id, - event_count: grant.events.length, - events: [...grant.events], }; } @@ -546,18 +546,18 @@ export interface RunSummary { function runToSummary(run: DemoRunDef): RunSummary { return { - object: "run_summary", - run_id: run.run_id, connector_id: run.connector_id, - grant_id: run.grant_id, - status: run.status, - needs_input: run.needs_input, + event_count: run.events.length, failure_reason: run.failure_reason, - started_at: run.started_at, finished_at: run.finished_at, first_at: run.first_at, + grant_id: run.grant_id, last_at: run.last_at, - event_count: run.events.length, + needs_input: run.needs_input, + object: "run_summary", + run_id: run.run_id, + started_at: run.started_at, + status: run.status, }; } @@ -586,14 +586,14 @@ export function buildRunTimeline(runId: string): TimelineEnvelope | null { } const traceId = run.events[0]?.trace_id ?? null; return { - object: "timeline", + event_count: run.events.length, + events: [...run.events], is_demo: true, notice: DEMO_NOTICE.notice, - subject_type: "run", + object: "timeline", subject_id: run.run_id, + subject_type: "run", trace_id: traceId, - event_count: run.events.length, - events: [...run.events], }; } @@ -613,17 +613,17 @@ export interface TraceSummary { function traceToSummary(trace: DemoTraceDef): TraceSummary { return { - object: "trace_summary", - trace_id: trace.trace_id, client_id: trace.client_id, - grant_id: trace.grant_id, - run_id: trace.run_id, - status: trace.status, + event_count: trace.kinds.length, failure_reason: trace.failure_reason, - kinds: [...trace.kinds], first_at: trace.first_at, + grant_id: trace.grant_id, + kinds: [...trace.kinds], last_at: trace.last_at, - event_count: trace.kinds.length, + object: "trace_summary", + run_id: trace.run_id, + status: trace.status, + trace_id: trace.trace_id, }; } @@ -647,14 +647,14 @@ export function buildTraceTimeline(traceId: string): TimelineEnvelope | null { return null; } return { - object: "timeline", + event_count: events.length, + events, is_demo: true, notice: DEMO_NOTICE.notice, - subject_type: "trace", + object: "timeline", subject_id: traceId, + subject_type: "trace", trace_id: traceId, - event_count: events.length, - events, }; } @@ -708,17 +708,17 @@ export function buildDatasetSummary(): DatasetSummary { const blobBytes = approximateRetainedBytes(); const recordTimes = recordTimesSorted(); return { - object: "dataset_summary", - is_demo: true, - notice: DEMO_NOTICE.notice, + blob_bytes: blobBytes, connector_count: DEMO_CONNECTORS.length, - stream_count: DEMO_STREAMS.length, - record_count: DEMO_RECORDS.length, earliest_record_time: recordTimes.earliest, + is_demo: true, latest_record_time: recordTimes.latest, - blob_bytes: blobBytes, - total_retained_bytes: blobBytes, + notice: DEMO_NOTICE.notice, + object: "dataset_summary", + record_count: DEMO_RECORDS.length, + stream_count: DEMO_STREAMS.length, top_connectors: topConnectors, + total_retained_bytes: blobBytes, }; } @@ -776,28 +776,28 @@ export interface OAuthAuthServerMetadata { export function buildAuthServerMetadata(issuer = DEMO_ISSUER): OAuthAuthServerMetadata { return { + authorization_endpoint: `${issuer}/authorize`, + grant_types_supported: ["authorization_code", "urn:pdpp:params:oauth:grant-type:scoped_grant"], + introspection_endpoint: `${issuer}/introspect`, is_demo: true, - notice: DEMO_NOTICE.notice, issuer, - authorization_endpoint: `${issuer}/authorize`, + notice: DEMO_NOTICE.notice, + pdpp_demo: { + note: "Sandbox demo metadata. The real reference advertises live AS endpoints under the deployment origin.", + schema_endpoint: `${issuer}/v1/schema`, + search_endpoint: `${issuer}/v1/search`, + streams_endpoint: `${issuer}/v1/streams`, + }, pushed_authorization_request_endpoint: `${issuer}/par`, - token_endpoint: `${issuer}/token`, - introspection_endpoint: `${issuer}/introspect`, - revocation_endpoint: `${issuer}/revoke`, - grant_types_supported: ["authorization_code", "urn:pdpp:params:oauth:grant-type:scoped_grant"], response_types_supported: ["code"], + revocation_endpoint: `${issuer}/revoke`, scopes_supported: [ "stream:pay_statements:read", "stream:tax_documents:read", "stream:clinical_visits:read", "stream:transactions:read", ], - pdpp_demo: { - note: "Sandbox demo metadata. The real reference advertises live AS endpoints under the deployment origin.", - streams_endpoint: `${issuer}/v1/streams`, - schema_endpoint: `${issuer}/v1/schema`, - search_endpoint: `${issuer}/v1/search`, - }, + token_endpoint: `${issuer}/token`, }; } @@ -819,11 +819,17 @@ export interface OAuthProtectedResourceMetadata { export function buildProtectedResourceMetadata(issuer = DEMO_ISSUER): OAuthProtectedResourceMetadata { return { + authorization_servers: [issuer], + bearer_methods_supported: ["header"], is_demo: true, notice: DEMO_NOTICE.notice, + pdpp_demo: { + note: "Sandbox demo metadata. Advertises sandbox-prefixed endpoints.", + record_endpoint_template: `${issuer}/v1/streams/{stream}/records/{recordId}`, + search_endpoint: `${issuer}/v1/search`, + streams_endpoint: `${issuer}/v1/streams`, + }, resource: issuer, - authorization_servers: [issuer], - bearer_methods_supported: ["header"], resource_documentation: issuer.replace(SANDBOX_PATH_SUFFIX_RE, "/docs"), scopes_supported: [ "stream:pay_statements:read", @@ -831,12 +837,6 @@ export function buildProtectedResourceMetadata(issuer = DEMO_ISSUER): OAuthProte "stream:clinical_visits:read", "stream:transactions:read", ], - pdpp_demo: { - note: "Sandbox demo metadata. Advertises sandbox-prefixed endpoints.", - streams_endpoint: `${issuer}/v1/streams`, - record_endpoint_template: `${issuer}/v1/streams/{stream}/records/{recordId}`, - search_endpoint: `${issuer}/v1/search`, - }, }; } @@ -923,29 +923,29 @@ export function buildLiveStreamMetadata(stream: DemoStreamDef): LiveStreamMetada const properties: Record = {}; for (const field of stream.fields) { properties[field.name] = { - type: liveTypeFor(field.type), description: field.description, + type: liveTypeFor(field.type), }; } const allowed = stream.fields.map((f) => f.name); return { - object: "stream_metadata", + cursor_field: cursorFieldFor(stream), + expand_capabilities: [], + field_capabilities: { allowed_fields: allowed, restricted_fields: [] }, + freshness: { last_updated: latestRecordTimeForStream(stream.key) ?? stream.latest_record_time }, name: stream.key, - semantics: stream.label, + object: "stream_metadata", + primary_key: ["id"], + query: { expand: [], range_filters: {} }, + relationships: [], schema: { - type: "object", properties, required: allowed, + type: "object", }, - primary_key: ["id"], - cursor_field: cursorFieldFor(stream), selection: { all: true }, + semantics: stream.label, views: [], - relationships: [], - query: { range_filters: {}, expand: [] }, - field_capabilities: { allowed_fields: allowed, restricted_fields: [] }, - expand_capabilities: [], - freshness: { last_updated: latestRecordTimeForStream(stream.key) ?? stream.latest_record_time }, }; } diff --git a/apps/site/src/app/sandbox/_demo/components/shell.tsx b/apps/site/src/app/sandbox/_demo/components/shell.tsx index c4f75ae7a..d1cae601f 100644 --- a/apps/site/src/app/sandbox/_demo/components/shell.tsx +++ b/apps/site/src/app/sandbox/_demo/components/shell.tsx @@ -19,8 +19,8 @@ export function SandboxEducationalShell({ children }: { children: ReactNode }) {
diff --git a/apps/site/src/app/sandbox/_demo/data-source.ts b/apps/site/src/app/sandbox/_demo/data-source.ts index 283c1c66a..a4f1f7ae1 100644 --- a/apps/site/src/app/sandbox/_demo/data-source.ts +++ b/apps/site/src/app/sandbox/_demo/data-source.ts @@ -119,13 +119,13 @@ function connectorRunRef(runId: string | undefined): ConnectorRunRef | null { return null; } return { - run_id: run.run_id, - first_at: run.first_at, - last_at: run.last_at, event_count: run.events.length, - status: run.status, failure_reason: run.failure_reason, + first_at: run.first_at, known_gaps: [], + last_at: run.last_at, + run_id: run.run_id, + status: run.status, }; } @@ -138,54 +138,54 @@ function refConnectorRunSummary(runId: string | undefined): RefConnectorRunSumma return null; } return { - run_id: run.run_id, + event_count: run.events.length, + failure_reason: run.failure_reason, + finished_at: run.finished_at, first_at: run.first_at, + known_gaps: [], last_at: run.last_at, + run_id: run.run_id, started_at: run.started_at, - finished_at: run.finished_at, - event_count: run.events.length, status: run.status, - failure_reason: run.failure_reason, - known_gaps: [], }; } const SCHEDULE_INTERVAL: Record = { daily: 86_400, - weekly: 604_800, manual: 86_400, + weekly: 604_800, }; function buildDemoSchedule(connectorId: string, kind: string): RefSchedule { const interval = SCHEDULE_INTERVAL[kind] ?? 86_400; const now = new Date().toISOString(); return { - object: "schedule", - connector_id: connectorId, + active_run_id: null, automation_mode: kind === "manual" ? "manual_only" : "unattended", automation_summary: kind === "manual" ? "Starts only from an owner gesture." : "Can refresh in the background without expected owner action.", - interval_seconds: interval, - jitter_seconds: 0, - enabled: kind !== "manual", + connector_id: connectorId, created_at: now, - updated_at: now, - next_due_at: null, - active_run_id: null, - last_started_at: null, - last_finished_at: null, - last_error_code: null, - last_successful_at: null, effective_mode: kind === "manual" ? "manual" : "automatic", + enabled: kind !== "manual", human_attention_needed: false, ineligibility_reason: null, + interval_seconds: interval, + jitter_seconds: 0, + last_error_code: null, + last_finished_at: null, + last_started_at: null, + last_successful_at: null, + minimum_interval_warning: null, + next_due_at: null, notification_posture: "none", + object: "schedule", recommended_policy: null, scheduler_backoff: null, - minimum_interval_warning: null, trigger_kind: "scheduled", + updated_at: now, }; } @@ -198,7 +198,6 @@ function buildRefConnectorSummary(connectorId: string): RefConnectorSummary { const lastRun = refConnectorRunSummary(lastRunId); const lastSuccessfulRun = refConnectorRunSummary(lastSuccessId); return { - connection_id: connectorId, connection_health: { axes: { attention: "none", @@ -214,13 +213,14 @@ function buildRefConnectorSummary(connectorId: string): RefConnectorSummary { state: lastSuccessfulRun ? "healthy" : "idle", unknown_reasons: [], }, - next_action: null, + connection_id: connectorId, connector_id: connectorId, display_name: connector?.display_name ?? connectorId, freshness: {}, last_run: lastRun, last_successful_run: lastSuccessfulRun, manifest_version: "1.0.0-demo", + next_action: null, schedule: connector?.schedule ? buildDemoSchedule(connectorId, connector.schedule) : null, streams: streams.map((s) => s.key), total_records: totalRecordsForConnector(connectorId), @@ -236,8 +236,8 @@ function buildConnectorManifest(connectorId: string): ConnectorManifest { name: connector?.display_name ?? connectorId, provider_id: connector?.provider_id, streams: streams.map((s) => ({ - name: s.key, consent_time_field: s.consent_time_field, + name: s.key, preview_fields: s.fields.slice(0, 4).map((f) => f.name), // The demo fields carry a declared presentation type (timestamp, // currency_minor_units, …). Surface it on the JSON Schema extension @@ -259,31 +259,29 @@ function buildConnectorManifest(connectorId: string): ConnectorManifest { function buildStreamSummary(streamKey: string): StreamSummary { const stream = getDemoStreams().find((s) => s.key === streamKey); return { - object: "stream", + last_updated: latestRecordTimeForStream(streamKey) ?? stream?.latest_record_time ?? null, name: streamKey, + object: "stream", record_count: streamRecordCount(streamKey), - last_updated: latestRecordTimeForStream(streamKey) ?? stream?.latest_record_time ?? null, }; } function recordToStreamRecord(record: DemoRecord): StreamRecord { return { - object: "record", + data: { ...record.fields }, + emitted_at: record.ingested_at, id: record.record_id, + object: "record", stream: record.stream, - emitted_at: record.ingested_at, - data: { ...record.fields }, }; } function buildSandboxStreamMetadata(connectorId: string, streamKey: string): StreamMetadata { const stream = streamsForConnector(connectorId).find((s) => s.key === streamKey); if (!stream) { - return { object: "stream_metadata", name: streamKey, field_capabilities: {} }; + return { field_capabilities: {}, name: streamKey, object: "stream_metadata" }; } return { - object: "stream_metadata", - name: stream.key, field_capabilities: Object.fromEntries( stream.fields.map((field) => { // Deterministic projection fixture for Explorer honesty tests: the @@ -294,14 +292,16 @@ function buildSandboxStreamMetadata(connectorId: string, streamKey: string): Str return [ field.name, { - type: field.type, - schema: { type: field.type === "blob" ? "object" : "string" }, granted, + schema: { type: field.type === "blob" ? "object" : "string" }, + type: field.type, usable: granted, }, ]; }) ), + name: stream.key, + object: "stream_metadata", }; } @@ -325,9 +325,9 @@ function sandboxWindowMeta(connectorId: string, streamKey: string): RecordsPage[ .sort(); return { window: { - total: streamRecords(streamKey).filter((record) => record.connector_id === connectorId).length, earliest_at: times[0] ?? null, latest_at: times.at(-1) ?? null, + total: streamRecords(streamKey).filter((record) => record.connector_id === connectorId).length, }, }; } @@ -336,12 +336,12 @@ function sandboxWindowMeta(connectorId: string, streamKey: string): RecordsPage[ function actorFor(event: DemoTimelineEvent): { actor_type: string | null; actor_id: string | null } { if (event.run_id) { - return { actor_type: "runtime", actor_id: deriveRunConnector(event.run_id) ?? "demo" }; + return { actor_id: deriveRunConnector(event.run_id) ?? "demo", actor_type: "runtime" }; } if (event.client_id) { - return { actor_type: "client", actor_id: event.client_id }; + return { actor_id: event.client_id, actor_type: "client" }; } - return { actor_type: "owner", actor_id: "demo-owner" }; + return { actor_id: "demo-owner", actor_type: "owner" }; } function deriveRunConnector(runId: string): string | null { @@ -391,10 +391,10 @@ function adaptTimeline(envelope: ReturnType): Timelin } const events = envelope.events.map(eventToSpineEvent); return { - object: envelope.object, - trace_id: envelope.trace_id, event_count: events.length, events, + object: envelope.object, + trace_id: envelope.trace_id, }; } @@ -411,9 +411,9 @@ function paginate(rows: readonly T[], opts: { limit?: number; cursor?: string const next = start + limit; const hasMore = next < rows.length; return { - object: "list", data: slice as T[], has_more: hasMore, + object: "list", ...(hasMore ? { next_cursor: String(next) } : {}), }; } @@ -426,54 +426,54 @@ function adaptGrantSummary(g: ReturnType[number]): LiveGra failure = { event_type: "grant.revoked", reason: "grant_revoked" }; } return { - object: "grant_summary", - grant_id: g.grant_id, client_id: g.client_id, connector_id: g.connector_id, - provider_id: null, - status: g.status, - kinds: g.events.map((e) => e.event_type), event_count: g.events.length, + failure, first_at: g.first_at, + grant_id: g.grant_id, + kinds: g.events.map((e) => e.event_type), last_at: g.last_at, - failure, + object: "grant_summary", + provider_id: null, + status: g.status, }; } function adaptRunSummary(r: ReturnType[number]): LiveRunSummary { return { - object: "run_summary", - run_id: r.run_id, connector_id: r.connector_id, - grant_id: r.grant_id, - provider_id: null, - status: r.status, - needs_input: r.needs_input, - failure_reason: r.failure_reason, - kinds: r.events.map((e) => e.event_type), event_count: r.events.length, + failure_reason: r.failure_reason, first_at: r.first_at, + grant_id: r.grant_id, + kinds: r.events.map((e) => e.event_type), last_at: r.last_at, + needs_input: r.needs_input, + object: "run_summary", + provider_id: null, + run_id: r.run_id, + status: r.status, }; } function adaptTraceSummary(t: ReturnType[number]): LiveTraceSummary { return { - object: "trace_summary", - trace_id: t.trace_id, actor_id: t.client_id ?? null, actor_type: t.client_id ? "client" : "runtime", client_id: t.client_id, + event_count: t.kinds.length, + failure: t.failure_reason ? { event_type: "trace", reason: t.failure_reason } : null, + first_at: t.first_at, grant_id: t.grant_id, + kinds: [...t.kinds], + last_at: t.last_at, + object: "trace_summary", provider_id: null, request_id: null, run_id: t.run_id, status: t.status, - kinds: [...t.kinds], - event_count: t.kinds.length, - first_at: t.first_at, - last_at: t.last_at, - failure: t.failure_reason ? { event_type: "trace", reason: t.failure_reason } : null, + trace_id: t.trace_id, }; } @@ -481,10 +481,10 @@ function adaptTraceSummary(t: ReturnType[number]): LiveTra function searchHitToLiveHit(hit: ReturnType["data"][number]): SearchResultHit { return { - object: "search_result", connector_id: hit.connector_id, emitted_at: hit.emitted_at, matched_fields: hit.matched_fields, + object: "search_result", record_key: hit.record_key, record_url: hit.record_url, snippet: hit.snippet, @@ -500,22 +500,15 @@ function buildSandboxDeploymentDiagnostics(): DeploymentDiagnostics { const semanticCapability = DEMO_CAPABILITIES.find((c) => c.capability === "semantic_search"); return { database: { path: "(sandbox: in-memory deterministic dataset)" }, - runtime_capabilities: { - bindings: { browser: false, filesystem: false, local_device: false, network: true }, - accepted_collector_protocol_versions: ["1"], - collector_pairing: null, - collector_paired: false, - in_container: false, - }, environment: [ - { name: "PDPP_REFERENCE_MODE", value: "sandbox", provenance: "present", secret: false }, - { name: "PDPP_DB_PATH", value: null, provenance: "absent", secret: false }, - { name: "PDPP_ALLOW_MODEL_DOWNLOAD", value: null, provenance: "absent", secret: false }, + { name: "PDPP_REFERENCE_MODE", provenance: "present", secret: false, value: "sandbox" }, + { name: "PDPP_DB_PATH", provenance: "absent", secret: false, value: null }, + { name: "PDPP_ALLOW_MODEL_DOWNLOAD", provenance: "absent", secret: false, value: null }, ], lexical: { index: { - state: "built", backfill_progress: null, + state: "built", }, }, manifests: connectors.map((c) => ({ @@ -524,29 +517,36 @@ function buildSandboxDeploymentDiagnostics(): DeploymentDiagnostics { provenance: c.provenance, semantic_stream_count: 0, })), + runtime_capabilities: { + accepted_collector_protocol_versions: ["1"], + bindings: { browser: false, filesystem: false, local_device: false, network: true }, + collector_paired: false, + collector_pairing: null, + in_container: false, + }, semantic: { backend: { - configured: false, available: Boolean(semanticCapability?.implemented), - profile_id: null, - model: null, - dtype: null, + configured: false, dimensions: null, distance_metric: null, + download_allowed: null, + dtype: null, language_bias: null, + model: null, model_cache_path: null, model_cache_present: null, - download_allowed: null, + profile_id: null, }, index: { + backfill_progress: null, kind: null, state: null, - backfill_progress: null, }, participation: { connector_count: 0, - stream_count: 0, field_count: 0, + stream_count: 0, tuples: [], }, }, @@ -608,30 +608,6 @@ function sandboxBucketKey(recordTime: string, granularity: TimeBucketGranularity // ─── DashboardDataSource implementation ─────────────────────────────────── export const sandboxDashboardDataSource: DashboardDataSource = { - kind: "sandbox", - supportsExploreTimelineDirection: async () => true, - - async listConnectorSummaries(): Promise> { - const connectors = getDemoConnectors(); - return { - object: "list", - has_more: false, - data: connectors.map((c) => buildRefConnectorSummary(c.connector_id)), - }; - }, - - async listConnectorManifests(): Promise { - return getDemoConnectors().map((c) => buildConnectorManifest(c.connector_id)); - }, - - async listStreams(connectorId: string): Promise { - return streamsForConnector(connectorId).map((s) => buildStreamSummary(s.key)); - }, - - async getStreamMetadata(connectorId: string, stream: string): Promise { - return buildSandboxStreamMetadata(connectorId, stream); - }, - async aggregateRecordsByTime( connectorId: string, stream: string, @@ -658,7 +634,7 @@ export const sandboxDashboardDataSource: DashboardDataSource = { counts.set(key, (counts.get(key) ?? 0) + 1); } const groups: AggregateTimeBucket[] = [...counts.entries()] - .map(([key, count]) => ({ key: key === "__null__" ? null : key, count })) + .map(([key, count]) => ({ count, key: key === "__null__" ? null : key })) .sort((a, b) => { if (a.key == null) { return b.key == null ? 0 : 1; @@ -669,15 +645,102 @@ export const sandboxDashboardDataSource: DashboardDataSource = { return a.key.localeCompare(b.key); }); return { - object: "aggregation", - stream, - metric: "count", - group_by_time: opts.groupByTime, - granularity: opts.granularity, - time_zone: opts.timeZone ?? "UTC", approximate: false, filtered_record_count: filtered, + granularity: opts.granularity, + group_by_time: opts.groupByTime, groups, + metric: "count", + object: "aggregation", + stream, + time_zone: opts.timeZone ?? "UTC", + }; + }, + + async getConnectorOverview(connector: ConnectorManifest): Promise { + const streams = await this.listStreams(connector.connector_id); + const lastRun = connectorRunRef(connectorRecentRunIds(connector.connector_id)[0]); + const lastSuccessfulRunId = getDemoRuns().find( + (r) => r.connector_id === connector.connector_id && r.status === "succeeded" + )?.run_id; + const lastSuccessfulRun = connectorRunRef(lastSuccessfulRunId); + return { + connector, + isRunning: lastRun != null && new Set(["started", "in_progress"]).has(lastRun.status), + lastRun, + lastSuccessfulRun, + streams, + totalRecords: streams.reduce((sum, s) => sum + (s.record_count ?? 0), 0), + }; + }, + + async getDatasetSummary(): Promise { + // Mount the canonical `ref.dataset.summary` operation with sandbox + // fixture dependencies so the dashboard data source returns the same + // envelope the public `/sandbox/_ref/dataset/summary` route returns. + // The operation owns envelope assembly (object, total_retained_bytes, + // top-connector sort/limit/wrap, empty-corpus collapse, ingest-vs- + // record-time distinction) — the previous local mapping silently drifted + // (`record_json_bytes` was mapped from the demo `blob_bytes`, and the + // `*_ingested_at` bounds were sourced from record-time fields rather + // than the substrate's ingest-time bounds). Routing through the + // operation removes that drift class for the dashboard surface. + return await executeRefDatasetSummary(createSandboxRefDatasetSummaryDependencies()); + }, + + async getDeploymentDiagnostics(): Promise { + return buildSandboxDeploymentDiagnostics(); + }, + + async getGrantTimeline(grantId: string): Promise { + return adaptTimeline(buildGrantTimeline(grantId)); + }, + + async getRecord( + _connectorId: string, + stream: string, + recordId: string, + _opts?: { connectorInstanceId?: string | null } + ): Promise { + const record = DEMO_RECORDS.find((r) => r.stream === stream && r.record_id === recordId); + if (!record) { + // Match the live RS error shape so the existing 404 detection works. + throw new Error(`RS /v1/streams/${stream}/records/${recordId} failed (404): not found`); + } + return recordToStreamRecord(record); + }, + + async getRunTimeline(runId: string): Promise { + return adaptTimeline(buildRunTimeline(runId)); + }, + + async getStreamMetadata(connectorId: string, stream: string): Promise { + return buildSandboxStreamMetadata(connectorId, stream); + }, + + async getTraceTimeline(traceId: string): Promise { + return adaptTimeline(buildTraceTimeline(traceId)); + }, + + async isHybridRetrievalAdvertised(): Promise { + return false; + }, + + async isSemanticRetrievalAdvertised(): Promise { + return false; + }, + kind: "sandbox", + + async listConnectorManifests(): Promise { + return getDemoConnectors().map((c) => buildConnectorManifest(c.connector_id)); + }, + + async listConnectorSummaries(): Promise> { + const connectors = getDemoConnectors(); + return { + data: connectors.map((c) => buildRefConnectorSummary(c.connector_id)), + has_more: false, + object: "list", }; }, @@ -727,11 +790,11 @@ export const sandboxDashboardDataSource: DashboardDataSource = { // `minMs` means no in-scope record had a parseable time → an empty series. if (!Number.isFinite(minMs)) { return { - object: "explore_record_buckets", + buckets: [], + extent: { count: 0, end: null, start: null }, granularity: "day", + object: "explore_record_buckets", time_zone: opts.timeZone ?? "UTC", - extent: { start: null, end: null, count: 0 }, - buckets: [], }; } @@ -743,22 +806,22 @@ export const sandboxDashboardDataSource: DashboardDataSource = { const start = new Date(cursor).toISOString(); const dayKey = start.slice(0, 10); buckets.push({ - start, - end: new Date(cursor + MS_PER_DAY).toISOString(), count: dayCounts.get(dayKey) ?? 0, + end: new Date(cursor + MS_PER_DAY).toISOString(), + start, }); } return { - object: "explore_record_buckets", - granularity: "day", - time_zone: opts.timeZone ?? "UTC", + buckets, extent: { - start: new Date(minMs).toISOString(), - end: new Date(maxMs).toISOString(), count: inScope.length, + end: new Date(maxMs).toISOString(), + start: new Date(minMs).toISOString(), }, - buckets, + granularity: "day", + object: "explore_record_buckets", + time_zone: opts.timeZone ?? "UTC", }; }, @@ -786,43 +849,63 @@ export const sandboxDashboardDataSource: DashboardDataSource = { ascending ? a.record_time.localeCompare(b.record_time) : b.record_time.localeCompare(a.record_time) ) .map((r) => ({ - object: "timeline_record" as const, connector_id: r.connector_id, connector_instance_id: r.connector_id, - stream: r.stream, - record_key: r.record_id, - emitted_at: r.record_time, data: r.fields, + emitted_at: r.record_time, + object: "timeline_record" as const, + record_key: r.record_id, + stream: r.stream, })); // REWIND: the demo has no snapshot drift (fixed corpus), so re-rendering page 1 // means paginating from the start. Honor it by ignoring the cursor offset. const cursorForPage = opts.rewindToFirstPage ? undefined : (opts.cursor ?? undefined); const page = paginate(merged, { cursor: cursorForPage, limit: opts.limit }); return { - object: "list", data: page.data, has_more: page.has_more, + new_since_snapshot: 0, next_cursor: page.next_cursor ?? null, + object: "list", snapshot_at: merged[0]?.emitted_at ?? new Date(0).toISOString(), - new_since_snapshot: 0, }; }, - async getConnectorOverview(connector: ConnectorManifest): Promise { - const streams = await this.listStreams(connector.connector_id); - const lastRun = connectorRunRef(connectorRecentRunIds(connector.connector_id)[0]); - const lastSuccessfulRunId = getDemoRuns().find( - (r) => r.connector_id === connector.connector_id && r.status === "succeeded" - )?.run_id; - const lastSuccessfulRun = connectorRunRef(lastSuccessfulRunId); - return { - connector, - streams, - totalRecords: streams.reduce((sum, s) => sum + (s.record_count ?? 0), 0), - lastRun, - lastSuccessfulRun, - isRunning: lastRun != null && new Set(["started", "in_progress"]).has(lastRun.status), - }; + async listGrants(opts: ListQuery = {}): Promise> { + const all = getDemoGrants() + .filter((g) => statusFilterMatches(opts.status, g.status)) + .filter((g) => !opts.client_id || g.client_id === opts.client_id) + .filter((g) => !opts.q || g.grant_id.toLowerCase().includes(opts.q.toLowerCase())) + .map(adaptGrantSummary); + return paginate(all, opts); + }, + + async listPendingApprovals(): Promise> { + // The sandbox is read-only; there are never live device-flow or + // consent approvals waiting for an owner. Always empty. + return { data: [], has_more: false, object: "list" }; + }, + + async listRuns(opts: ListQuery = {}): Promise> { + const all = getDemoRuns() + .filter((r) => statusFilterMatches(opts.status, r.status)) + .filter((r) => !opts.connector_id || r.connector_id === opts.connector_id) + .filter((r) => !opts.q || r.run_id.toLowerCase().includes(opts.q.toLowerCase())) + .map(adaptRunSummary); + return paginate(all, opts); + }, + + async listStreams(connectorId: string): Promise { + return streamsForConnector(connectorId).map((s) => buildStreamSummary(s.key)); + }, + + async listTraces(opts: ListQuery = {}): Promise> { + const all = getDemoTraces() + .filter((t) => statusFilterMatches(opts.status, t.status)) + .filter((t) => !opts.client_id || t.client_id === opts.client_id) + .filter((t) => !opts.q || t.trace_id.toLowerCase().includes(opts.q.toLowerCase())) + .map(adaptTraceSummary); + return paginate(all, opts); }, async queryRecords( @@ -838,44 +921,30 @@ export const sandboxDashboardDataSource: DashboardDataSource = { ): Promise { const built = buildRecordsList({ connector_id: connectorId, - stream, - limit: opts.limit ?? 50, cursor: opts.cursor ?? null, + limit: opts.limit ?? 50, + stream, }); if (!built) { - return { object: "list", data: [], has_more: false }; + return { data: [], has_more: false, object: "list" }; } const records = built.data .map((summary) => DEMO_RECORDS.find((r) => r.record_id === summary.record_id)) .filter((r): r is DemoRecord => Boolean(r)) .map(recordToStreamRecord); return { - object: "list", data: records, has_more: built.has_more, + object: "list", ...(opts.window === "exact" ? { meta: sandboxWindowMeta(connectorId, stream) } : {}), ...(built.next_cursor ? { next_cursor: built.next_cursor } : {}), }; }, - async getRecord( - _connectorId: string, - stream: string, - recordId: string, - _opts?: { connectorInstanceId?: string | null } - ): Promise { - const record = DEMO_RECORDS.find((r) => r.stream === stream && r.record_id === recordId); - if (!record) { - // Match the live RS error shape so the existing 404 detection works. - throw new Error(`RS /v1/streams/${stream}/records/${recordId} failed (404): not found`); - } - return recordToStreamRecord(record); - }, - async refSearch(query: string) { const trimmed = query.trim().toLowerCase(); if (!trimmed) { - return { object: "search_result", traces: [], grants: [], runs: [], exact: null }; + return { exact: null, grants: [], object: "search_result", runs: [], traces: [] }; } const grants = getDemoGrants() .filter((g) => g.grant_id.toLowerCase().includes(trimmed) || g.client_id.toLowerCase().includes(trimmed)) @@ -889,17 +958,23 @@ export const sandboxDashboardDataSource: DashboardDataSource = { let exact: { kind: "trace" | "grant" | "run"; id: string } | null = null; const exactGrant = getDemoGrants().find((g) => g.grant_id === query); if (exactGrant) { - exact = { kind: "grant", id: exactGrant.grant_id }; + exact = { id: exactGrant.grant_id, kind: "grant" }; } const exactRun = getDemoRuns().find((r) => r.run_id === query); if (exactRun) { - exact = { kind: "run", id: exactRun.run_id }; + exact = { id: exactRun.run_id, kind: "run" }; } const exactTrace = getDemoTraces().find((t) => t.trace_id === query); if (exactTrace) { - exact = { kind: "trace", id: exactTrace.trace_id }; + exact = { id: exactTrace.trace_id, kind: "trace" }; } - return { object: "search_result", traces, grants, runs, exact }; + return { exact, grants, object: "search_result", runs, traces }; + }, + + async searchRecordsHybrid(): Promise { + // Sandbox does not advertise hybrid retrieval. This method should never + // be reached because isHybridRetrievalAdvertised() returns false. + return { data: [], has_more: false, object: "list" }; }, async searchRecordsLexical(query: string, opts: { limit?: number; cursor?: string } = {}): Promise { @@ -910,9 +985,9 @@ export const sandboxDashboardDataSource: DashboardDataSource = { const next = start + limit; const hasMore = next < built.data.length; return { - object: "list", data: slice, has_more: hasMore, + object: "list", ...(hasMore ? { next_cursor: String(next) } : {}), }; }, @@ -922,83 +997,7 @@ export const sandboxDashboardDataSource: DashboardDataSource = { // listed in capabilities as "implemented but not demonstrated in demo". // Returning an empty page keeps the dashboard search view's "blend" // logic intact without claiming semantic results. - return { object: "list", data: [], has_more: false }; - }, - - async isSemanticRetrievalAdvertised(): Promise { - return false; - }, - - async searchRecordsHybrid(): Promise { - // Sandbox does not advertise hybrid retrieval. This method should never - // be reached because isHybridRetrievalAdvertised() returns false. - return { object: "list", data: [], has_more: false }; - }, - - async isHybridRetrievalAdvertised(): Promise { - return false; - }, - - async listGrants(opts: ListQuery = {}): Promise> { - const all = getDemoGrants() - .filter((g) => statusFilterMatches(opts.status, g.status)) - .filter((g) => !opts.client_id || g.client_id === opts.client_id) - .filter((g) => !opts.q || g.grant_id.toLowerCase().includes(opts.q.toLowerCase())) - .map(adaptGrantSummary); - return paginate(all, opts); - }, - - async listRuns(opts: ListQuery = {}): Promise> { - const all = getDemoRuns() - .filter((r) => statusFilterMatches(opts.status, r.status)) - .filter((r) => !opts.connector_id || r.connector_id === opts.connector_id) - .filter((r) => !opts.q || r.run_id.toLowerCase().includes(opts.q.toLowerCase())) - .map(adaptRunSummary); - return paginate(all, opts); - }, - - async listTraces(opts: ListQuery = {}): Promise> { - const all = getDemoTraces() - .filter((t) => statusFilterMatches(opts.status, t.status)) - .filter((t) => !opts.client_id || t.client_id === opts.client_id) - .filter((t) => !opts.q || t.trace_id.toLowerCase().includes(opts.q.toLowerCase())) - .map(adaptTraceSummary); - return paginate(all, opts); - }, - - async getGrantTimeline(grantId: string): Promise { - return adaptTimeline(buildGrantTimeline(grantId)); - }, - - async getRunTimeline(runId: string): Promise { - return adaptTimeline(buildRunTimeline(runId)); - }, - - async getTraceTimeline(traceId: string): Promise { - return adaptTimeline(buildTraceTimeline(traceId)); - }, - - async getDatasetSummary(): Promise { - // Mount the canonical `ref.dataset.summary` operation with sandbox - // fixture dependencies so the dashboard data source returns the same - // envelope the public `/sandbox/_ref/dataset/summary` route returns. - // The operation owns envelope assembly (object, total_retained_bytes, - // top-connector sort/limit/wrap, empty-corpus collapse, ingest-vs- - // record-time distinction) — the previous local mapping silently drifted - // (`record_json_bytes` was mapped from the demo `blob_bytes`, and the - // `*_ingested_at` bounds were sourced from record-time fields rather - // than the substrate's ingest-time bounds). Routing through the - // operation removes that drift class for the dashboard surface. - return await executeRefDatasetSummary(createSandboxRefDatasetSummaryDependencies()); - }, - - async listPendingApprovals(): Promise> { - // The sandbox is read-only; there are never live device-flow or - // consent approvals waiting for an owner. Always empty. - return { object: "list", data: [], has_more: false }; - }, - - async getDeploymentDiagnostics(): Promise { - return buildSandboxDeploymentDiagnostics(); + return { data: [], has_more: false, object: "list" }; }, + supportsExploreTimelineDirection: async () => true, }; diff --git a/apps/site/src/app/sandbox/_demo/dataset.test.ts b/apps/site/src/app/sandbox/_demo/dataset.test.ts index 179692fd0..f752c4862 100644 --- a/apps/site/src/app/sandbox/_demo/dataset.test.ts +++ b/apps/site/src/app/sandbox/_demo/dataset.test.ts @@ -104,14 +104,14 @@ test("capabilities include the headline reference flows", () => { test("seeded data uses safe public domains only", () => { const blob = JSON.stringify({ - DEMO_CONNECTORS, + DEMO_CAPABILITIES, DEMO_CLIENTS, + DEMO_CONNECTORS, DEMO_GRANTS, DEMO_RECORDS, DEMO_RUNS, DEMO_STREAMS, DEMO_TRACES, - DEMO_CAPABILITIES, }); for (const m of blob.matchAll(REAL_LOOKING_DOMAIN_RE)) { assert.ok( @@ -126,14 +126,14 @@ test("seeded data uses safe public domains only", () => { test("seeded data contains no obvious credentials, tokens, or PII shibboleths", () => { const blob = JSON.stringify({ - DEMO_CONNECTORS, + DEMO_CAPABILITIES, DEMO_CLIENTS, + DEMO_CONNECTORS, DEMO_GRANTS, DEMO_RECORDS, DEMO_RUNS, DEMO_STREAMS, DEMO_TRACES, - DEMO_CAPABILITIES, }); for (const re of FORBIDDEN_CREDENTIAL_PATTERNS) { assert.ok(!re.test(blob), `dataset matched forbidden pattern ${re}`); diff --git a/apps/site/src/app/sandbox/_demo/dataset.ts b/apps/site/src/app/sandbox/_demo/dataset.ts index 586fb3d64..5187c55cf 100644 --- a/apps/site/src/app/sandbox/_demo/dataset.ts +++ b/apps/site/src/app/sandbox/_demo/dataset.ts @@ -38,27 +38,27 @@ export const DEMO_ISSUER = "https://pdpp.example.invalid/sandbox"; export const DEMO_CLIENTS: readonly DemoClientDef[] = [ { client_id: "client_quill_tax_demo", + client_uri: "https://example.invalid/quill-tax", display_name: "Quill Tax (simulated)", logo_initials: "QT", - client_uri: "https://example.invalid/quill-tax", policy_uri: "https://example.invalid/quill-tax/privacy", tos_uri: "https://example.invalid/quill-tax/terms", verified: true, }, { client_id: "client_sherwood_health_demo", + client_uri: "https://example.invalid/sherwood-health", display_name: "Sherwood Health (simulated)", logo_initials: "SH", - client_uri: "https://example.invalid/sherwood-health", policy_uri: "https://example.invalid/sherwood-health/privacy", tos_uri: "https://example.invalid/sherwood-health/terms", verified: false, }, { client_id: "client_ledger_atlas_demo", + client_uri: "https://example.invalid/ledger-atlas", display_name: "Ledger Atlas (simulated)", logo_initials: "LA", - client_uri: "https://example.invalid/ledger-atlas", policy_uri: "https://example.invalid/ledger-atlas/privacy", tos_uri: "https://example.invalid/ledger-atlas/terms", verified: true, @@ -70,28 +70,28 @@ export const DEMO_CLIENTS: readonly DemoClientDef[] = [ export const DEMO_CONNECTORS: readonly DemoConnectorDef[] = [ { connector_id: "acme_payroll_demo", - provider_id: "provider_acme_payroll_demo", - display_name: "Acme Payroll (simulated)", description: "Demo payroll connector for Acme Corporation pay statements.", + display_name: "Acme Payroll (simulated)", provenance: "native", + provider_id: "provider_acme_payroll_demo", schedule: "weekly", streams: ["pay_statements", "tax_documents"], }, { connector_id: "northwind_health_demo", - provider_id: "provider_northwind_health_demo", - display_name: "Northwind Health (simulated)", description: "Demo personal-health connector for claims and visit history.", + display_name: "Northwind Health (simulated)", provenance: "polyfill-registered", + provider_id: "provider_northwind_health_demo", schedule: "manual", streams: ["clinical_visits"], }, { connector_id: "fabrikam_bank_demo", - provider_id: "provider_fabrikam_bank_demo", - display_name: "Fabrikam Bank (simulated)", description: "Stand-in retail-bank connector for demo. Numbers and balances are invented.", + display_name: "Fabrikam Bank (simulated)", provenance: "native", + provider_id: "provider_fabrikam_bank_demo", schedule: "daily", streams: ["transactions"], }, @@ -102,111 +102,111 @@ export const DEMO_CONNECTORS: readonly DemoConnectorDef[] = [ export const DEMO_STREAMS: readonly DemoStreamDef[] = [ { connector_id: "acme_payroll_demo", - key: "pay_statements", - label: "Pay statements", - description: "Per-period gross/net pay totals and the issuing employer.", - retention_label: "Retained while this demo dataset is seeded.", - latest_record_time: "2026-03-31T00:00:00Z", consent_time_field: "period_end", + description: "Per-period gross/net pay totals and the issuing employer.", fields: [ - { name: "period_end", type: "timestamp", semantic_class: "common", description: "End of the pay period." }, - { name: "employer", type: "string", semantic_class: "common", description: "Issuing employer display name." }, + { description: "End of the pay period.", name: "period_end", semantic_class: "common", type: "timestamp" }, + { description: "Issuing employer display name.", name: "employer", semantic_class: "common", type: "string" }, { + description: "Gross pay in USD minor units.", name: "gross_pay_cents", - type: "currency_minor_units", semantic_class: "sensitive", - description: "Gross pay in USD minor units.", + type: "currency_minor_units", }, { + description: "Net pay in USD minor units.", name: "net_pay_cents", - type: "currency_minor_units", semantic_class: "sensitive", - description: "Net pay in USD minor units.", + type: "currency_minor_units", }, - { name: "currency", type: "string", semantic_class: "common", description: "ISO-4217 currency code." }, + { description: "ISO-4217 currency code.", name: "currency", semantic_class: "common", type: "string" }, ], + key: "pay_statements", + label: "Pay statements", + latest_record_time: "2026-03-31T00:00:00Z", + retention_label: "Retained while this demo dataset is seeded.", }, { connector_id: "acme_payroll_demo", - key: "tax_documents", - label: "Tax documents", - description: "End-of-year tax document index from the simulated payroll provider.", - retention_label: "Retained while this demo dataset is seeded.", - latest_record_time: "2026-01-31T00:00:00Z", consent_time_field: "issued_at", + description: "End-of-year tax document index from the simulated payroll provider.", fields: [ - { name: "year", type: "number", semantic_class: "common", description: "Tax year covered by the document." }, - { name: "document_kind", type: "string", semantic_class: "common", description: "e.g. W2, 1099." }, + { description: "Tax year covered by the document.", name: "year", semantic_class: "common", type: "number" }, + { description: "e.g. W2, 1099.", name: "document_kind", semantic_class: "common", type: "string" }, { + description: "Issuing entity display name.", name: "issuer", - type: "string", semantic_class: "common", - description: "Issuing entity display name.", + type: "string", }, { + description: "When the document was issued.", name: "issued_at", - type: "timestamp", semantic_class: "common", - description: "When the document was issued.", + type: "timestamp", }, { + description: "Reference to the simulated document bytes exposed through the blob read path.", name: "blob_ref", - type: "blob", semantic_class: "sensitive", - description: "Reference to the simulated document bytes exposed through the blob read path.", + type: "blob", }, ], + key: "tax_documents", + label: "Tax documents", + latest_record_time: "2026-01-31T00:00:00Z", + retention_label: "Retained while this demo dataset is seeded.", }, { connector_id: "northwind_health_demo", - key: "clinical_visits", - label: "Clinical visits", - description: "Visit summary entries from the simulated personal-health provider.", - retention_label: "Retained while this demo dataset is seeded.", - latest_record_time: "2026-02-14T00:00:00Z", consent_time_field: "visit_at", + description: "Visit summary entries from the simulated personal-health provider.", fields: [ - { name: "visit_at", type: "timestamp", semantic_class: "common", description: "When the visit occurred." }, + { description: "When the visit occurred.", name: "visit_at", semantic_class: "common", type: "timestamp" }, { + description: "Practitioner display name.", name: "provider_name", - type: "string", semantic_class: "identifying", - description: "Practitioner display name.", + type: "string", }, { + description: "Short visit summary written for the patient.", name: "summary", - type: "string", semantic_class: "sensitive", - description: "Short visit summary written for the patient.", + type: "string", }, { + description: "Whether the simulated provider flagged follow-up.", name: "follow_up_needed", - type: "boolean", semantic_class: "common", - description: "Whether the simulated provider flagged follow-up.", + type: "boolean", }, ], + key: "clinical_visits", + label: "Clinical visits", + latest_record_time: "2026-02-14T00:00:00Z", + retention_label: "Retained while this demo dataset is seeded.", }, { connector_id: "fabrikam_bank_demo", - key: "transactions", - label: "Transactions", - description: "Per-transaction posting from the simulated bank account.", - retention_label: "Retained while this demo dataset is seeded.", - latest_record_time: "2026-04-22T00:00:00Z", consent_time_field: "posted_at", + description: "Per-transaction posting from the simulated bank account.", fields: [ - { name: "posted_at", type: "timestamp", semantic_class: "common", description: "When the transaction posted." }, - { name: "merchant", type: "string", semantic_class: "common", description: "Merchant display name." }, + { description: "When the transaction posted.", name: "posted_at", semantic_class: "common", type: "timestamp" }, + { description: "Merchant display name.", name: "merchant", semantic_class: "common", type: "string" }, { + description: "Transaction amount in USD minor units; negative for debits.", name: "amount_cents", - type: "currency_minor_units", semantic_class: "sensitive", - description: "Transaction amount in USD minor units; negative for debits.", + type: "currency_minor_units", }, - { name: "currency", type: "string", semantic_class: "common", description: "ISO-4217 currency code." }, - { name: "category", type: "string", semantic_class: "common", description: "Provider-assigned category." }, + { description: "ISO-4217 currency code.", name: "currency", semantic_class: "common", type: "string" }, + { description: "Provider-assigned category.", name: "category", semantic_class: "common", type: "string" }, ], + key: "transactions", + label: "Transactions", + latest_record_time: "2026-04-22T00:00:00Z", + retention_label: "Retained while this demo dataset is seeded.", }, ] as const; @@ -214,133 +214,133 @@ export const DEMO_STREAMS: readonly DemoStreamDef[] = [ export const DEMO_RECORDS: readonly DemoRecord[] = [ { - record_id: "rec_sb_paystmt_2026_03", connector_id: "acme_payroll_demo", - stream: "pay_statements", - record_time: "2026-03-31T00:00:00Z", - ingested_at: "2026-04-01T05:14:00Z", fields: { - period_end: "2026-03-31", + currency: "USD", employer: "Northwind Studios (simulated)", gross_pay_cents: 612_500, net_pay_cents: 438_120, - currency: "USD", + period_end: "2026-03-31", }, + ingested_at: "2026-04-01T05:14:00Z", + record_id: "rec_sb_paystmt_2026_03", + record_time: "2026-03-31T00:00:00Z", + stream: "pay_statements", }, { - record_id: "rec_sb_paystmt_2026_02", connector_id: "acme_payroll_demo", - stream: "pay_statements", - record_time: "2026-02-28T00:00:00Z", - ingested_at: "2026-03-01T05:11:00Z", fields: { - period_end: "2026-02-28", + currency: "USD", employer: "Northwind Studios (simulated)", gross_pay_cents: 612_500, net_pay_cents: 437_980, - currency: "USD", + period_end: "2026-02-28", }, + ingested_at: "2026-03-01T05:11:00Z", + record_id: "rec_sb_paystmt_2026_02", + record_time: "2026-02-28T00:00:00Z", + stream: "pay_statements", }, { - record_id: "rec_sb_paystmt_2026_01", connector_id: "acme_payroll_demo", - stream: "pay_statements", - record_time: "2026-01-31T00:00:00Z", - ingested_at: "2026-02-01T05:09:00Z", fields: { - period_end: "2026-01-31", + currency: "USD", employer: "Northwind Studios (simulated)", gross_pay_cents: 612_500, net_pay_cents: 436_640, - currency: "USD", + period_end: "2026-01-31", }, + ingested_at: "2026-02-01T05:09:00Z", + record_id: "rec_sb_paystmt_2026_01", + record_time: "2026-01-31T00:00:00Z", + stream: "pay_statements", }, { - record_id: "rec_sb_taxdoc_2025_w2", connector_id: "acme_payroll_demo", - stream: "tax_documents", - record_time: "2026-01-31T00:00:00Z", - ingested_at: "2026-02-02T08:00:00Z", fields: { - year: 2025, - document_kind: "W2", - issuer: "Northwind Studios (simulated)", - issued_at: "2026-01-31T00:00:00Z", blob_ref: { blob_id: "blob_sb_taxdoc_2025_w2", content_type: "application/pdf", fetch_url: "/v1/blobs/blob_sb_taxdoc_2025_w2", size_bytes: 184_320, }, + document_kind: "W2", + issued_at: "2026-01-31T00:00:00Z", + issuer: "Northwind Studios (simulated)", + year: 2025, }, + ingested_at: "2026-02-02T08:00:00Z", + record_id: "rec_sb_taxdoc_2025_w2", + record_time: "2026-01-31T00:00:00Z", + stream: "tax_documents", }, { - record_id: "rec_sb_visit_2026_02", connector_id: "northwind_health_demo", - stream: "clinical_visits", - record_time: "2026-02-14T15:30:00Z", - ingested_at: "2026-02-15T09:00:00Z", fields: { - visit_at: "2026-02-14T15:30:00Z", + follow_up_needed: false, provider_name: "Dr. Avery Hale (simulated)", summary: "Annual physical. Routine bloodwork ordered. Follow-up not required.", - follow_up_needed: false, + visit_at: "2026-02-14T15:30:00Z", }, + ingested_at: "2026-02-15T09:00:00Z", + record_id: "rec_sb_visit_2026_02", + record_time: "2026-02-14T15:30:00Z", + stream: "clinical_visits", }, { - record_id: "rec_sb_visit_2025_11", connector_id: "northwind_health_demo", - stream: "clinical_visits", - record_time: "2025-11-09T09:00:00Z", - ingested_at: "2025-11-10T07:30:00Z", fields: { - visit_at: "2025-11-09T09:00:00Z", + follow_up_needed: true, provider_name: "Dr. Iris Park (simulated)", summary: "Sinus infection. Prescribed amoxicillin. Re-check in two weeks.", - follow_up_needed: true, + visit_at: "2025-11-09T09:00:00Z", }, + ingested_at: "2025-11-10T07:30:00Z", + record_id: "rec_sb_visit_2025_11", + record_time: "2025-11-09T09:00:00Z", + stream: "clinical_visits", }, { - record_id: "rec_sb_txn_2026_04_22", connector_id: "fabrikam_bank_demo", - stream: "transactions", - record_time: "2026-04-22T13:42:00Z", - ingested_at: "2026-04-22T14:00:00Z", fields: { - posted_at: "2026-04-22T13:42:00Z", - merchant: "Bluebird Bakery (simulated)", amount_cents: -1245, - currency: "USD", category: "food_drink", + currency: "USD", + merchant: "Bluebird Bakery (simulated)", + posted_at: "2026-04-22T13:42:00Z", }, + ingested_at: "2026-04-22T14:00:00Z", + record_id: "rec_sb_txn_2026_04_22", + record_time: "2026-04-22T13:42:00Z", + stream: "transactions", }, { - record_id: "rec_sb_txn_2026_04_18", connector_id: "fabrikam_bank_demo", - stream: "transactions", - record_time: "2026-04-18T19:08:00Z", - ingested_at: "2026-04-18T19:30:00Z", fields: { - posted_at: "2026-04-18T19:08:00Z", - merchant: "Acme Payroll (simulated)", amount_cents: 438_120, - currency: "USD", category: "income_payroll", + currency: "USD", + merchant: "Acme Payroll (simulated)", + posted_at: "2026-04-18T19:08:00Z", }, + ingested_at: "2026-04-18T19:30:00Z", + record_id: "rec_sb_txn_2026_04_18", + record_time: "2026-04-18T19:08:00Z", + stream: "transactions", }, { - record_id: "rec_sb_txn_2026_04_05", connector_id: "fabrikam_bank_demo", - stream: "transactions", - record_time: "2026-04-05T10:00:00Z", - ingested_at: "2026-04-05T10:30:00Z", fields: { - posted_at: "2026-04-05T10:00:00Z", - merchant: "Fabrikam Insurance (simulated)", amount_cents: -28_400, - currency: "USD", category: "insurance", + currency: "USD", + merchant: "Fabrikam Insurance (simulated)", + posted_at: "2026-04-05T10:00:00Z", }, + ingested_at: "2026-04-05T10:30:00Z", + record_id: "rec_sb_txn_2026_04_05", + record_time: "2026-04-05T10:00:00Z", + stream: "transactions", }, ] as const; @@ -348,171 +348,171 @@ export const DEMO_RECORDS: readonly DemoRecord[] = [ export const DEMO_GRANTS: readonly DemoGrantDef[] = [ { - grant_id: "grant_sb_quill_paystmt", client_id: "client_quill_tax_demo", connector_id: "acme_payroll_demo", - stream: "pay_statements", - fields: ["period_end", "employer", "gross_pay_cents", "net_pay_cents", "currency"], - status: "issued", - trace_id: "trace_sb_quill_paystmt", - first_at: "2026-04-22T16:01:00Z", - last_at: "2026-04-22T16:02:35Z", events: [ { - event_id: "evt_sb_quill_001", - event_type: "request.received", - occurred_at: "2026-04-22T16:01:00Z", - trace_id: "trace_sb_quill_paystmt", client_id: "client_quill_tax_demo", - grant_id: null, - run_id: null, - object_type: "request", - status: "received", data: { purpose_code: "tax_filing", - requested_streams: ["pay_statements"], requested_fields: ["period_end", "employer", "gross_pay_cents", "net_pay_cents", "currency"], + requested_streams: ["pay_statements"], }, + event_id: "evt_sb_quill_001", + event_type: "request.received", + grant_id: null, + object_type: "request", + occurred_at: "2026-04-22T16:01:00Z", + run_id: null, + status: "received", + trace_id: "trace_sb_quill_paystmt", }, { + client_id: "client_quill_tax_demo", + data: { access_mode: "single_use", surface: "owner_browser" }, event_id: "evt_sb_quill_002", event_type: "consent.presented", - occurred_at: "2026-04-22T16:01:42Z", - trace_id: "trace_sb_quill_paystmt", - client_id: "client_quill_tax_demo", grant_id: null, - run_id: null, object_type: "consent", + occurred_at: "2026-04-22T16:01:42Z", + run_id: null, status: "presented", - data: { surface: "owner_browser", access_mode: "single_use" }, + trace_id: "trace_sb_quill_paystmt", }, { + client_id: "client_quill_tax_demo", + data: { access_mode: "single_use", expires_at: "2026-05-22T16:02:00Z" }, event_id: "evt_sb_quill_003", event_type: "grant.issued", - occurred_at: "2026-04-22T16:02:00Z", - trace_id: "trace_sb_quill_paystmt", - client_id: "client_quill_tax_demo", grant_id: "grant_sb_quill_paystmt", - run_id: null, object_type: "grant", + occurred_at: "2026-04-22T16:02:00Z", + run_id: null, status: "issued", - data: { access_mode: "single_use", expires_at: "2026-05-22T16:02:00Z" }, + trace_id: "trace_sb_quill_paystmt", }, { + client_id: "client_quill_tax_demo", + data: { record_count: 3, stream: "pay_statements" }, event_id: "evt_sb_quill_004", event_type: "resource.read.succeeded", - occurred_at: "2026-04-22T16:02:35Z", - trace_id: "trace_sb_quill_paystmt", - client_id: "client_quill_tax_demo", grant_id: "grant_sb_quill_paystmt", - run_id: null, object_type: "resource", + occurred_at: "2026-04-22T16:02:35Z", + run_id: null, status: "succeeded", - data: { stream: "pay_statements", record_count: 3 }, + trace_id: "trace_sb_quill_paystmt", }, ], + fields: ["period_end", "employer", "gross_pay_cents", "net_pay_cents", "currency"], + first_at: "2026-04-22T16:01:00Z", + grant_id: "grant_sb_quill_paystmt", + last_at: "2026-04-22T16:02:35Z", + status: "issued", + stream: "pay_statements", + trace_id: "trace_sb_quill_paystmt", }, { - grant_id: "grant_sb_ledger_txns_revoked", client_id: "client_ledger_atlas_demo", connector_id: "fabrikam_bank_demo", - stream: "transactions", - fields: ["posted_at", "merchant", "amount_cents", "currency", "category"], - status: "revoked", - trace_id: "trace_sb_ledger_txns", - first_at: "2026-04-10T09:00:00Z", - last_at: "2026-04-23T11:45:00Z", events: [ { + client_id: "client_ledger_atlas_demo", + data: { purpose_code: "personal_finance", requested_streams: ["transactions"] }, event_id: "evt_sb_ledger_001", event_type: "request.received", - occurred_at: "2026-04-10T09:00:00Z", - trace_id: "trace_sb_ledger_txns", - client_id: "client_ledger_atlas_demo", grant_id: null, - run_id: null, object_type: "request", + occurred_at: "2026-04-10T09:00:00Z", + run_id: null, status: "received", - data: { purpose_code: "personal_finance", requested_streams: ["transactions"] }, + trace_id: "trace_sb_ledger_txns", }, { + client_id: "client_ledger_atlas_demo", + data: { access_mode: "continuous" }, event_id: "evt_sb_ledger_002", event_type: "grant.issued", - occurred_at: "2026-04-10T09:01:30Z", - trace_id: "trace_sb_ledger_txns", - client_id: "client_ledger_atlas_demo", grant_id: "grant_sb_ledger_txns_revoked", - run_id: null, object_type: "grant", + occurred_at: "2026-04-10T09:01:30Z", + run_id: null, status: "issued", - data: { access_mode: "continuous" }, + trace_id: "trace_sb_ledger_txns", }, { + client_id: "client_ledger_atlas_demo", + data: { reason: "no_longer_needed", revoked_by: "owner" }, event_id: "evt_sb_ledger_003", event_type: "grant.revoked", - occurred_at: "2026-04-23T11:45:00Z", - trace_id: "trace_sb_ledger_txns", - client_id: "client_ledger_atlas_demo", grant_id: "grant_sb_ledger_txns_revoked", - run_id: null, object_type: "grant", + occurred_at: "2026-04-23T11:45:00Z", + run_id: null, status: "revoked", - data: { revoked_by: "owner", reason: "no_longer_needed" }, + trace_id: "trace_sb_ledger_txns", }, { + client_id: "client_ledger_atlas_demo", + data: { error: "grant_revoked", http_status: 403, stream: "transactions" }, event_id: "evt_sb_ledger_004", event_type: "resource.read.refused", - occurred_at: "2026-04-23T12:00:00Z", - trace_id: "trace_sb_ledger_txns", - client_id: "client_ledger_atlas_demo", grant_id: "grant_sb_ledger_txns_revoked", - run_id: null, object_type: "resource", + occurred_at: "2026-04-23T12:00:00Z", + run_id: null, status: "refused", - data: { stream: "transactions", error: "grant_revoked", http_status: 403 }, + trace_id: "trace_sb_ledger_txns", }, ], + fields: ["posted_at", "merchant", "amount_cents", "currency", "category"], + first_at: "2026-04-10T09:00:00Z", + grant_id: "grant_sb_ledger_txns_revoked", + last_at: "2026-04-23T11:45:00Z", + status: "revoked", + stream: "transactions", + trace_id: "trace_sb_ledger_txns", }, { - grant_id: "grant_sb_sherwood_visits_denied", client_id: "client_sherwood_health_demo", connector_id: "northwind_health_demo", - stream: "clinical_visits", - fields: ["visit_at", "provider_name", "summary", "follow_up_needed"], - status: "denied", - trace_id: "trace_sb_sherwood_visits", - first_at: "2026-04-20T18:30:00Z", - last_at: "2026-04-20T18:31:11Z", events: [ { - event_id: "evt_sb_sherwood_001", - event_type: "request.received", - occurred_at: "2026-04-20T18:30:00Z", - trace_id: "trace_sb_sherwood_visits", client_id: "client_sherwood_health_demo", - grant_id: null, - run_id: null, - object_type: "request", - status: "received", data: { purpose_code: "wellness_coaching", - requested_streams: ["clinical_visits"], requested_fields: ["visit_at", "provider_name", "summary", "follow_up_needed"], + requested_streams: ["clinical_visits"], }, + event_id: "evt_sb_sherwood_001", + event_type: "request.received", + grant_id: null, + object_type: "request", + occurred_at: "2026-04-20T18:30:00Z", + run_id: null, + status: "received", + trace_id: "trace_sb_sherwood_visits", }, { + client_id: "client_sherwood_health_demo", + data: { note: "Declined; client unverified.", reason: "owner_declined" }, event_id: "evt_sb_sherwood_002", event_type: "consent.declined", - occurred_at: "2026-04-20T18:31:11Z", - trace_id: "trace_sb_sherwood_visits", - client_id: "client_sherwood_health_demo", grant_id: null, - run_id: null, object_type: "consent", + occurred_at: "2026-04-20T18:31:11Z", + run_id: null, status: "declined", - data: { reason: "owner_declined", note: "Declined; client unverified." }, + trace_id: "trace_sb_sherwood_visits", }, ], + fields: ["visit_at", "provider_name", "summary", "follow_up_needed"], + first_at: "2026-04-20T18:30:00Z", + grant_id: "grant_sb_sherwood_visits_denied", + last_at: "2026-04-20T18:31:11Z", + status: "denied", + stream: "clinical_visits", + trace_id: "trace_sb_sherwood_visits", }, ] as const; @@ -520,130 +520,130 @@ export const DEMO_GRANTS: readonly DemoGrantDef[] = [ export const DEMO_RUNS: readonly DemoRunDef[] = [ { - run_id: "run_sb_acme_2026_04_22", connector_id: "acme_payroll_demo", - grant_id: null, - status: "succeeded", - needs_input: false, - failure_reason: null, - started_at: "2026-04-22T05:00:00Z", - finished_at: "2026-04-22T05:00:42Z", - first_at: "2026-04-22T05:00:00Z", - last_at: "2026-04-22T05:00:42Z", events: [ { + client_id: null, + data: { connector_id: "acme_payroll_demo", reason: "scheduled" }, event_id: "evt_sb_run_acme_001", event_type: "run.started", - occurred_at: "2026-04-22T05:00:00Z", - trace_id: "trace_sb_run_acme", - client_id: null, grant_id: null, - run_id: "run_sb_acme_2026_04_22", object_type: "run", + occurred_at: "2026-04-22T05:00:00Z", + run_id: "run_sb_acme_2026_04_22", status: "started", - data: { connector_id: "acme_payroll_demo", reason: "scheduled" }, + trace_id: "trace_sb_run_acme", }, { + client_id: null, + data: { record_count: 1, stream: "pay_statements" }, event_id: "evt_sb_run_acme_002", event_type: "run.records.synced", - occurred_at: "2026-04-22T05:00:30Z", - trace_id: "trace_sb_run_acme", - client_id: null, grant_id: null, - run_id: "run_sb_acme_2026_04_22", object_type: "run", + occurred_at: "2026-04-22T05:00:30Z", + run_id: "run_sb_acme_2026_04_22", status: "running", - data: { stream: "pay_statements", record_count: 1 }, + trace_id: "trace_sb_run_acme", }, { + client_id: null, + data: { records_total: 4 }, event_id: "evt_sb_run_acme_003", event_type: "run.succeeded", - occurred_at: "2026-04-22T05:00:42Z", - trace_id: "trace_sb_run_acme", - client_id: null, grant_id: null, - run_id: "run_sb_acme_2026_04_22", object_type: "run", + occurred_at: "2026-04-22T05:00:42Z", + run_id: "run_sb_acme_2026_04_22", status: "succeeded", - data: { records_total: 4 }, + trace_id: "trace_sb_run_acme", }, ], + failure_reason: null, + finished_at: "2026-04-22T05:00:42Z", + first_at: "2026-04-22T05:00:00Z", + grant_id: null, + last_at: "2026-04-22T05:00:42Z", + needs_input: false, + run_id: "run_sb_acme_2026_04_22", + started_at: "2026-04-22T05:00:00Z", + status: "succeeded", }, { - run_id: "run_sb_northwind_2026_04_24_failed", connector_id: "northwind_health_demo", - grant_id: null, - status: "failed", - needs_input: false, - failure_reason: "captcha_challenge_unresolved", - started_at: "2026-04-24T03:15:00Z", - finished_at: "2026-04-24T03:16:11Z", - first_at: "2026-04-24T03:15:00Z", - last_at: "2026-04-24T03:16:11Z", events: [ { + client_id: null, + data: { connector_id: "northwind_health_demo", reason: "manual" }, event_id: "evt_sb_run_nw_001", event_type: "run.started", - occurred_at: "2026-04-24T03:15:00Z", - trace_id: "trace_sb_run_northwind", - client_id: null, grant_id: null, - run_id: "run_sb_northwind_2026_04_24_failed", object_type: "run", + occurred_at: "2026-04-24T03:15:00Z", + run_id: "run_sb_northwind_2026_04_24_failed", status: "started", - data: { connector_id: "northwind_health_demo", reason: "manual" }, + trace_id: "trace_sb_run_northwind", }, { + client_id: null, + data: { reason: "captcha_challenge_unresolved", retryable: true }, event_id: "evt_sb_run_nw_002", event_type: "run.failed", - occurred_at: "2026-04-24T03:16:11Z", - trace_id: "trace_sb_run_northwind", - client_id: null, grant_id: null, - run_id: "run_sb_northwind_2026_04_24_failed", object_type: "run", + occurred_at: "2026-04-24T03:16:11Z", + run_id: "run_sb_northwind_2026_04_24_failed", status: "failed", - data: { reason: "captcha_challenge_unresolved", retryable: true }, + trace_id: "trace_sb_run_northwind", }, ], + failure_reason: "captcha_challenge_unresolved", + finished_at: "2026-04-24T03:16:11Z", + first_at: "2026-04-24T03:15:00Z", + grant_id: null, + last_at: "2026-04-24T03:16:11Z", + needs_input: false, + run_id: "run_sb_northwind_2026_04_24_failed", + started_at: "2026-04-24T03:15:00Z", + status: "failed", }, { - run_id: "run_sb_fabrikam_2026_04_25_needs_input", connector_id: "fabrikam_bank_demo", - grant_id: null, - status: "needs_input", - needs_input: true, - failure_reason: null, - started_at: "2026-04-25T08:00:00Z", - finished_at: null, - first_at: "2026-04-25T08:00:00Z", - last_at: "2026-04-25T08:00:21Z", events: [ { + client_id: null, + data: { connector_id: "fabrikam_bank_demo", reason: "scheduled" }, event_id: "evt_sb_run_fb_001", event_type: "run.started", - occurred_at: "2026-04-25T08:00:00Z", - trace_id: "trace_sb_run_fabrikam", - client_id: null, grant_id: null, - run_id: "run_sb_fabrikam_2026_04_25_needs_input", object_type: "run", + occurred_at: "2026-04-25T08:00:00Z", + run_id: "run_sb_fabrikam_2026_04_25_needs_input", status: "started", - data: { connector_id: "fabrikam_bank_demo", reason: "scheduled" }, + trace_id: "trace_sb_run_fabrikam", }, { + client_id: null, + data: { kind: "owner_2fa", message: "Owner must approve from a trusted device." }, event_id: "evt_sb_run_fb_002", event_type: "run.needs_input", - occurred_at: "2026-04-25T08:00:21Z", - trace_id: "trace_sb_run_fabrikam", - client_id: null, grant_id: null, - run_id: "run_sb_fabrikam_2026_04_25_needs_input", object_type: "run", + occurred_at: "2026-04-25T08:00:21Z", + run_id: "run_sb_fabrikam_2026_04_25_needs_input", status: "needs_input", - data: { kind: "owner_2fa", message: "Owner must approve from a trusted device." }, + trace_id: "trace_sb_run_fabrikam", }, ], + failure_reason: null, + finished_at: null, + first_at: "2026-04-25T08:00:00Z", + grant_id: null, + last_at: "2026-04-25T08:00:21Z", + needs_input: true, + run_id: "run_sb_fabrikam_2026_04_25_needs_input", + started_at: "2026-04-25T08:00:00Z", + status: "needs_input", }, ] as const; @@ -652,52 +652,52 @@ export const DEMO_RUNS: readonly DemoRunDef[] = [ /** Traces are a denormalized view: per-trace summaries assembled from grant + run events. */ export const DEMO_TRACES: readonly DemoTraceDef[] = [ { - trace_id: "trace_sb_quill_paystmt", client_id: "client_quill_tax_demo", - grant_id: "grant_sb_quill_paystmt", - run_id: null, - status: "succeeded", + events: [], failure_reason: null, - kinds: ["request.received", "consent.presented", "grant.issued", "resource.read.succeeded"], first_at: "2026-04-22T16:01:00Z", + grant_id: "grant_sb_quill_paystmt", + kinds: ["request.received", "consent.presented", "grant.issued", "resource.read.succeeded"], last_at: "2026-04-22T16:02:35Z", - events: [], + run_id: null, + status: "succeeded", + trace_id: "trace_sb_quill_paystmt", }, { - trace_id: "trace_sb_ledger_txns", client_id: "client_ledger_atlas_demo", - grant_id: "grant_sb_ledger_txns_revoked", - run_id: null, - status: "revoked", + events: [], failure_reason: "grant_revoked", - kinds: ["request.received", "grant.issued", "grant.revoked", "resource.read.refused"], first_at: "2026-04-10T09:00:00Z", + grant_id: "grant_sb_ledger_txns_revoked", + kinds: ["request.received", "grant.issued", "grant.revoked", "resource.read.refused"], last_at: "2026-04-23T12:00:00Z", - events: [], + run_id: null, + status: "revoked", + trace_id: "trace_sb_ledger_txns", }, { - trace_id: "trace_sb_sherwood_visits", client_id: "client_sherwood_health_demo", - grant_id: null, - run_id: null, - status: "denied", + events: [], failure_reason: "owner_declined", - kinds: ["request.received", "consent.declined"], first_at: "2026-04-20T18:30:00Z", + grant_id: null, + kinds: ["request.received", "consent.declined"], last_at: "2026-04-20T18:31:11Z", - events: [], + run_id: null, + status: "denied", + trace_id: "trace_sb_sherwood_visits", }, { - trace_id: "trace_sb_run_northwind", client_id: null, - grant_id: null, - run_id: "run_sb_northwind_2026_04_24_failed", - status: "failed", + events: [], failure_reason: "captcha_challenge_unresolved", - kinds: ["run.started", "run.failed"], first_at: "2026-04-24T03:15:00Z", + grant_id: null, + kinds: ["run.started", "run.failed"], last_at: "2026-04-24T03:16:11Z", - events: [], + run_id: "run_sb_northwind_2026_04_24_failed", + status: "failed", + trace_id: "trace_sb_run_northwind", }, ] as const; @@ -706,58 +706,58 @@ export const DEMO_TRACES: readonly DemoTraceDef[] = [ export const DEMO_CAPABILITIES: readonly DemoCapabilityDef[] = [ { capability: "scoped_grant_issuance", + demonstrated_in_demo: true, description: "Owner approves a request restricted to specific streams and fields.", implemented: true, - demonstrated_in_demo: true, notes: "See grant_sb_quill_paystmt and the /sandbox/walkthrough story.", }, { capability: "grant_revocation", + demonstrated_in_demo: true, description: "Owner can revoke an outstanding grant; subsequent reads are refused.", implemented: true, - demonstrated_in_demo: true, notes: "See grant_sb_ledger_txns_revoked.", }, { capability: "consent_decline", + demonstrated_in_demo: true, description: "Owner can decline a request without minting a grant.", implemented: true, - demonstrated_in_demo: true, notes: "See grant_sb_sherwood_visits_denied.", }, { capability: "stream_schema_discovery", + demonstrated_in_demo: true, description: "Clients enumerate available streams and field schemas before requesting.", implemented: true, - demonstrated_in_demo: true, notes: "GET /sandbox/v1/schema and /sandbox/v1/streams.", }, { capability: "lexical_search", + demonstrated_in_demo: true, description: "Operator can search retained records by free-text keywords.", implemented: true, - demonstrated_in_demo: true, notes: "GET /sandbox/v1/search.", }, { capability: "single_use_access", + demonstrated_in_demo: false, description: "A grant can be marked single-use so it cannot be replayed.", implemented: true, - demonstrated_in_demo: false, notes: "Single-use semantics are visible in walkthrough JSON; not yet enforced by sandbox API.", }, { capability: "token_introspection", + demonstrated_in_demo: false, description: "Authorization server exposes RFC 7662-style introspection.", implemented: true, - demonstrated_in_demo: false, notes: "Introspection is a normative reference feature; sandbox documents it but does not stub it.", }, { capability: "semantic_search", + demonstrated_in_demo: false, description: "Vector-backed semantic search over retained records.", implemented: true, - demonstrated_in_demo: false, notes: "Sandbox uses lexical search only; semantic backend requires a model cache.", }, ]; diff --git a/apps/site/src/app/sandbox/_demo/operations-fixtures.ts b/apps/site/src/app/sandbox/_demo/operations-fixtures.ts index d85b34b74..7fa83651e 100644 --- a/apps/site/src/app/sandbox/_demo/operations-fixtures.ts +++ b/apps/site/src/app/sandbox/_demo/operations-fixtures.ts @@ -88,7 +88,7 @@ import type { DemoGrantDef, DemoRecord, DemoRunDef, DemoTimelineEvent, DemoTrace const SANDBOX_AGGREGATE_SOURCE_ID = "sandbox_demo"; function connectorSource(connectorId: string): { kind: "connector"; id: string } { - return { kind: "connector", id: connectorId }; + return { id: connectorId, kind: "connector" }; } function connectorIdForStream(streamName: string): string { @@ -129,10 +129,10 @@ export function createSandboxStreamsListDependencies( const summaries: StreamSummary[] = filtered.map((stream) => { const lastUpdated = latestRecordTimeForStream(stream.key) ?? stream.latest_record_time; return { - object: "stream", + last_updated: lastUpdated, name: stream.key, + object: "stream", record_count: streamRecordCount(stream.key), - last_updated: lastUpdated, }; }); const sourceDescriptor: StreamsListSourceDescriptor = options.connectorId @@ -140,8 +140,8 @@ export function createSandboxStreamsListDependencies( : connectorSource(SANDBOX_AGGREGATE_SOURCE_ID); return { - listSummaries: () => Promise.resolve(summaries), getSourceDescriptor: () => sourceDescriptor, + listSummaries: () => Promise.resolve(summaries), }; } @@ -161,13 +161,6 @@ export function createSandboxStreamDetailDependencies(streamName: string): Strea const sourceDescriptor: StreamDetailSourceDescriptor = connectorSource(connectorIdForStream(streamName)); return { - getSourceDescriptor: () => sourceDescriptor, - hasManifestStream: (name: string) => Promise.resolve(streamByKey.has(name)), - // Sandbox routes always run as owner; this dependency is unreachable from - // the sandbox host but the operation requires it on the type. Returning - // `true` matches owner-equivalent visibility so any future client-actor - // mounting of this fixture profile would behave like the demo schema. - isStreamInGrant: () => true, buildStreamMetadata: (name: string) => { const stream = streamByKey.get(name); if (!stream) { @@ -177,11 +170,18 @@ export function createSandboxStreamDetailDependencies(streamName: string): Strea } const metadata: StreamMetadataEnvelope = { ...buildLiveStreamMetadata(stream), - object: "stream_metadata", name: stream.key, + object: "stream_metadata", }; return Promise.resolve(metadata); }, + getSourceDescriptor: () => sourceDescriptor, + hasManifestStream: (name: string) => Promise.resolve(streamByKey.has(name)), + // Sandbox routes always run as owner; this dependency is unreachable from + // the sandbox host but the operation requires it on the type. Returning + // `true` matches owner-equivalent visibility so any future client-actor + // mounting of this fixture profile would behave like the demo schema. + isStreamInGrant: () => true, }; } @@ -203,14 +203,14 @@ export function createSandboxSchemaGetDependencies(): SchemaGetDependencies { const connectors: ConnectorSchemaItem[] = DEMO_CONNECTORS.map((connector) => { const streams = DEMO_STREAMS.filter((s) => s.connector_id === connector.connector_id); return { + connector_id: connector.connector_id, object: "connector", source: connectorSource(connector.connector_id), - connector_id: connector.connector_id, stream_count: streams.length, streams: streams.map((stream) => ({ ...buildLiveStreamMetadata(stream), - object: "stream_metadata", name: stream.key, + object: "stream_metadata", })), }; }); @@ -283,11 +283,11 @@ interface SandboxLiveRecord { function recordToLiveRecord(record: DemoRecord): SandboxLiveRecord { return { - object: "record", - id: record.record_id, - stream: record.stream, data: { ...record.fields }, emitted_at: record.ingested_at, + id: record.record_id, + object: "record", + stream: record.stream, }; } @@ -325,9 +325,10 @@ export function createSandboxRecordsListDependencies( : connectorSource(connectorIdForStream(options.streamName ?? "")); return { - getSourceDescriptor: () => sourceDescriptor, - getManifest: () => sandboxManifest(), + decorateRecord: (record) => record, getGrant: () => sandboxOwnerGrantPlaceholder(), + getManifest: () => sandboxManifest(), + getSourceDescriptor: () => sourceDescriptor, queryRecords: (stream, _grant, params) => { const matching = DEMO_RECORDS.filter((record) => { if (record.stream !== stream) { @@ -345,16 +346,15 @@ export function createSandboxRecordsListDependencies( const next = start + limit; const hasMore = next < sorted.length; const result: RecordsListQueryResult = { - object: "list", - has_more: hasMore, data: slice.map((record) => recordToLiveRecord(record) as unknown as Record), + has_more: hasMore, + object: "list", }; if (hasMore) { result.next_cursor = encodeSandboxCursor(next); } return Promise.resolve(result); }, - decorateRecord: (record) => record, // Sandbox demo records have no field/filter validation yet; the route // never previously enforced manifest field/filter shape, so this is a // no-op fixture validator. @@ -368,13 +368,13 @@ export function createSandboxRecordsListDependencies( export function createSandboxRecordDetailDependencies(streamName: string): RecordDetailDependencies { const sourceDescriptor: RecordDetailSourceDescriptor = connectorSource(connectorIdForStream(streamName)); return { - getSourceDescriptor: () => sourceDescriptor, - getManifest: (): RecordDetailManifest => ({ - streams: DEMO_STREAMS.map((stream) => ({ name: stream.key })), - }), + decorateRecord: (record) => record, // Sandbox is owner-shaped; the operation's owner branch overwrites this // with an owner read-grant before any capability call. getGrant: (): RecordDetailGrant => ({ streams: [] }), + getManifest: (): RecordDetailManifest => ({ + streams: DEMO_STREAMS.map((stream) => ({ name: stream.key })), + }), getRecord: (stream, recordId) => { const record = DEMO_RECORDS.find((r) => r.stream === stream && r.record_id === recordId); if (!record) { @@ -382,7 +382,7 @@ export function createSandboxRecordDetailDependencies(streamName: string): Recor } return Promise.resolve(recordToLiveRecord(record) as unknown as Record); }, - decorateRecord: (record) => record, + getSourceDescriptor: () => sourceDescriptor, // Sandbox demo records have no field validation yet; detail mirrors list // so the shared operation contract stays wired without changing demo data. validateRequestFields: () => undefined, @@ -432,17 +432,17 @@ const SANDBOX_SEARCH_DEFAULT_LIMIT = 25; const SANDBOX_SEARCH_MAX_LIMIT = 100; const SANDBOX_LEXICAL_ADVERTISEMENT: SearchLexicalAdvertisement = { - supported: true, cross_stream: true, - snippets: true, default_limit: SANDBOX_SEARCH_DEFAULT_LIMIT, max_limit: SANDBOX_SEARCH_MAX_LIMIT, score: { - supported: true, kind: "bm25", order: "lower_is_better", + supported: true, value_semantics: "implementation_relative", }, + snippets: true, + supported: true, }; function escapeRegexLiteral(value: string): string { @@ -493,7 +493,7 @@ interface SandboxCompiledFilters { */ function compileSandboxFilterForStream(filter: unknown, streamName: string): SandboxCompiledFilters { if (filter == null) { - return { streamName, exact: [] }; + return { exact: [], streamName }; } if (typeof filter !== "object" || Array.isArray(filter)) { throw new SearchLexicalRequestError( @@ -535,7 +535,7 @@ function compileSandboxFilterForStream(filter: unknown, streamName: string): San } exact.push({ field: fieldName, value: String(rawValue) }); } - return { streamName, exact }; + return { exact, streamName }; } /** @@ -582,12 +582,12 @@ function matchSandboxRecord(record: DemoRecord, trimmed: string, lower: string): const score = 1 / (1 + matchedFields.length + bestOccurrences); return { connectorId: record.connector_id, - stream: record.stream, - recordKey: record.record_id, emittedAt: record.ingested_at, matchedFields, - snippet: { field: bestField, text: bestSnippet }, + recordKey: record.record_id, score, + snippet: { field: bestField, text: bestSnippet }, + stream: record.stream, }; } @@ -611,25 +611,9 @@ function generateSandboxSnapshotId(): string { export function createSandboxSearchLexicalDependencies(): SearchLexicalDependencies { const snapshotCache = new Map(); return { - getAdvertisement: () => SANDBOX_LEXICAL_ADVERTISEMENT, - listOwnerVisibleConnectorIds: () => DEMO_CONNECTORS.map((c) => c.connector_id), - resolveOwnerManifestForConnector: (connectorId: string): SearchLexicalManifest | null => { - const streams = DEMO_STREAMS.filter((s) => s.connector_id === connectorId).map((s) => ({ name: s.key })); - if (streams.length === 0) { - return null; - } - return { connector_id: connectorId, streams }; - }, buildOwnerReadGrantForManifest: (manifest: SearchLexicalManifest): SearchLexicalGrant => ({ streams: (manifest.streams ?? []).map((s) => ({ name: s.name })), }), - // Sandbox routes do not currently mount client-actor flows; the - // operation's owner branch is the only consumer. This stub is here so - // the dependency type is satisfied and future client-actor wiring stays - // mechanical. - resolveClientManifest: ({ grant }) => ({ - streams: (grant.streams ?? []).map((s) => ({ name: s.name })), - }), buildSearchPlanForGrant: ({ manifest, grant, streamsFilter, filter, filteredStream, connectorId }) => { const grantedStreams = new Set((grant.streams ?? []).map((s) => s.name)); // Compile the request filter once. The operation guarantees that @@ -654,17 +638,17 @@ export function createSandboxSearchLexicalDependencies(): SearchLexicalDependenc continue; } plan.push({ - streamName: stream.name, + // The fixture-only `compiledFilter` field rides on the plan + // entry so `buildSnapshot` can evaluate filters per-stream + // without re-parsing the request payload. + compiledFilter: compiledFilter && compiledFilter.streamName === stream.name ? compiledFilter : null, + connectorId: connectorId ?? null, // Sandbox records are scanned through every string field; the // declared search-field list is not surfaced in fixtures yet, so // the operation receives a sentinel non-empty list to keep the // plan-emptiness check meaningful. searchableFields: ["__sandbox_any_string_field__"], - connectorId: connectorId ?? null, - // The fixture-only `compiledFilter` field rides on the plan - // entry so `buildSnapshot` can evaluate filters per-stream - // without re-parsing the request payload. - compiledFilter: compiledFilter && compiledFilter.streamName === stream.name ? compiledFilter : null, + streamName: stream.name, }); } return plan; @@ -712,28 +696,44 @@ export function createSandboxSearchLexicalDependencies(): SearchLexicalDependenc // exact. This mirrors the SLVP-ideal global top-k path and gives the // public sandbox surface an exhaustive, exact-counted envelope. return { - snapshot_id: generateSandboxSnapshotId(), query: q, - results: hits, recall_meta: { count: hits.length, count_accuracy: "exact", recall: { complete: true, - ranking_scope: "all_matches", - truncated: false, ranked_candidate_count: hits.length, + ranking_scope: "all_matches", sources_searched_count: perConnectorPlans.length, + truncated: false, }, }, + results: hits, + snapshot_id: generateSandboxSnapshotId(), }; }, + formatRecordUrl: ({ stream, recordKey }) => + `/sandbox/v1/streams/${encodeURIComponent(stream)}/records/${encodeURIComponent(recordKey)}`, + getAdvertisement: () => SANDBOX_LEXICAL_ADVERTISEMENT, + listOwnerVisibleConnectorIds: () => DEMO_CONNECTORS.map((c) => c.connector_id), + loadSnapshot: (snapshotId) => snapshotCache.get(snapshotId) ?? null, persistSnapshot: (snapshot) => { snapshotCache.set(snapshot.snapshot_id, snapshot); }, - loadSnapshot: (snapshotId) => snapshotCache.get(snapshotId) ?? null, - formatRecordUrl: ({ stream, recordKey }) => - `/sandbox/v1/streams/${encodeURIComponent(stream)}/records/${encodeURIComponent(recordKey)}`, + // Sandbox routes do not currently mount client-actor flows; the + // operation's owner branch is the only consumer. This stub is here so + // the dependency type is satisfied and future client-actor wiring stays + // mechanical. + resolveClientManifest: ({ grant }) => ({ + streams: (grant.streams ?? []).map((s) => ({ name: s.name })), + }), + resolveOwnerManifestForConnector: (connectorId: string): SearchLexicalManifest | null => { + const streams = DEMO_STREAMS.filter((s) => s.connector_id === connectorId).map((s) => ({ name: s.key })); + if (streams.length === 0) { + return null; + } + return { connector_id: connectorId, streams }; + }, }; } @@ -765,17 +765,17 @@ export function createSandboxSearchLexicalDependencies(): SearchLexicalDependenc function sandboxDatasetCounts() { return { connector_count: DEMO_CONNECTORS.length, - stream_count: DEMO_STREAMS.length, record_count: DEMO_RECORDS.length, + stream_count: DEMO_STREAMS.length, }; } function sandboxDatasetRetainedBytes() { const recordJsonBytes = DEMO_RECORDS.reduce((sum, r) => sum + JSON.stringify(r.fields).length, 0); return { - record_json_bytes: recordJsonBytes, - record_changes_json_bytes: 0, blob_bytes: 0, + record_changes_json_bytes: 0, + record_json_bytes: recordJsonBytes, }; } @@ -813,9 +813,9 @@ function sandboxDatasetTopConnectorCandidates(): Array<{ export function createSandboxRefDatasetSummaryDependencies(): RefDatasetSummaryDependencies { return { getCounts: () => sandboxDatasetCounts(), - getRetainedBytes: () => sandboxDatasetRetainedBytes(), - getRecordTimeBounds: () => sandboxDatasetTimeBounds(DEMO_RECORDS.map((r) => r.record_time)), getIngestedTimeBounds: () => sandboxDatasetTimeBounds(DEMO_RECORDS.map((r) => r.ingested_at)), + getRecordTimeBounds: () => sandboxDatasetTimeBounds(DEMO_RECORDS.map((r) => r.record_time)), + getRetainedBytes: () => sandboxDatasetRetainedBytes(), listTopConnectorCandidates: () => sandboxDatasetTopConnectorCandidates(), }; } @@ -846,9 +846,9 @@ function paginateSandboxSpineSummaries( const slice = summaries.slice(start, start + limit); const next = start + limit; return { - summaries: slice, hasMore: next < summaries.length, nextCursor: next < summaries.length ? encodeSandboxCursor(next) : null, + summaries: slice, }; } @@ -884,47 +884,47 @@ function connectorIdForTrace(trace: DemoTraceDef): string | null { function demoGrantToSpineSummary(grant: DemoGrantDef): RefSpineCorrelationSummary { return { - id: grant.grant_id, - first_at: grant.first_at, - last_at: grant.last_at, + actor_id: grant.client_id, + actor_type: "client", + client_id: grant.client_id, + connector_id: grant.connector_id, event_count: grant.events.length, - status: grant.status, + failure: demoGrantFailure(grant), + first_at: grant.first_at, + grant_id: grant.grant_id, + id: grant.grant_id, kinds: grant.events.map((event) => event.event_type), + last_at: grant.last_at, + needs_input: false, request_id: null, - grant_id: grant.grant_id, run_id: null, - client_id: grant.client_id, - connector_id: grant.connector_id, source: connectorSource(grant.connector_id), source_id: grant.connector_id, source_kind: "connector", - actor_type: "client", - actor_id: grant.client_id, - failure: demoGrantFailure(grant), - needs_input: false, + status: grant.status, }; } function demoRunToSpineSummary(run: DemoRunDef): RefSpineCorrelationSummary { return { - id: run.run_id, - first_at: run.first_at, - last_at: run.last_at, + actor_id: run.connector_id, + actor_type: "runtime", + client_id: null, + connector_id: run.connector_id, event_count: run.events.length, - status: run.status, + failure: run.failure_reason ? { event_type: "run.failed", reason: run.failure_reason } : null, + first_at: run.first_at, + grant_id: run.grant_id, + id: run.run_id, kinds: run.events.map((event) => event.event_type), + last_at: run.last_at, + needs_input: run.needs_input, request_id: null, - grant_id: run.grant_id, run_id: run.run_id, - client_id: null, - connector_id: run.connector_id, source: connectorSource(run.connector_id), source_id: run.connector_id, source_kind: "connector", - actor_type: "runtime", - actor_id: run.connector_id, - failure: run.failure_reason ? { event_type: "run.failed", reason: run.failure_reason } : null, - needs_input: run.needs_input, + status: run.status, }; } @@ -937,24 +937,24 @@ function demoTraceToSpineSummary(trace: DemoTraceDef): RefSpineCorrelationSummar actorType = "runtime"; } return { - id: trace.trace_id, - first_at: trace.first_at, - last_at: trace.last_at, + actor_id: trace.client_id ?? trace.run_id ?? "sandbox", + actor_type: actorType, + client_id: trace.client_id, + connector_id: connectorId, event_count: trace.kinds.length, - status: trace.status, + failure: demoTraceFailure(trace), + first_at: trace.first_at, + grant_id: trace.grant_id, + id: trace.trace_id, kinds: [...trace.kinds], + last_at: trace.last_at, + needs_input: false, request_id: null, - grant_id: trace.grant_id, run_id: trace.run_id, - client_id: trace.client_id, - connector_id: connectorId, source: connectorId ? connectorSource(connectorId) : null, source_id: connectorId, source_kind: connectorId ? "connector" : null, - actor_type: actorType, - actor_id: trace.client_id ?? trace.run_id ?? "sandbox", - failure: demoTraceFailure(trace), - needs_input: false, + status: trace.status, }; } @@ -1051,10 +1051,10 @@ function sandboxEventsFor(kind: RefSpineEventsKind, id: string): DemoTimelineEve function demoEventToRefSpineInput(event: DemoTimelineEvent): RefSpineEventInput { return { ...event, - object_type: event.object_type ?? "event", + data: { ...event.data }, object_id: event.event_id, + object_type: event.object_type ?? "event", trace_id: event.trace_id, - data: { ...event.data }, }; } @@ -1076,14 +1076,14 @@ export function createSandboxRefSpineEventsPageInput( const next = start + limit; const hasMore = next < events.length; return { - kind, - id, cursor, + id, + kind, page: { events: slice.map(demoEventToRefSpineInput), - truncated: hasMore, - next_cursor: hasMore ? encodeSandboxCursor(next) : null, limit, + next_cursor: hasMore ? encodeSandboxCursor(next) : null, + truncated: hasMore, }, }; } @@ -1118,8 +1118,8 @@ function buildSandboxAuthorizationServerMetadataDocument({ tokenEndpointAuthMethodsSupported, }: AsAuthorizationServerMetadataBuilderInput): SandboxAuthorizationServerMetadata { const metadata: SandboxAuthorizationServerMetadata = { - issuer, introspection_endpoint: introspectionEndpoint, + issuer, pdpp_provider_connect_capabilities: providerConnectCapabilities, }; if (pushedAuthorizationRequestEndpoint) { @@ -1157,7 +1157,7 @@ export function createSandboxAsAuthorizationServerMetadataDependencies(): AsAuth export function buildSandboxAuthorizationServerMetadata(issuer: string): unknown { return executeAsAuthorizationServerMetadata( - { issuer, dynamicClientRegistrationEnabled: false }, + { dynamicClientRegistrationEnabled: false, issuer }, createSandboxAsAuthorizationServerMetadataDependencies() ); } @@ -1202,23 +1202,23 @@ function buildSandboxAgentDiscovery(issuer: string): SandboxProtectedResourceMet const cli = getPdppCliPackageInfo(issuer); return { advisory: true, - skill_name: "pdpp-data-access", - recommended_flow: "pdpp connect", cli: { - package: cli.packageName, - package_specifier: cli.packageSpecifier, bin_name: cli.binName, - install_command: `npx -y ${cli.packageSpecifier} --help`, - run_command: cli.runCommand, connect_command: createPdppCliCommand(""), - version_policy: cli.versionPolicy, + install_command: `npx -y ${cli.packageSpecifier} --help`, no_owner_token: cli.noOwnerToken, no_owner_token_policy: cli.noOwnerTokenPolicy, + package: cli.packageName, + package_specifier: cli.packageSpecifier, + run_command: cli.runCommand, + version_policy: cli.versionPolicy, }, - skill_catalog: `${siteOrigin}/.well-known/skills/index.json`, - skill: `${siteOrigin}/.well-known/skills/pdpp-data-access/SKILL.md`, - llms_txt: `${siteOrigin}/llms.txt`, llms_full_txt: `${siteOrigin}/llms-full.txt`, + llms_txt: `${siteOrigin}/llms.txt`, + recommended_flow: "pdpp connect", + skill: `${siteOrigin}/.well-known/skills/pdpp-data-access/SKILL.md`, + skill_catalog: `${siteOrigin}/.well-known/skills/index.json`, + skill_name: "pdpp-data-access", }; } @@ -1226,27 +1226,27 @@ export function createSandboxRsProtectedResourceMetadataDependencies( issuer: string ): RsProtectedResourceMetadataDependencies { const lexical: RsProtectedResourceMetadataLexicalCapability = { - supported: true, - endpoint: `${issuer}/v1/search`, cross_stream: true, - snippets: true, default_limit: SANDBOX_SEARCH_DEFAULT_LIMIT, + endpoint: `${issuer}/v1/search`, max_limit: SANDBOX_SEARCH_MAX_LIMIT, score: { - supported: true, kind: "bm25", order: "lower_is_better", + supported: true, value_semantics: "implementation_relative", }, + snippets: true, + supported: true, }; return { - resolveLexicalCapability: () => lexical, - resolveSemanticCapability: () => null, - resolveHybridCapabilityOverride: () => null, buildDefaultHybridCapability: (): RsProtectedResourceMetadataHybridCapability | null => null, isHybridSuppressed: () => false, isNativeSingleSourceMode: () => false, resolveClientEventSubscriptionsCapability: () => null, + resolveHybridCapabilityOverride: () => null, + resolveLexicalCapability: () => lexical, + resolveSemanticCapability: () => null, }; } @@ -1255,16 +1255,16 @@ export function buildSandboxProtectedResourceMetadataDocument( composition: RsProtectedResourceMetadataComposition ): SandboxProtectedResourceMetadata { const metadata: SandboxProtectedResourceMetadata = { - resource: issuer, - resource_name: "Sandbox demo Resource Server", authorization_servers: [issuer], bearer_methods_supported: ["header"], + pdpp_agent_discovery: buildSandboxAgentDiscovery(issuer), + pdpp_core_query_base: `${issuer}/v1`, + pdpp_discovery_hints: composition.discoveryHints, pdpp_provider_connect_version: SANDBOX_PROVIDER_CONNECT_VERSION, pdpp_self_export_supported: true, pdpp_token_kinds_supported: ["owner", "client"], - pdpp_core_query_base: `${issuer}/v1`, - pdpp_discovery_hints: composition.discoveryHints, - pdpp_agent_discovery: buildSandboxAgentDiscovery(issuer), + resource: issuer, + resource_name: "Sandbox demo Resource Server", }; if (Object.keys(composition.capabilities).length > 0) { metadata.capabilities = composition.capabilities as Record; diff --git a/apps/site/src/app/sandbox/_demo/route-operation-parity.test.ts b/apps/site/src/app/sandbox/_demo/route-operation-parity.test.ts index 4f662559c..139408a2e 100644 --- a/apps/site/src/app/sandbox/_demo/route-operation-parity.test.ts +++ b/apps/site/src/app/sandbox/_demo/route-operation-parity.test.ts @@ -85,11 +85,11 @@ async function collectRouteSources(dir = SANDBOX_DIR): Promise { } const src = await readFile(full, "utf8"); routes.push({ - rel: relative(SANDBOX_DIR, full).split(sep).join("/"), - src, operations: [...src.matchAll(OPERATIONS_IMPORT_RE)] .map((match) => match[1]) .filter((operation): operation is string => typeof operation === "string"), + rel: relative(SANDBOX_DIR, full).split(sep).join("/"), + src, }); } return routes.sort((a, b) => a.rel.localeCompare(b.rel)); diff --git a/apps/site/src/app/sandbox/_demo/routes.test.ts b/apps/site/src/app/sandbox/_demo/routes.test.ts index aa65683d0..315609921 100644 --- a/apps/site/src/app/sandbox/_demo/routes.test.ts +++ b/apps/site/src/app/sandbox/_demo/routes.test.ts @@ -196,7 +196,7 @@ test("/sandbox/v1/streams/:s/records returns 404 for unknown stream", async () = test("/sandbox/v1/streams/:s/records/:id returns a live `record` envelope", async () => { const res = await recordDetailGet( new Request("https://example.invalid/sandbox/v1/streams/pay_statements/records/rec_sb_paystmt_2026_03"), - { params: Promise.resolve({ stream: "pay_statements", recordId: "rec_sb_paystmt_2026_03" }) } + { params: Promise.resolve({ recordId: "rec_sb_paystmt_2026_03", stream: "pay_statements" }) } ); assert.equal(res.status, 200); const body = (await jsonOf(res)) as Record; @@ -564,15 +564,15 @@ test("/sandbox/.well-known/oauth-protected-resource returns the live RS metadata assert.equal(agentDiscovery.llms_txt, "https://example.invalid/llms.txt"); assert.equal(agentDiscovery.llms_full_txt, "https://example.invalid/llms-full.txt"); assert.deepEqual(agentDiscovery.cli, { - package: "@pdpp/cli", - package_specifier: "@pdpp/cli", bin_name: "pdpp", - install_command: "npx -y @pdpp/cli --help", - run_command: "npx -y @pdpp/cli connect https://example.invalid/sandbox", connect_command: "npx -y @pdpp/cli connect ", - version_policy: "latest", + install_command: "npx -y @pdpp/cli --help", no_owner_token: true, no_owner_token_policy: "owner_browser_approval_required", + package: "@pdpp/cli", + package_specifier: "@pdpp/cli", + run_command: "npx -y @pdpp/cli connect https://example.invalid/sandbox", + version_policy: "latest", }); // Lexical retrieval must be advertised because the route is implemented. const caps = body.capabilities as { lexical_retrieval?: { supported?: boolean; endpoint?: string } } | undefined; @@ -587,9 +587,9 @@ test("sandbox metadata prefers X-Forwarded-Host/Proto over the request URL", asy const res = authServerGet( new Request("http://0.0.0.0:3010/sandbox/.well-known/oauth-authorization-server", { headers: { + host: "0.0.0.0:3010", "x-forwarded-host": "pdpp.example.com", "x-forwarded-proto": "https", - host: "0.0.0.0:3010", }, }) ); diff --git a/apps/site/src/app/sandbox/api-examples/page.tsx b/apps/site/src/app/sandbox/api-examples/page.tsx index 24d849697..164dc12ed 100644 --- a/apps/site/src/app/sandbox/api-examples/page.tsx +++ b/apps/site/src/app/sandbox/api-examples/page.tsx @@ -19,124 +19,124 @@ interface Example { const EXAMPLES: readonly Example[] = [ { - surface: "v1", - title: "Schema", - method: "GET", + description: + "Discover connectors, streams, and field schemas. Same shape as /v1/schema on a real reference server.", endpoint: "/sandbox/v1/schema", + method: "GET", request: "curl -s /sandbox/v1/schema", responseHint: "Live `schema` envelope: object='schema', bearer, connectors[].streams[] with stream_metadata.", - description: - "Discover connectors, streams, and field schemas. Same shape as /v1/schema on a real reference server.", + surface: "v1", + title: "Schema", }, { - surface: "v1", - title: "List streams", - method: "GET", + description: "Supports cursor pagination via `limit` and `cursor`. Optional `connector_id` filter.", endpoint: "/sandbox/v1/streams", + method: "GET", request: "curl -s '/sandbox/v1/streams?limit=10'", responseHint: "Live `list` envelope of `stream` rows: { name, record_count, last_updated, freshness }.", - description: "Supports cursor pagination via `limit` and `cursor`. Optional `connector_id` filter.", + surface: "v1", + title: "List streams", }, { - surface: "v1", - title: "Stream metadata", - method: "GET", + description: "Replace the path segment with any stream name from /sandbox/v1/schema.", endpoint: "/sandbox/v1/streams/pay_statements", + method: "GET", request: "curl -s /sandbox/v1/streams/pay_statements", responseHint: "Live `stream_metadata` envelope with schema, primary_key, cursor_field, freshness.", - description: "Replace the path segment with any stream name from /sandbox/v1/schema.", + surface: "v1", + title: "Stream metadata", }, { - surface: "v1", - title: "List records", - method: "GET", + description: "Supports cursor pagination via `limit` and `cursor`.", endpoint: "/sandbox/v1/streams/pay_statements/records", + method: "GET", request: "curl -s '/sandbox/v1/streams/pay_statements/records?limit=2'", responseHint: "Live `list` envelope of `record` rows: { object:'record', id, stream, data, emitted_at }.", - description: "Supports cursor pagination via `limit` and `cursor`.", + surface: "v1", + title: "List records", }, { - surface: "v1", - title: "Record detail", - method: "GET", + description: "Use the `id` returned in list responses (record_key in the underlying RS).", endpoint: "/sandbox/v1/streams/pay_statements/records/rec_sb_paystmt_2026_03", + method: "GET", request: "curl -s /sandbox/v1/streams/pay_statements/records/rec_sb_paystmt_2026_03", responseHint: "Single live `record` envelope: { id, stream, data, emitted_at }.", - description: "Use the `id` returned in list responses (record_key in the underlying RS).", + surface: "v1", + title: "Record detail", }, { - surface: "v1", - title: "Lexical search", - method: "GET", + description: "Same shape as the public lexical retrieval extension. Try `payroll`, `Northwind`, `follow-up`.", endpoint: "/sandbox/v1/search", + method: "GET", request: "curl -s '/sandbox/v1/search?q=payroll'", responseHint: "Live `list` of `search_result` rows: { stream, record_key, connector_id, record_url, emitted_at, matched_fields, snippet?, score? } with score.kind='bm25'.", - description: "Same shape as the public lexical retrieval extension. Try `payroll`, `Northwind`, `follow-up`.", + surface: "v1", + title: "Lexical search", }, { - surface: "_ref", - title: "List grants", - method: "GET", + description: "Filter by `status` (issued, revoked, denied) or `client_id`.", endpoint: "/sandbox/_ref/grants", + method: "GET", request: "curl -s '/sandbox/_ref/grants?status=revoked'", responseHint: "Live `list` of `grant_summary` rows with kinds[], failure?: { event_type, reason }.", - description: "Filter by `status` (issued, revoked, denied) or `client_id`.", + surface: "_ref", + title: "List grants", }, { - surface: "_ref", - title: "Grant timeline", - method: "GET", + description: "See request → consent → grant → resource read events end-to-end.", endpoint: "/sandbox/_ref/grants/grant_sb_quill_paystmt/timeline", + method: "GET", request: "curl -s /sandbox/_ref/grants/grant_sb_quill_paystmt/timeline", responseHint: "Live `grant_timeline` envelope: { object, grant_id, trace_id, event_count, data: events[] }.", - description: "See request → consent → grant → resource read events end-to-end.", + surface: "_ref", + title: "Grant timeline", }, { - surface: "_ref", - title: "Run timeline", - method: "GET", + description: "Includes `started`, `records.synced`, and `succeeded`/`failed` events.", endpoint: "/sandbox/_ref/runs/run_sb_acme_2026_04_22/timeline", + method: "GET", request: "curl -s /sandbox/_ref/runs/run_sb_acme_2026_04_22/timeline", responseHint: "Live `run_timeline` envelope: { object, run_id, trace_id, event_count, data: events[] }.", - description: "Includes `started`, `records.synced`, and `succeeded`/`failed` events.", + surface: "_ref", + title: "Run timeline", }, { - surface: "_ref", - title: "Trace timeline", - method: "GET", + description: "Shows the full PDPP interaction across grant + resource read.", endpoint: "/sandbox/_ref/traces/trace_sb_quill_paystmt", + method: "GET", request: "curl -s /sandbox/_ref/traces/trace_sb_quill_paystmt", responseHint: "Live `trace` envelope: { object, trace_id, event_count, data: events[] sorted by occurred_at }.", - description: "Shows the full PDPP interaction across grant + resource read.", + surface: "_ref", + title: "Trace timeline", }, { - surface: "_ref", - title: "Dataset summary", - method: "GET", + description: "Same fields as the live reference deployment summary.", endpoint: "/sandbox/_ref/dataset/summary", + method: "GET", request: "curl -s /sandbox/_ref/dataset/summary", responseHint: "Live `dataset_summary` envelope with retained-bytes, record-time bounds, top_connectors[].", - description: "Same fields as the live reference deployment summary.", + surface: "_ref", + title: "Dataset summary", }, { - surface: "well-known", - title: "Authorization server metadata", - method: "GET", + description: "Inspect the issuer, token/PAR/device endpoints, and provider-connect capabilities.", endpoint: "/sandbox/.well-known/oauth-authorization-server", + method: "GET", request: "curl -s /sandbox/.well-known/oauth-authorization-server", responseHint: "RFC 8414 + PDPP `pdpp_provider_connect_capabilities`. Same shape as a real AS.", - description: "Inspect the issuer, token/PAR/device endpoints, and provider-connect capabilities.", + surface: "well-known", + title: "Authorization server metadata", }, { - surface: "well-known", - title: "Protected resource metadata", - method: "GET", + description: "Drives discovery: schema_endpoint, query_base, search endpoint, blob indirection.", endpoint: "/sandbox/.well-known/oauth-protected-resource", + method: "GET", request: "curl -s /sandbox/.well-known/oauth-protected-resource", responseHint: "Live RS metadata: resource, authorization_servers, pdpp_discovery_hints, capabilities.lexical_retrieval.", - description: "Drives discovery: schema_endpoint, query_base, search endpoint, blob indirection.", + surface: "well-known", + title: "Protected resource metadata", }, ]; diff --git a/apps/site/src/app/sandbox/grants/[grantId]/page.tsx b/apps/site/src/app/sandbox/grants/[grantId]/page.tsx index 024e4c515..9a900035f 100644 --- a/apps/site/src/app/sandbox/grants/[grantId]/page.tsx +++ b/apps/site/src/app/sandbox/grants/[grantId]/page.tsx @@ -20,7 +20,7 @@ export default async function SandboxGrantDetailPage({ params }: { params: Promi return ( ) export default async function SandboxGrantsPage({ searchParams }: { searchParams: Promise }) { const params = await searchParams; const ds = sandboxDashboardDataSource; - const result = await ds.listGrants({ cursor: params.cursor, status: params.status, q: params.q, limit: 50 }); + const result = await ds.listGrants({ cursor: params.cursor, limit: 50, q: params.q, status: params.status }); const peekEnvelope = params.peek ? await ds.getGrantTimeline(params.peek) : null; const viewParams: ListWithPeekParams = { active: "grants", - routes: sandboxRoutes, - subject: "grant", - title: "Grants", + activeFilterChips: [ + params.status ? { label: "state", value: params.status } : null, + params.q ? { label: "query", value: params.q } : null, + ].filter((c): c is { label: string; value: string } => Boolean(c)), + buildListHref: (overrides) => listHref(params, overrides), description: "Issued, revoked, and denied grant decisions.", - result, - rowKey: (g) => g.grant_id, + emptyHint: "No grants match this filter.", + emptyTitle: "No grants", + filters: { + query: { defaultValue: params.q ?? "", name: "q", placeholder: "id contains…" }, + status: { + defaultValue: params.status ?? "", + name: "status", + options: [ + { label: "issued", value: "issued" }, + { label: "revoked", value: "revoked" }, + { label: "denied", value: "denied" }, + ], + }, + }, + peekCliCommand: (id) => `pdpp ref grant timeline ${id}`, + peekEnvelope, + peekId: params.peek, renderRow: (grant, { peeked, href, detailHref }) => { const rowContent = ( <> @@ -77,29 +94,12 @@ export default async function SandboxGrantsPage({ searchParams }: { searchParams
); }, - filters: { - query: { name: "q", placeholder: "id contains…", defaultValue: params.q ?? "" }, - status: { - name: "status", - defaultValue: params.status ?? "", - options: [ - { value: "issued", label: "issued" }, - { value: "revoked", label: "revoked" }, - { value: "denied", label: "denied" }, - ], - }, - }, - activeFilterChips: [ - params.status ? { label: "state", value: params.status } : null, - params.q ? { label: "query", value: params.q } : null, - ].filter((c): c is { label: string; value: string } => Boolean(c)), resetHref: "/sandbox/grants", - buildListHref: (overrides) => listHref(params, overrides), - peekId: params.peek, - peekEnvelope, - peekCliCommand: (id) => `pdpp ref grant timeline ${id}`, - emptyTitle: "No grants", - emptyHint: "No grants match this filter.", + result, + routes: sandboxRoutes, + rowKey: (g) => g.grant_id, + subject: "grant", + title: "Grants", }; return ( diff --git a/apps/site/src/app/sandbox/overview-content.tsx b/apps/site/src/app/sandbox/overview-content.tsx index 8ea4cfe4c..295f10f75 100644 --- a/apps/site/src/app/sandbox/overview-content.tsx +++ b/apps/site/src/app/sandbox/overview-content.tsx @@ -17,23 +17,23 @@ async function loadOverview(): Promise { const ds = sandboxDashboardDataSource; const [summary, failedTraces, failedRuns, revoked, denied, issued, recentRuns] = await Promise.all([ ds.getDatasetSummary(), - ds.listTraces({ status: "failed", limit: 5 }), - ds.listRuns({ status: "failed", limit: 5 }), - ds.listGrants({ status: "revoked", limit: 5 }), - ds.listGrants({ status: "denied", limit: 5 }), - ds.listGrants({ status: "issued", limit: 5 }), + ds.listTraces({ limit: 5, status: "failed" }), + ds.listRuns({ limit: 5, status: "failed" }), + ds.listGrants({ limit: 5, status: "revoked" }), + ds.listGrants({ limit: 5, status: "denied" }), + ds.listGrants({ limit: 5, status: "issued" }), ds.listRuns({ limit: 8 }), ]); const recentDecisions = [...revoked.data, ...denied.data, ...issued.data] .sort((a, b) => (a.last_at < b.last_at ? 1 : -1)) .slice(0, 6); return { - summary, - failedTraces: failedTraces.data, + actionNeeded: failedTraces.data.length + failedRuns.data.length, failedRuns: failedRuns.data, + failedTraces: failedTraces.data, recentDecisions, recentRuns: recentRuns.data, - actionNeeded: failedTraces.data.length + failedRuns.data.length, + summary, }; } diff --git a/apps/site/src/app/sandbox/overview/page.tsx b/apps/site/src/app/sandbox/overview/page.tsx index 09538cf55..cf9dd2381 100644 --- a/apps/site/src/app/sandbox/overview/page.tsx +++ b/apps/site/src/app/sandbox/overview/page.tsx @@ -5,8 +5,8 @@ import type { Metadata } from "next"; import { SandboxOverviewContent } from "../overview-content.tsx"; export const metadata: Metadata = { - title: "PDPP reference instance · Overview", description: "Overview of the PDPP reference dashboard, bound to deterministic mock AS/RS data.", + title: "PDPP reference instance · Overview", }; export const dynamic = "force-static"; diff --git a/apps/site/src/app/sandbox/page.tsx b/apps/site/src/app/sandbox/page.tsx index 88e604d5b..0938d32d1 100644 --- a/apps/site/src/app/sandbox/page.tsx +++ b/apps/site/src/app/sandbox/page.tsx @@ -5,8 +5,8 @@ import type { Metadata } from "next"; import { SandboxOverviewContent } from "./overview-content.tsx"; export const metadata: Metadata = { - title: "PDPP reference instance · Sandbox", description: "Explore a mock-adapter-backed PDPP reference instance with a complete reference data profile.", + title: "PDPP reference instance · Sandbox", }; export const dynamic = "force-static"; diff --git a/apps/site/src/app/sandbox/records/explore-redirect.ts b/apps/site/src/app/sandbox/records/explore-redirect.ts index b119d6d99..6dbd5a069 100644 --- a/apps/site/src/app/sandbox/records/explore-redirect.ts +++ b/apps/site/src/app/sandbox/records/explore-redirect.ts @@ -24,10 +24,10 @@ export function sandboxExploreRedirectHref({ params.set( "peek", explorerPeekParam({ - connectorId, connectionId: connectorId, - stream, + connectorId, recordId, + stream, }) ); } diff --git a/apps/site/src/app/sandbox/ref/grants/route.ts b/apps/site/src/app/sandbox/ref/grants/route.ts index f22549aee..8109e3a02 100644 --- a/apps/site/src/app/sandbox/ref/grants/route.ts +++ b/apps/site/src/app/sandbox/ref/grants/route.ts @@ -13,13 +13,13 @@ export async function GET(request: Request) { return jsonResponse( await executeRefSpineCorrelationsList( { - kind: "grant", filters: { + client_id: params.client_id, cursor: params.cursor, limit: params.limit, status: params.status, - client_id: params.client_id, }, + kind: "grant", }, createSandboxRefSpineCorrelationsListDependencies() ) diff --git a/apps/site/src/app/sandbox/ref/runs/route.ts b/apps/site/src/app/sandbox/ref/runs/route.ts index 40a4fdf34..9e5ce4329 100644 --- a/apps/site/src/app/sandbox/ref/runs/route.ts +++ b/apps/site/src/app/sandbox/ref/runs/route.ts @@ -13,13 +13,13 @@ export async function GET(request: Request) { return jsonResponse( await executeRefSpineCorrelationsList( { - kind: "run", filters: { + connector_id: params.connector_id, cursor: params.cursor, limit: params.limit, status: params.status, - connector_id: params.connector_id, }, + kind: "run", }, createSandboxRefSpineCorrelationsListDependencies() ) diff --git a/apps/site/src/app/sandbox/ref/traces/route.ts b/apps/site/src/app/sandbox/ref/traces/route.ts index e0e837d6d..527b476d8 100644 --- a/apps/site/src/app/sandbox/ref/traces/route.ts +++ b/apps/site/src/app/sandbox/ref/traces/route.ts @@ -13,12 +13,12 @@ export async function GET(request: Request) { return jsonResponse( await executeRefSpineCorrelationsList( { - kind: "trace", filters: { cursor: params.cursor, limit: params.limit, status: params.status, }, + kind: "trace", }, createSandboxRefSpineCorrelationsListDependencies() ) diff --git a/apps/site/src/app/sandbox/runs/[runId]/page.tsx b/apps/site/src/app/sandbox/runs/[runId]/page.tsx index 78a9a5309..c31c88d18 100644 --- a/apps/site/src/app/sandbox/runs/[runId]/page.tsx +++ b/apps/site/src/app/sandbox/runs/[runId]/page.tsx @@ -20,7 +20,7 @@ export default async function SandboxRunDetailPage({ params }: { params: Promise return ( diff --git a/apps/site/src/app/sandbox/runs/page.tsx b/apps/site/src/app/sandbox/runs/page.tsx index 92f3a6ffd..d03de711c 100644 --- a/apps/site/src/app/sandbox/runs/page.tsx +++ b/apps/site/src/app/sandbox/runs/page.tsx @@ -33,22 +33,42 @@ export default async function SandboxRunsPage({ searchParams }: { searchParams: const params = await searchParams; const ds = sandboxDashboardDataSource; const result = await ds.listRuns({ - cursor: params.cursor, - status: params.status, connector_id: params.connector_id, - q: params.q, + cursor: params.cursor, limit: 50, + q: params.q, + status: params.status, }); const peekEnvelope = params.peek ? await ds.getRunTimeline(params.peek) : null; const viewParams: ListWithPeekParams = { active: "runs", - routes: sandboxRoutes, - subject: "run", - title: "Runs", + activeFilterChips: [ + params.status ? { label: "status", value: params.status } : null, + params.connector_id ? { label: "connector", value: params.connector_id } : null, + params.q ? { label: "query", value: params.q } : null, + ].filter((c): c is { label: string; value: string } => Boolean(c)), + buildListHref: (overrides) => listHref(params, overrides), description: "Connector runs and their outcomes.", - result, - rowKey: (r) => r.run_id, + emptyHint: "No runs match this filter.", + emptyTitle: "No runs", + filters: { + connector: { defaultValue: params.connector_id ?? "", name: "connector_id" }, + query: { defaultValue: params.q ?? "", name: "q", placeholder: "id contains…" }, + status: { + defaultValue: params.status ?? "", + name: "status", + options: [ + { label: "succeeded", value: "succeeded" }, + { label: "failed", value: "failed" }, + { label: "needs input", value: "needs_input" }, + { label: "started", value: "started" }, + ], + }, + }, + peekCliCommand: (id) => `pdpp ref run timeline ${id}`, + peekEnvelope, + peekId: params.peek, renderRow: (run, { peeked, href, detailHref }) => { const rowContent = ( <> @@ -88,32 +108,12 @@ export default async function SandboxRunsPage({ searchParams }: { searchParams:
); }, - filters: { - query: { name: "q", placeholder: "id contains…", defaultValue: params.q ?? "" }, - connector: { name: "connector_id", defaultValue: params.connector_id ?? "" }, - status: { - name: "status", - defaultValue: params.status ?? "", - options: [ - { value: "succeeded", label: "succeeded" }, - { value: "failed", label: "failed" }, - { value: "needs_input", label: "needs input" }, - { value: "started", label: "started" }, - ], - }, - }, - activeFilterChips: [ - params.status ? { label: "status", value: params.status } : null, - params.connector_id ? { label: "connector", value: params.connector_id } : null, - params.q ? { label: "query", value: params.q } : null, - ].filter((c): c is { label: string; value: string } => Boolean(c)), resetHref: "/sandbox/runs", - buildListHref: (overrides) => listHref(params, overrides), - peekId: params.peek, - peekEnvelope, - peekCliCommand: (id) => `pdpp ref run timeline ${id}`, - emptyTitle: "No runs", - emptyHint: "No runs match this filter.", + result, + routes: sandboxRoutes, + rowKey: (r) => r.run_id, + subject: "run", + title: "Runs", }; return ( diff --git a/apps/site/src/app/sandbox/traces/[traceId]/page.tsx b/apps/site/src/app/sandbox/traces/[traceId]/page.tsx index 07422434d..a464514f4 100644 --- a/apps/site/src/app/sandbox/traces/[traceId]/page.tsx +++ b/apps/site/src/app/sandbox/traces/[traceId]/page.tsx @@ -20,7 +20,7 @@ export default async function SandboxTraceDetailPage({ params }: { params: Promi return ( diff --git a/apps/site/src/app/sandbox/traces/page.tsx b/apps/site/src/app/sandbox/traces/page.tsx index 9d6e6f540..f980aafa4 100644 --- a/apps/site/src/app/sandbox/traces/page.tsx +++ b/apps/site/src/app/sandbox/traces/page.tsx @@ -33,20 +33,38 @@ export default async function SandboxTracesPage({ searchParams }: { searchParams const ds = sandboxDashboardDataSource; const result = await ds.listTraces({ cursor: params.cursor, - status: params.status, - q: params.q, limit: 50, + q: params.q, + status: params.status, }); const peekEnvelope = params.peek ? await ds.getTraceTimeline(params.peek) : null; const viewParams: ListWithPeekParams = { active: "traces", - routes: sandboxRoutes, - subject: "trace", - title: "Traces", + activeFilterChips: [ + params.status ? { label: "status", value: params.status } : null, + params.q ? { label: "query", value: params.q } : null, + ].filter((c): c is { label: string; value: string } => Boolean(c)), + buildListHref: (overrides) => listHref(params, overrides), description: "Event-spine traces across grants and runs.", - result, - rowKey: (t) => t.trace_id, + emptyHint: "No traces match this filter.", + emptyTitle: "No traces", + filters: { + query: { defaultValue: params.q ?? "", name: "q", placeholder: "id contains…" }, + status: { + defaultValue: params.status ?? "", + name: "status", + options: [ + { label: "succeeded", value: "succeeded" }, + { label: "failed", value: "failed" }, + { label: "denied", value: "denied" }, + { label: "revoked", value: "revoked" }, + ], + }, + }, + peekCliCommand: (id) => `pdpp ref trace show ${id}`, + peekEnvelope, + peekId: params.peek, renderRow: (trace, { peeked, href, detailHref }) => { const rowContent = ( <> @@ -83,30 +101,12 @@ export default async function SandboxTracesPage({ searchParams }: { searchParams
); }, - filters: { - query: { name: "q", placeholder: "id contains…", defaultValue: params.q ?? "" }, - status: { - name: "status", - defaultValue: params.status ?? "", - options: [ - { value: "succeeded", label: "succeeded" }, - { value: "failed", label: "failed" }, - { value: "denied", label: "denied" }, - { value: "revoked", label: "revoked" }, - ], - }, - }, - activeFilterChips: [ - params.status ? { label: "status", value: params.status } : null, - params.q ? { label: "query", value: params.q } : null, - ].filter((c): c is { label: string; value: string } => Boolean(c)), resetHref: "/sandbox/traces", - buildListHref: (overrides) => listHref(params, overrides), - peekId: params.peek, - peekEnvelope, - peekCliCommand: (id) => `pdpp ref trace show ${id}`, - emptyTitle: "No traces", - emptyHint: "No traces match this filter.", + result, + routes: sandboxRoutes, + rowKey: (t) => t.trace_id, + subject: "trace", + title: "Traces", }; return ( diff --git a/apps/site/src/app/sandbox/v1/_helpers.ts b/apps/site/src/app/sandbox/v1/_helpers.ts index 9c0455b38..764f003e4 100644 --- a/apps/site/src/app/sandbox/v1/_helpers.ts +++ b/apps/site/src/app/sandbox/v1/_helpers.ts @@ -10,16 +10,16 @@ */ const DEMO_HEADERS: HeadersInit = { - "content-type": "application/json; charset=utf-8", "cache-control": "public, max-age=0, must-revalidate", + "content-type": "application/json; charset=utf-8", "x-pdpp-demo": "1", "x-pdpp-demo-notice": "sandbox-fictional-data-only", }; export function jsonResponse(body: unknown, init?: { status?: number }): Response { return new Response(JSON.stringify(body, null, 2), { - status: init?.status ?? 200, headers: DEMO_HEADERS, + status: init?.status ?? 200, }); } @@ -27,10 +27,10 @@ export function notFound(message: string): Response { return jsonResponse( { error: { - type: "not_found_error", code: "not_found", message, request_id: "req_sandbox_not_found", + type: "not_found_error", }, }, { status: 404 } @@ -128,10 +128,10 @@ export function readListParams(url: URL): { } } return { + client_id: url.searchParams.get("client_id") ?? undefined, + connector_id: url.searchParams.get("connector_id") ?? undefined, cursor, limit, status: url.searchParams.get("status") ?? undefined, - client_id: url.searchParams.get("client_id") ?? undefined, - connector_id: url.searchParams.get("connector_id") ?? undefined, }; } diff --git a/apps/site/src/app/sandbox/v1/search/route.ts b/apps/site/src/app/sandbox/v1/search/route.ts index 72d9d2f34..ec1d0e444 100644 --- a/apps/site/src/app/sandbox/v1/search/route.ts +++ b/apps/site/src/app/sandbox/v1/search/route.ts @@ -107,9 +107,9 @@ function errorResponse(err: SearchLexicalRequestError): Response { return jsonResponse( { error: { - type, code: err.code, message: err.message, + type, ...(err.param ? { param: err.param } : {}), request_id: "req_sandbox_search_invalid", }, diff --git a/apps/site/src/app/sandbox/v1/streams/[stream]/records/[recordId]/route.ts b/apps/site/src/app/sandbox/v1/streams/[stream]/records/[recordId]/route.ts index ca8bca86b..70a65e0a3 100644 --- a/apps/site/src/app/sandbox/v1/streams/[stream]/records/[recordId]/route.ts +++ b/apps/site/src/app/sandbox/v1/streams/[stream]/records/[recordId]/route.ts @@ -16,8 +16,8 @@ export async function GET(_request: Request, ctx: { params: Promise<{ stream: st const out = await executeRecordDetail( { actor: { kind: "owner", subject_id: null }, - streamName: stream, recordId, + streamName: stream, }, createSandboxRecordDetailDependencies(stream) ); diff --git a/apps/site/src/app/sandbox/v1/streams/[stream]/records/route.ts b/apps/site/src/app/sandbox/v1/streams/[stream]/records/route.ts index 220dfdbe5..71f96644d 100644 --- a/apps/site/src/app/sandbox/v1/streams/[stream]/records/route.ts +++ b/apps/site/src/app/sandbox/v1/streams/[stream]/records/route.ts @@ -26,8 +26,8 @@ export async function GET(request: Request, ctx: { params: Promise<{ stream: str const out = await executeRecordsList( { actor: { kind: "owner", subject_id: null }, - streamName: stream, requestParams, + streamName: stream, }, createSandboxRecordsListDependencies({ streamName: stream, diff --git a/apps/site/src/app/sandbox/v1/streams/route.ts b/apps/site/src/app/sandbox/v1/streams/route.ts index fb87cd755..c189b8cae 100644 --- a/apps/site/src/app/sandbox/v1/streams/route.ts +++ b/apps/site/src/app/sandbox/v1/streams/route.ts @@ -59,11 +59,11 @@ export async function GET(request: Request): Promise { record_count: number; } const data: SandboxStream[] = slice.map((s) => ({ - object: s.object, + freshness: { last_updated: s.last_updated }, + last_updated: s.last_updated, name: s.name, + object: s.object, record_count: s.record_count, - last_updated: s.last_updated, - freshness: { last_updated: s.last_updated }, })); interface SandboxStreamsEnvelope { @@ -73,9 +73,9 @@ export async function GET(request: Request): Promise { object: "list"; } const envelope: SandboxStreamsEnvelope = { - object: "list", - has_more: hasMore, data, + has_more: hasMore, + object: "list", }; if (hasMore) { envelope.next_cursor = String(next); diff --git a/apps/site/src/app/sandbox/walkthrough/page.tsx b/apps/site/src/app/sandbox/walkthrough/page.tsx index a64de2d0a..94dbf14dd 100644 --- a/apps/site/src/app/sandbox/walkthrough/page.tsx +++ b/apps/site/src/app/sandbox/walkthrough/page.tsx @@ -8,9 +8,9 @@ import { SandboxEducationalShell } from "../_demo/components/shell.tsx"; import { SandboxWalkthrough } from "./sandbox-walkthrough.tsx"; export const metadata: Metadata = { - title: "PDPP Sandbox walkthrough - scoped grant end to end", description: "An interactive PDPP reference scenario: a client requests scoped pay-statement access, the owner approves a bounded grant, the resource server returns only granted fields, and revocation refuses the next read.", + title: "PDPP Sandbox walkthrough - scoped grant end to end", }; export default function WalkthroughPage() { @@ -26,13 +26,13 @@ export default function WalkthroughPage() { The transcript shows the API-shaped artifact for each step.

- + Start the walkthrough - + Open sandbox dashboard - + Protocol docs
diff --git a/apps/site/src/app/sandbox/walkthrough/sandbox-walkthrough.tsx b/apps/site/src/app/sandbox/walkthrough/sandbox-walkthrough.tsx index 9a33f6520..c964e9037 100644 --- a/apps/site/src/app/sandbox/walkthrough/sandbox-walkthrough.tsx +++ b/apps/site/src/app/sandbox/walkthrough/sandbox-walkthrough.tsx @@ -29,25 +29,25 @@ import { } from "./state.ts"; const STEP_COPY: Record, { eyebrow: string; title: string; body: string }> = { - requested: { - eyebrow: "Step 1 of 4", - title: "Quill Tax asks for scoped pay statements", - body: "A fictional tax-prep app stages a request through the authorization endpoint. It names the streams, fields, and connector it needs, plus a short purpose and self-asserted commitments.", - }, granted: { + body: "The owner sees exactly what was asked for, prunes anything they don't want shared (no SSN, no bank number), and issues a single-use grant scoped to the requested fields.", eyebrow: "Step 2 of 4", title: "Sam approves a bounded grant", - body: "The owner sees exactly what was asked for, prunes anything they don't want shared (no SSN, no bank number), and issues a single-use grant scoped to the requested fields.", }, queried: { + body: "The resource server projects records down to the granted fields and returns them to the client. Nothing outside the scope is reachable through this grant.", eyebrow: "Step 3 of 4", title: "Quill Tax reads only the granted fields", - body: "The resource server projects records down to the granted fields and returns them to the client. Nothing outside the scope is reachable through this grant.", + }, + requested: { + body: "A fictional tax-prep app stages a request through the authorization endpoint. It names the streams, fields, and connector it needs, plus a short purpose and self-asserted commitments.", + eyebrow: "Step 1 of 4", + title: "Quill Tax asks for scoped pay statements", }, revoked: { + body: "Revocation tears the grant down. Any further read attempt against the same grant id receives a 403 and a `grant_revoked` error code.", eyebrow: "Step 4 of 4", title: "Sam revokes; the next read is refused", - body: "Revocation tears the grant down. Any further read attempt against the same grant id receives a 403 and a `grant_revoked` error code.", }, }; @@ -63,58 +63,58 @@ function nextActions(state: SandboxState): readonly ButtonSpec[] { case "initial": return [ { - label: state.decision === "denied" ? "Stage a new request" : "Stage the request", action: { type: "request" }, - variant: "default", hint: "Simulates a client POST to /par with the proposed scope.", + label: state.decision === "denied" ? "Stage a new request" : "Stage the request", + variant: "default", }, ]; case "requested": return [ { - label: "Approve as Sam", action: { type: "approve" }, - variant: "default", hint: "Issues a grant scoped to the listed fields only.", + label: "Approve as Sam", + variant: "default", }, { - label: "Deny as Sam", action: { type: "deny" }, - variant: "outline", hint: "Sends the visitor back to the start. No grant is created.", + label: "Deny as Sam", + variant: "outline", }, ]; case "granted": return [ { - label: "Run the scoped query", action: { type: "query" }, - variant: "default", hint: "GETs records using the new grant; only granted fields come back.", + label: "Run the scoped query", + variant: "default", }, { - label: "Revoke before any read", action: { type: "revoke" }, - variant: "destructive", hint: "Owner can revoke at any time, even before the client reads.", + label: "Revoke before any read", + variant: "destructive", }, ]; case "queried": return [ { - label: "Revoke the grant", action: { type: "revoke" }, - variant: "destructive", hint: "Tears the grant down and refuses any further read.", + label: "Revoke the grant", + variant: "destructive", }, ]; case "revoked": return [ { - label: "Reset and replay", action: { type: "reset" }, - variant: "outline", hint: "Clears all sandbox state and returns to step 0.", + label: "Reset and replay", + variant: "outline", }, ]; default: diff --git a/apps/site/src/app/sandbox/walkthrough/scenario.ts b/apps/site/src/app/sandbox/walkthrough/scenario.ts index 3038601db..c3661508e 100644 --- a/apps/site/src/app/sandbox/walkthrough/scenario.ts +++ b/apps/site/src/app/sandbox/walkthrough/scenario.ts @@ -15,42 +15,42 @@ export const SANDBOX_OWNER = { export const SANDBOX_CLIENT = { clientId: "client_quill_tax_demo", - name: "Quill Tax", - monogram: "QT", - homepage: "https://example.invalid/quill-tax", - policyUri: "https://example.invalid/quill-tax/privacy", - tosUri: "https://example.invalid/quill-tax/terms", - verified: false, - purpose: "Import the last three pay statements so you can finish your tax return without re-keying numbers.", commitments: [ "Read-only access; no writes back to the payroll connector.", "Hold imported pay statements only until the return is filed.", "No resale or sharing with third-party advertisers.", ], + homepage: "https://example.invalid/quill-tax", + monogram: "QT", + name: "Quill Tax", + policyUri: "https://example.invalid/quill-tax/privacy", + purpose: "Import the last three pay statements so you can finish your tax return without re-keying numbers.", + tosUri: "https://example.invalid/quill-tax/terms", + verified: false, } as const; export const SANDBOX_CONNECTOR = { - source: { kind: "connector", id: "acme_payroll_demo" }, name: "Acme Payroll (simulated)", notes: "Demo payroll connector with a stable pay-statement profile for the guided walkthrough.", + source: { id: "acme_payroll_demo", kind: "connector" }, } as const; export const SANDBOX_STREAM = { - key: "pay_statements", - label: "Pay statements", detail: "Net and gross pay totals from the last three pay periods, plus the issuing employer name.", fields: ["period_end", "employer", "gross_pay_cents", "net_pay_cents", "currency"] as const, + key: "pay_statements", + label: "Pay statements", retention: "Until the visitor resets this sandbox in the browser.", } as const; export const SANDBOX_GRANT = { - grantId: "grant_sb_2026_demo", - purposeCode: "tax_filing", accessMode: "single_use" as const, expiresAt: "2026-05-25T00:00:00Z", + grantId: "grant_sb_2026_demo", + purposeCode: "tax_filing", scope: { - streams: [SANDBOX_STREAM.key], fields: SANDBOX_STREAM.fields, + streams: [SANDBOX_STREAM.key], }, } as const; @@ -65,28 +65,28 @@ export interface SandboxRecord { export const SANDBOX_RECORDS: readonly SandboxRecord[] = [ { - recordId: "rec_sb_paystmt_2026_03", - period_end: "2026-03-31", + currency: "USD", employer: "Northwind Studios (simulated)", gross_pay_cents: 612_500, net_pay_cents: 438_120, - currency: "USD", + period_end: "2026-03-31", + recordId: "rec_sb_paystmt_2026_03", }, { - recordId: "rec_sb_paystmt_2026_02", - period_end: "2026-02-28", + currency: "USD", employer: "Northwind Studios (simulated)", gross_pay_cents: 612_500, net_pay_cents: 437_980, - currency: "USD", + period_end: "2026-02-28", + recordId: "rec_sb_paystmt_2026_02", }, { - recordId: "rec_sb_paystmt_2026_01", - period_end: "2026-01-31", + currency: "USD", employer: "Northwind Studios (simulated)", gross_pay_cents: 612_500, net_pay_cents: 436_640, - currency: "USD", + period_end: "2026-01-31", + recordId: "rec_sb_paystmt_2026_01", }, ] as const; @@ -110,5 +110,5 @@ export const SANDBOX_CONSENT_RATIONALE = { */ export function formatUsdCents(cents: number): string { const dollars = cents / 100; - return dollars.toLocaleString("en-US", { style: "currency", currency: "USD" }); + return dollars.toLocaleString("en-US", { currency: "USD", style: "currency" }); } diff --git a/apps/site/src/app/sandbox/walkthrough/state.ts b/apps/site/src/app/sandbox/walkthrough/state.ts index d4cfa0c54..2ff7dce72 100644 --- a/apps/site/src/app/sandbox/walkthrough/state.ts +++ b/apps/site/src/app/sandbox/walkthrough/state.ts @@ -34,11 +34,11 @@ export interface SandboxState { } export const INITIAL_STATE: SandboxState = { - phase: "initial", decision: "pending", - recordsVisible: false, - lastDeniedQueryAt: null, history: ["initial"], + lastDeniedQueryAt: null, + phase: "initial", + recordsVisible: false, }; const FROZEN_AT = "2026-04-25T15:00:00Z"; @@ -49,13 +49,13 @@ export function reduce(state: SandboxState, action: SandboxAction): SandboxState if (state.phase !== "initial") { return state; } - return advance(state, { phase: "requested", decision: "pending" }); + return advance(state, { decision: "pending", phase: "requested" }); } case "approve": { if (state.phase !== "requested") { return state; } - return advance(state, { phase: "granted", decision: "approved" }); + return advance(state, { decision: "approved", phase: "granted" }); } case "deny": { if (state.phase !== "requested") { @@ -80,9 +80,9 @@ export function reduce(state: SandboxState, action: SandboxAction): SandboxState return state; } return advance(state, { + lastDeniedQueryAt: FROZEN_AT, phase: "revoked", recordsVisible: false, - lastDeniedQueryAt: FROZEN_AT, }); } case "reset": @@ -122,99 +122,99 @@ export function buildTranscript(state: SandboxState): readonly TranscriptEntry[] return [ { - id: "requested", - label: "1. Client requests access", - method: "POST", - endpoint: "/par", available: requested, body: { - simulated: true, + access_mode: SANDBOX_GRANT.accessMode, + client_claims: { + attribution: "self-asserted by client; not verified by the sandbox", + commitments: SANDBOX_CLIENT.commitments, + }, client_id: SANDBOX_CLIENT.clientId, - purpose_description: SANDBOX_CLIENT.purpose, purpose_code: SANDBOX_GRANT.purposeCode, - access_mode: SANDBOX_GRANT.accessMode, + purpose_description: SANDBOX_CLIENT.purpose, requested_scope: { - streams: [SANDBOX_STREAM.key], fields: SANDBOX_STREAM.fields, sources: [SANDBOX_CONNECTOR.source], + streams: [SANDBOX_STREAM.key], }, - client_claims: { - commitments: SANDBOX_CLIENT.commitments, - attribution: "self-asserted by client; not verified by the sandbox", - }, + simulated: true, }, + endpoint: "/par", + id: "requested", + label: "1. Client requests access", + method: "POST", }, { - id: "denied", - label: "2a. Owner denies the request", - method: "POST", - endpoint: "/grants", available: denied, body: { - simulated: true, - status: 403, + client_id: SANDBOX_CLIENT.clientId, error: "owner_denied", + message: "Owner declined this request. No grant was created and no records were returned.", owner_id: SANDBOX_OWNER.ownerId, - client_id: SANDBOX_CLIENT.clientId, requested_scope: { - streams: [SANDBOX_STREAM.key], fields: SANDBOX_STREAM.fields, + streams: [SANDBOX_STREAM.key], }, - message: "Owner declined this request. No grant was created and no records were returned.", + simulated: true, + status: 403, }, + endpoint: "/grants", + id: "denied", + label: "2a. Owner denies the request", + method: "POST", }, { - id: "granted", - label: "2. Owner consent + grant issued", - method: "POST", - endpoint: "/grants", available: granted, body: { - simulated: true, + access_mode: SANDBOX_GRANT.accessMode, + client_id: SANDBOX_CLIENT.clientId, + consent_rationale: SANDBOX_CONSENT_RATIONALE, + expires_at: SANDBOX_GRANT.expiresAt, grant_id: SANDBOX_GRANT.grantId, owner_id: SANDBOX_OWNER.ownerId, - client_id: SANDBOX_CLIENT.clientId, - access_mode: SANDBOX_GRANT.accessMode, purpose_code: SANDBOX_GRANT.purposeCode, - expires_at: SANDBOX_GRANT.expiresAt, scope: SANDBOX_GRANT.scope, - consent_rationale: SANDBOX_CONSENT_RATIONALE, + simulated: true, }, + endpoint: "/grants", + id: "granted", + label: "2. Owner consent + grant issued", + method: "POST", }, { - id: "queried", - label: "3. Resource query returns scoped records", - method: "GET", - endpoint: `/streams/${SANDBOX_STREAM.key}/records?grant_id=${SANDBOX_GRANT.grantId}`, available: queried, body: { - simulated: true, grant_id: SANDBOX_GRANT.grantId, - stream: SANDBOX_STREAM.key, projected_fields: SANDBOX_STREAM.fields, - source: SANDBOX_CONNECTOR.source, record_count: SANDBOX_RECORDS.length, records: SANDBOX_RECORDS, + simulated: true, + source: SANDBOX_CONNECTOR.source, + stream: SANDBOX_STREAM.key, }, + endpoint: `/streams/${SANDBOX_STREAM.key}/records?grant_id=${SANDBOX_GRANT.grantId}`, + id: "queried", + label: "3. Resource query returns scoped records", + method: "GET", }, { - id: "revoked", - label: "4. Owner revokes / next query refused", - method: "DELETE", - endpoint: `/grants/${SANDBOX_GRANT.grantId}`, available: revoked, body: { - simulated: true, grant_id: SANDBOX_GRANT.grantId, - revoked_at: FROZEN_AT, next_attempt: { - method: "GET", endpoint: `/streams/${SANDBOX_STREAM.key}/records?grant_id=${SANDBOX_GRANT.grantId}`, - status: 403, error: "grant_revoked", message: "Grant was revoked by owner; no further records will be returned.", + method: "GET", + status: 403, }, + revoked_at: FROZEN_AT, + simulated: true, }, + endpoint: `/grants/${SANDBOX_GRANT.grantId}`, + id: "revoked", + label: "4. Owner revokes / next query refused", + method: "DELETE", }, ]; } diff --git a/apps/site/src/app/sandbox/well-known/oauth-authorization-server/route.ts b/apps/site/src/app/sandbox/well-known/oauth-authorization-server/route.ts index bf9f4463b..65044d738 100644 --- a/apps/site/src/app/sandbox/well-known/oauth-authorization-server/route.ts +++ b/apps/site/src/app/sandbox/well-known/oauth-authorization-server/route.ts @@ -10,7 +10,7 @@ export const dynamic = "force-dynamic"; export function GET(request: Request) { return jsonResponse( executeAsAuthorizationServerMetadata( - { issuer: sandboxIssuerFromRequest(request), dynamicClientRegistrationEnabled: false }, + { dynamicClientRegistrationEnabled: false, issuer: sandboxIssuerFromRequest(request) }, createSandboxAsAuthorizationServerMetadataDependencies() ) ); diff --git a/apps/site/src/app/well-known/skills/[...path]/route.ts b/apps/site/src/app/well-known/skills/[...path]/route.ts index a2a6ce3fb..bbd5a7f70 100644 --- a/apps/site/src/app/well-known/skills/[...path]/route.ts +++ b/apps/site/src/app/well-known/skills/[...path]/route.ts @@ -26,9 +26,9 @@ function notFound(): Response { return jsonResponse( { error: { - type: "not_found_error", code: "not_found", message: "Skill file not found", + type: "not_found_error", }, }, { status: 404 } diff --git a/apps/site/src/app/well-known/skills/route.test.ts b/apps/site/src/app/well-known/skills/route.test.ts index aaded22a3..e011f322e 100644 --- a/apps/site/src/app/well-known/skills/route.test.ts +++ b/apps/site/src/app/well-known/skills/route.test.ts @@ -100,9 +100,9 @@ test("agent skill .well-known route serves only allowlisted files", async () => const traversalBody = await jsonOf(traversal); assert.deepEqual(traversalBody, { error: { - type: "not_found_error", code: "not_found", message: "Skill file not found", + type: "not_found_error", }, }); }); diff --git a/apps/site/src/components/hero.tsx b/apps/site/src/components/hero.tsx index 61999232c..1d1850f9c 100644 --- a/apps/site/src/components/hero.tsx +++ b/apps/site/src/components/hero.tsx @@ -74,17 +74,17 @@ export function Hero({
@@ -95,9 +95,9 @@ export function Hero({
@@ -110,9 +110,9 @@ export function Hero({ return (
@@ -144,14 +144,14 @@ function HeroContent({

{title}

{description && ( -
+
{description}
)} diff --git a/apps/site/src/components/reference-app.tsx b/apps/site/src/components/reference-app.tsx index bbff3f482..413f92c53 100644 --- a/apps/site/src/components/reference-app.tsx +++ b/apps/site/src/components/reference-app.tsx @@ -85,11 +85,10 @@ const MULTI_CONNECTORS: ConnectorCardProps[] = [ { connectorId: "https://registry.pdpp.org/connectors/spotify", displayName: "Spotify", - version: "2.0.0", streams: [ { - name: "top_artists", label: "Top artists", + name: "top_artists", semantics: "mutable_state", supportsFields: true, supportsResources: false, @@ -97,8 +96,8 @@ const MULTI_CONNECTORS: ConnectorCardProps[] = [ viewCount: 2, }, { - name: "play_events", label: "Play history", + name: "play_events", semantics: "append_only", supportsFields: true, supportsResources: false, @@ -106,15 +105,15 @@ const MULTI_CONNECTORS: ConnectorCardProps[] = [ viewCount: 0, }, ], + version: "2.0.0", }, { connectorId: "https://registry.pdpp.org/connectors/oura", displayName: "Oura Ring", - version: "1.0.0", streams: [ { - name: "sleep_sessions", label: "Sleep sessions", + name: "sleep_sessions", semantics: "append_only", supportsFields: true, supportsResources: false, @@ -122,6 +121,7 @@ const MULTI_CONNECTORS: ConnectorCardProps[] = [ viewCount: 0, }, ], + version: "1.0.0", }, ]; @@ -139,73 +139,73 @@ interface SectionConfig { // under `noUncheckedIndexedAccess`. const SECTION_CONTENT = [ { - id: "ingest", headline: "Native where possible, connector-backed where needed", + id: "ingest", narrative: "Platforms can implement PDPP directly. Native endpoints, browser automation, and imports can all feed the same compensation records into one grant and enforcement model.", surface: "protocol", }, { - id: "inventory", headline: "Records make access exact", + id: "inventory", narrative: "Pay statements, equity grants, and benefits enrollments become records the server can match, project, and revoke. Once the data has shape, access can become exact.", surface: "protocol", }, { - id: "request", headline: "A client app requests access", + id: "request", narrative: `${LONGVIEW_CLIENT_NAME} is the client app. It requests pay statements and equity grants for career-move compensation planning. The request names the client, the purpose, and the exact streams.`, surface: "protocol", }, { - id: "consent", headline: "Consent fixes the boundary", + id: "consent", narrative: "The consent surface shows the client, streams, and enforced boundary before approval.", surface: "human", }, { - id: "grant", headline: "The grant makes it durable", + id: "grant", narrative: "Approval becomes a grant with exact streams, fields, access mode, and time window.", surface: "protocol", }, { - id: "enforce", headline: "Only the granted fields come back", + id: "enforce", narrative: `${LONGVIEW_CLIENT_NAME} queries pay statements. The server returns the four granted comparison fields and leaves the identity-heavy payroll fields behind.`, surface: "protocol", }, { - id: "sync", headline: "Only what changed", + id: "sync", narrative: "On the next payroll cycle, one new pay statement lands. Longview syncs again and gets only the new record.", surface: "protocol", }, { - id: "revoke", headline: "Access is revocable", + id: "revoke", narrative: `One click revokes the grant. The next query from ${LONGVIEW_CLIENT_NAME} receives a refusal within 60 seconds.`, surface: "human", }, { - id: "export", headline: "Self-export remains full-fidelity", + id: "export", narrative: "Owner access can retrieve full records at any time. Every field, every stream, no third-party grant required.", surface: "human", }, { - id: "multi", headline: "One protocol across platforms", + id: "multi", narrative: "Compensation planning is one reference world. Subscription review, travel reimbursement, and benefits disputes can use the same grant-and-enforcement model across different platforms and deployment paths.", surface: "neutral", }, { - id: "spec", headline: "Built on an open specification", + id: "spec", narrative: "Every component on this page implements a section of the PDPP specification. Published, versioned, and open for review.", surface: "neutral", @@ -215,17 +215,17 @@ const SECTION_CONTENT = [ // ─── Stepper navigation ───────────────────────────────────────────────────── const SECTION_TEMPERATURE: Record = { - ingest: "protocol", - inventory: "protocol", - request: "protocol", consent: "human", - grant: "protocol", enforce: "protocol", - sync: "protocol", - revoke: "human", export: "human", + grant: "protocol", + ingest: "protocol", + inventory: "protocol", multi: "neutral", + request: "protocol", + revoke: "human", spec: "neutral", + sync: "protocol", }; function Stepper({ @@ -284,7 +284,7 @@ function GrantStatusRow({ phase }: { phase: ProtocolPhase }) {
Grant: {grantLabel} @@ -428,9 +428,9 @@ function FieldProjection({ grantedFields, allFields }: { grantedFields: string[] backgroundColor: granted ? "var(--success-wash)" : "var(--muted)", color: granted ? "var(--success)" : "var(--muted-foreground)", opacity, + textDecoration: dimmedByFilter ? "line-through" : "none", transform, transition: `opacity ${dur}ms ${easeOut} ${phase === "hidden" ? i * stagger : stagger * 4}ms, transform ${dur}ms ${easeOut} ${phase === "hidden" ? i * stagger : stagger * 4}ms`, - textDecoration: dimmedByFilter ? "line-through" : "none", }} > {f} @@ -447,11 +447,11 @@ function FieldProjection({ grantedFields, allFields }: { grantedFields: string[] style={{ backgroundColor: phase === "filter" || phase === "result" ? "var(--authorship-protocol-accent)" : "var(--border)", - opacity: phase === "hidden" ? 0 : 1, boxShadow: phase === "filter" || phase === "result" ? "0 0 8px color-mix(in oklab, var(--authorship-protocol-accent) 55%, transparent)" : "none", + opacity: phase === "hidden" ? 0 : 1, transition: `opacity ${dur}ms ${easeOut} ${stagger * 9}ms, background-color ${dur}ms ${easeOut}, box-shadow ${dur}ms ${easeOut}`, }} /> @@ -473,11 +473,11 @@ function FieldProjection({ grantedFields, allFields }: { grantedFields: string[] style={{ backgroundColor: phase === "filter" || phase === "result" ? "var(--authorship-protocol-accent)" : "var(--border)", - opacity: phase === "hidden" ? 0 : 1, boxShadow: phase === "filter" || phase === "result" ? "0 0 8px color-mix(in oklab, var(--authorship-protocol-accent) 55%, transparent)" : "none", + opacity: phase === "hidden" ? 0 : 1, transition: `opacity ${dur}ms ${easeOut} ${stagger * 9}ms, background-color ${dur}ms ${easeOut}, box-shadow ${dur}ms ${easeOut}`, }} /> @@ -568,8 +568,8 @@ function IncrementalSync() { backgroundColor: "var(--primary)", opacity: phase === "hidden" ? 0 : 0.6, transform: phase === "hidden" ? "scaleY(0)" : "scaleY(1)", - transition: `opacity 200ms ${i * 30}ms, transform 200ms ${i * 30}ms`, transformOrigin: "bottom", + transition: `opacity 200ms ${i * 30}ms, transform 200ms ${i * 30}ms`, }} /> ))} @@ -630,8 +630,8 @@ function IncrementalSync() { backgroundColor: "var(--success)", opacity: phase === "delta" ? 1 : 0, transform: phase === "delta" ? "scaleY(1)" : "scaleY(0)", - transition: `opacity 220ms ${420 + i * 80}ms, transform 220ms ${420 + i * 80}ms`, transformOrigin: "bottom", + transition: `opacity 220ms ${420 + i * 80}ms, transform 220ms ${420 + i * 80}ms`, }} /> ))} @@ -698,7 +698,7 @@ function TokenIntrospection({ introspection }: { introspection: ClientIntrospect RS reads first
-
+        
           {"token="}
         
@@ -742,7 +742,7 @@ function OutcomeCard({
@@ -872,10 +872,10 @@ function CollectionConvergence() { {/* Connecting line */}
@@ -907,10 +907,10 @@ function CollectionConvergence() { >
@@ -919,8 +919,8 @@ function CollectionConvergence() { style={{ color: "var(--foreground)", fontWeight: 600, - whiteSpace: "nowrap", paddingLeft: "0.75rem", + whiteSpace: "nowrap", }} > Structured records @@ -932,8 +932,8 @@ function CollectionConvergence() {

@@ -1076,8 +1076,8 @@ function DefaultReferenceHero() { className="rounded px-2 py-0.5 font-mono text-xs" style={{ backgroundColor: "var(--primary-wash)", - color: "var(--primary)", border: "1px solid color-mix(in oklab, var(--primary) 34%, var(--border))", + color: "var(--primary)", }} > PDPP @@ -1088,7 +1088,7 @@ function DefaultReferenceHero() {

Granular access
@@ -1203,24 +1203,24 @@ function deriveGrantInspectorProps( protocolGrant: NonNullable["grant"]> ): GrantInspectorProps { return { + accessMode: protocolGrant.access_mode, + client: { clientId: protocolGrant.client_id, name: LONGVIEW_CLIENT_NAME }, + expiresAt: protocolGrant.expires_at ?? null, grantId: protocolGrant.grant_id, issuedAt: protocolGrant.issued_at, - status: protocolGrant.status, - client: { clientId: protocolGrant.client_id, name: LONGVIEW_CLIENT_NAME }, purposeCode: protocolGrant.purpose_code, purposeDescription: protocolGrant.purpose_description, - accessMode: protocolGrant.access_mode, - expiresAt: protocolGrant.expires_at ?? null, retention: protocolGrant.retention ? { duration: "90 days", onExpiry: protocolGrant.retention.on_expiry } : undefined, + status: protocolGrant.status, streams: protocolGrant.streams.map((s) => ({ - name: s.name, - label: INVENTORY_SPECIMEN.streams.find((stream) => stream.name === s.name)?.label || s.name, detail: INVENTORY_SPECIMEN.streams.find((stream) => stream.name === s.name)?.detail, fields: s.fields || undefined, - view: s.view || undefined, + label: INVENTORY_SPECIMEN.streams.find((stream) => stream.name === s.name)?.label || s.name, + name: s.name, timeRange: s.time_range || undefined, + view: s.view || undefined, })), }; } @@ -1242,8 +1242,8 @@ function StickyHeader({
@@ -1275,7 +1275,7 @@ function StickyHeader({
Grant: {grantLabel} @@ -1456,52 +1456,52 @@ export function ReferenceApp({ hero, currentLabel = "Reference" }: ReferenceAppP
{[ { + color: "var(--primary)", + detail: "{ collection_mode, state, bindings }", dir: "→", from: "Runtime", - to: "Connector", msg: "START", - detail: "{ collection_mode, state, bindings }", - color: "var(--primary)", + to: "Connector", }, { + color: "var(--success)", + detail: '{ stream: "pay_statements", key: "pay_0", data: {...} }', dir: "←", from: "Connector", - to: "Runtime", msg: "RECORD", - detail: '{ stream: "pay_statements", key: "pay_0", data: {...} }', - color: "var(--success)", + to: "Runtime", }, { + color: "var(--success)", + detail: '{ stream: "pay_statements", key: "pay_1", data: {...} }', dir: "←", from: "Connector", - to: "Runtime", msg: "RECORD", - detail: '{ stream: "pay_statements", key: "pay_1", data: {...} }', - color: "var(--success)", + to: "Runtime", }, { + color: "var(--warning)", + detail: '{ cursor: "..." }', dir: "←", from: "Connector", - to: "Runtime", msg: "STATE", - detail: '{ cursor: "..." }', - color: "var(--warning)", + to: "Runtime", }, { + color: "var(--success)", + detail: "...24 records total", dir: "←", from: "Connector", - to: "Runtime", msg: "RECORD", - detail: "...24 records total", - color: "var(--success)", + to: "Runtime", }, { + color: "var(--primary)", + detail: '{ status: "succeeded", records_emitted: 24 }', dir: "←", from: "Connector", - to: "Runtime", msg: "DONE", - detail: '{ status: "succeeded", records_emitted: 24 }', - color: "var(--primary)", + to: "Runtime", }, ].map((m, i, arr) => (
({ - name: s.name, - label: INVENTORY_SPECIMEN.streams.find((stream) => stream.name === s.name)?.label || s.name, detail: INVENTORY_SPECIMEN.streams.find((is) => is.name === s.name)?.detail || "", + label: INVENTORY_SPECIMEN.streams.find((stream) => stream.name === s.name)?.label || s.name, + lastSynced: "Apr 15, 2026", + name: s.name, + recordCount: s.recordCount, semantics: INVENTORY_SPECIMEN.streams.find((stream) => stream.name === s.name)?.semantics || "append_only", - recordCount: s.recordCount, - lastSynced: "Apr 15, 2026", }))} /> @@ -1747,46 +1747,46 @@ Content-Type: application/json
{[ { + color: "var(--primary)", element: '"Pay statements", "Equity grants"', source: "Manifest display.label (manifest-authored)", trust: "Data description", - color: "var(--primary)", }, { + color: "var(--primary)", element: '"Employer, pay period, gross pay..."', source: "Manifest display.detail (manifest-authored, never client)", trust: "Data description", - color: "var(--primary)", }, { + color: "var(--success)", element: "Streams · fields · access_mode", source: "grant constraints (AS/RS enforce)", trust: "Protocol-enforced", - color: "var(--success)", }, { + color: "var(--success)", element: '"Ongoing access until revocation" / "One-time"', source: "grant.access_mode (server-derived)", trust: "Protocol-enforced", - color: "var(--success)", }, { + color: "var(--warning)", element: "Purpose · retention", source: "purpose_code, purpose_description, retention", trust: "Policy declaration", - color: "var(--warning)", }, { + color: "var(--human)", element: `"${LONGVIEW_CLIENT_NAME} says: Workspace only..."`, source: "client_claims.commitments (request-scoped)", trust: "Attributed client claim", - color: "var(--human)", }, { + color: "var(--primary)", element: `"${LONGVIEW_CLIENT_NAME}" + CLIENT APP + VERIFIED badge`, source: "client_display (entity-scoped) resolved under AS policy", trust: "Identity metadata", - color: "var(--primary)", }, ].map((row, i, arr) => (
{JSON.stringify( { + access_mode: protocol.grant.access_mode, + client: { client_id: protocol.grant.client_id }, + expires_at: protocol.grant.expires_at, grant_id: protocol.grant.grant_id, issued_at: protocol.grant.issued_at, - status: protocol.grant.status, - client: { client_id: protocol.grant.client_id }, purpose_code: protocol.grant.purpose_code, - access_mode: protocol.grant.access_mode, + retention: protocol.grant.retention, + status: protocol.grant.status, streams: protocol.grant.streams.map((s) => ({ - name: s.name, fields: s.fields, + name: s.name, view: s.view, })), - retention: protocol.grant.retention, - expires_at: protocol.grant.expires_at, }, null, 2 @@ -2318,18 +2318,18 @@ Authorization: Bearer
{[ { + desc: "Token introspection, field projection, effective filter composition", ref: "Enforce", spec: "§8 Resource Server", - desc: "Token introspection, field projection, effective filter composition", }, - { ref: "Request", spec: "§5 Selection Request", desc: "RFC 9396 authorization_details envelope" }, - { ref: "Consent", spec: "§5.1, §5.2", desc: "Client display, client claims, attribution" }, - { ref: "Grant", spec: "§6 Grant", desc: "Immutable consent artifact with three time axes" }, - { ref: "Sync", spec: "§4.1 Incremental", desc: "Projection-aware deltas via changes_since" }, - { ref: "Revoke", spec: "§6.5 Revocation", desc: "60s propagation window, retention governs past data" }, - { ref: "Export", spec: "§8.3 Owner Tokens", desc: "Self-export via owner token, no grant required" }, - { ref: "Inventory", spec: "§4 Record Model", desc: "Flat relational streams with primary keys" }, - { ref: "Ingest", spec: "§7 Manifest", desc: "Connector manifest declares the consent surface" }, + { desc: "RFC 9396 authorization_details envelope", ref: "Request", spec: "§5 Selection Request" }, + { desc: "Client display, client claims, attribution", ref: "Consent", spec: "§5.1, §5.2" }, + { desc: "Immutable consent artifact with three time axes", ref: "Grant", spec: "§6 Grant" }, + { desc: "Projection-aware deltas via changes_since", ref: "Sync", spec: "§4.1 Incremental" }, + { desc: "60s propagation window, retention governs past data", ref: "Revoke", spec: "§6.5 Revocation" }, + { desc: "Self-export via owner token, no grant required", ref: "Export", spec: "§8.3 Owner Tokens" }, + { desc: "Flat relational streams with primary keys", ref: "Inventory", spec: "§4 Record Model" }, + { desc: "Connector manifest declares the consent surface", ref: "Ingest", spec: "§7 Manifest" }, ].map(({ ref, spec, desc }) => (
-g when supported; otherwise fetch files from this catalog", + description: DATA_ACCESS_SKILL_DESCRIPTION, files: [ { - routePath: `${DATA_ACCESS_SKILL_NAME}/SKILL.md`, - repoRelativePath: `${DATA_ACCESS_SKILL_BASE_REPO_PATH}/SKILL.md`, mediaType: "text/markdown; charset=utf-8", + repoRelativePath: `${DATA_ACCESS_SKILL_BASE_REPO_PATH}/SKILL.md`, + routePath: `${DATA_ACCESS_SKILL_NAME}/SKILL.md`, }, { - routePath: `${DATA_ACCESS_SKILL_NAME}/references/grant-design.md`, - repoRelativePath: `${DATA_ACCESS_SKILL_BASE_REPO_PATH}/references/grant-design.md`, mediaType: "text/markdown; charset=utf-8", + repoRelativePath: `${DATA_ACCESS_SKILL_BASE_REPO_PATH}/references/grant-design.md`, + routePath: `${DATA_ACCESS_SKILL_NAME}/references/grant-design.md`, }, { - routePath: `${DATA_ACCESS_SKILL_NAME}/references/query-cookbook.md`, - repoRelativePath: `${DATA_ACCESS_SKILL_BASE_REPO_PATH}/references/query-cookbook.md`, mediaType: "text/markdown; charset=utf-8", + repoRelativePath: `${DATA_ACCESS_SKILL_BASE_REPO_PATH}/references/query-cookbook.md`, + routePath: `${DATA_ACCESS_SKILL_NAME}/references/query-cookbook.md`, }, { - routePath: `${DATA_ACCESS_SKILL_NAME}/references/security.md`, - repoRelativePath: `${DATA_ACCESS_SKILL_BASE_REPO_PATH}/references/security.md`, mediaType: "text/markdown; charset=utf-8", + repoRelativePath: `${DATA_ACCESS_SKILL_BASE_REPO_PATH}/references/security.md`, + routePath: `${DATA_ACCESS_SKILL_NAME}/references/security.md`, }, { - routePath: `${DATA_ACCESS_SKILL_NAME}/references/troubleshooting.md`, - repoRelativePath: `${DATA_ACCESS_SKILL_BASE_REPO_PATH}/references/troubleshooting.md`, mediaType: "text/markdown; charset=utf-8", + repoRelativePath: `${DATA_ACCESS_SKILL_BASE_REPO_PATH}/references/troubleshooting.md`, + routePath: `${DATA_ACCESS_SKILL_NAME}/references/troubleshooting.md`, }, ], + name: DATA_ACCESS_SKILL_NAME, + recommended_install: "npx skills add -g when supported; otherwise fetch files from this catalog", }, { - name: OWNER_AGENT_SKILL_NAME, - description: OWNER_AGENT_SKILL_DESCRIPTION, canonical_source: "docs/agent-skills", - recommended_install: "npx skills add -g when supported; otherwise fetch files from this catalog", + description: OWNER_AGENT_SKILL_DESCRIPTION, files: [ { - routePath: OWNER_AGENT_SKILL_ROUTE_PATH, - repoRelativePath: `${OWNER_AGENT_SKILL_BASE_REPO_PATH}/SKILL.md`, mediaType: "text/markdown; charset=utf-8", + repoRelativePath: `${OWNER_AGENT_SKILL_BASE_REPO_PATH}/SKILL.md`, + routePath: OWNER_AGENT_SKILL_ROUTE_PATH, }, ], + name: OWNER_AGENT_SKILL_NAME, + recommended_install: "npx skills add -g when supported; otherwise fetch files from this catalog", }, ]; @@ -140,10 +140,10 @@ export async function buildAgentSkillCatalog(origin: string): Promise => { const bytes = await readRepoFile(file.repoRelativePath); return { + bytes: bytes.byteLength, + media_type: file.mediaType, path: file.routePath, repo_path: file.repoRelativePath, - media_type: file.mediaType, - bytes: bytes.byteLength, sha256: sha256(bytes), url: catalogUrl(origin, file.routePath), }; @@ -155,8 +155,8 @@ export async function buildAgentSkillCatalog(origin: string): Promise ({ - id: connection.connectionId, displayName: deriveConnectionDisplayName({ connector, displayName: connection.displayName, - ordinal: index + 1, groupSize, + ordinal: index + 1, }), + id: connection.connectionId, })); } diff --git a/apps/site/src/lib/docs-shared.tsx b/apps/site/src/lib/docs-shared.tsx index dc650d844..91c3056fd 100644 --- a/apps/site/src/lib/docs-shared.tsx +++ b/apps/site/src/lib/docs-shared.tsx @@ -6,12 +6,12 @@ import { ThemeToggle } from "@/components/theme/theme-toggle.tsx"; export function baseOptions(): BaseLayoutProps { return { + githubUrl: "https://github.com/PDP-Connect/pdpp", nav: { // Title is rendered by SiteHeader above the docs layout; no sidebar title. title: null, url: "/", }, - githubUrl: "https://github.com/PDP-Connect/pdpp", themeSwitch: { component: , enabled: true, diff --git a/apps/site/src/lib/docs-source.ts b/apps/site/src/lib/docs-source.ts index 3ddb3677b..930c9c7b9 100644 --- a/apps/site/src/lib/docs-source.ts +++ b/apps/site/src/lib/docs-source.ts @@ -10,8 +10,8 @@ export const docsImageRoute = "/og/docs"; export const source = loader({ baseUrl: docsRoute, - source: docs.toFumadocsSource(), plugins: [lucideIconsPlugin()], + source: docs.toFumadocsSource(), }); export function getPageImage(page: InferPageType) { diff --git a/apps/site/src/lib/longview-world.ts b/apps/site/src/lib/longview-world.ts index 90add3103..b7df7febf 100644 --- a/apps/site/src/lib/longview-world.ts +++ b/apps/site/src/lib/longview-world.ts @@ -38,22 +38,22 @@ export const LONGVIEW_PAY_STATEMENT_ALL_FIELDS = [ export const LONGVIEW_PAY_STATEMENT_GRANTED_FIELDS = ["employer", "pay_period", "gross_pay", "net_pay"] as const; export const LONGVIEW_STREAM_DETAILS = { - payStatements: - "Employer, pay period, gross pay, and net pay from each payroll cycle. No bank account details, home address, or tax ID fragments.", - equityGrants: - "Grant type, quantity, vesting start, and vesting schedule. No brokerage account numbers or beneficiary details.", benefitsEnrollments: "Plan name, coverage tier, and employer contribution. No dependent details, claims, or provider notes.", + equityGrants: + "Grant type, quantity, vesting start, and vesting schedule. No brokerage account numbers or beneficiary details.", + payStatements: + "Employer, pay period, gross pay, and net pay from each payroll cycle. No bank account details, home address, or tax ID fragments.", } as const; export const LONGVIEW_CONNECTOR_SPECIMEN: ConnectorCardProps = { connectorId: "https://registry.pdpp.org/profiles/compensation-v1", displayName: "Compensation profile", - version: "1.0.0", + profiles: [{ id: "career-move", label: "Career move planning", streamCount: 3 }], streams: [ { - name: "pay_statements", label: "Pay statements", + name: "pay_statements", semantics: "append_only", supportsFields: true, supportsResources: false, @@ -61,8 +61,8 @@ export const LONGVIEW_CONNECTOR_SPECIMEN: ConnectorCardProps = { viewCount: 2, }, { - name: "equity_grants", label: "Equity grants", + name: "equity_grants", semantics: "mutable_state", supportsFields: true, supportsResources: false, @@ -70,8 +70,8 @@ export const LONGVIEW_CONNECTOR_SPECIMEN: ConnectorCardProps = { viewCount: 2, }, { - name: "benefits_enrollments", label: "Benefits enrollments", + name: "benefits_enrollments", semantics: "mutable_state", supportsFields: true, supportsResources: false, @@ -79,7 +79,7 @@ export const LONGVIEW_CONNECTOR_SPECIMEN: ConnectorCardProps = { viewCount: 1, }, ], - profiles: [{ id: "career-move", label: "Career move planning", streamCount: 3 }], + version: "1.0.0", }; export const LONGVIEW_INVENTORY_SPECIMEN: StreamInventoryProps = { @@ -87,103 +87,103 @@ export const LONGVIEW_INVENTORY_SPECIMEN: StreamInventoryProps = { connectorVersion: "1.0.0", streams: [ { - name: "pay_statements", - label: "Pay statements", detail: LONGVIEW_STREAM_DETAILS.payStatements, - semantics: "append_only", - recordCount: 24, + label: "Pay statements", lastSynced: "Apr 15, 2026", + name: "pay_statements", + recordCount: 24, + semantics: "append_only", }, { - name: "equity_grants", - label: "Equity grants", detail: LONGVIEW_STREAM_DETAILS.equityGrants, - semantics: "mutable_state", - recordCount: 3, + label: "Equity grants", lastSynced: "Apr 15, 2026", + name: "equity_grants", + recordCount: 3, + semantics: "mutable_state", }, { - name: "benefits_enrollments", - label: "Benefits enrollments", detail: LONGVIEW_STREAM_DETAILS.benefitsEnrollments, - semantics: "mutable_state", - recordCount: 1, + label: "Benefits enrollments", lastSynced: "Apr 15, 2026", + name: "benefits_enrollments", + recordCount: 1, + semantics: "mutable_state", }, ], }; export const LONGVIEW_CONSENT_SPECIMEN: ConsentCardProps = { + accessMode: "continuous", + commitments: [...LONGVIEW_COMMITMENTS], + optional: { + consequenceOff: "Leaves the rest of the compensation analysis intact.", + consequenceOn: "Improves plan comparison and exposes coverage tradeoffs.", + detail: LONGVIEW_STREAM_DETAILS.benefitsEnrollments, + key: "benefits_enrollments", + label: "Benefits enrollments", + }, + purpose: LONGVIEW_PURPOSE, requester: { - name: LONGVIEW_CLIENT_NAME, monogram: LONGVIEW_CLIENT_MONOGRAM, - verified: true, - uri: LONGVIEW_CLIENT_URI, + name: LONGVIEW_CLIENT_NAME, policyUri: LONGVIEW_POLICY_URI, tosUri: LONGVIEW_TOS_URI, + uri: LONGVIEW_CLIENT_URI, + verified: true, }, - purpose: LONGVIEW_PURPOSE, - commitments: [...LONGVIEW_COMMITMENTS], streams: [ { + detail: LONGVIEW_STREAM_DETAILS.payStatements, key: "pay_statements", label: "Pay statements", - detail: LONGVIEW_STREAM_DETAILS.payStatements, }, { + detail: LONGVIEW_STREAM_DETAILS.equityGrants, key: "equity_grants", label: "Equity grants", - detail: LONGVIEW_STREAM_DETAILS.equityGrants, }, ], - optional: { - key: "benefits_enrollments", - label: "Benefits enrollments", - detail: LONGVIEW_STREAM_DETAILS.benefitsEnrollments, - consequenceOn: "Improves plan comparison and exposes coverage tradeoffs.", - consequenceOff: "Leaves the rest of the compensation analysis intact.", - }, - accessMode: "continuous", technical: { clientId: LONGVIEW_CLIENT_ID, - purposeCode: LONGVIEW_PURPOSE_CODE, grantExpires: "Apr 15, 2027", + purposeCode: LONGVIEW_PURPOSE_CODE, }, }; export const LONGVIEW_GRANT_SPECIMEN: GrantInspectorProps = { + accessMode: "continuous", + client: { clientId: LONGVIEW_CLIENT_ID, name: LONGVIEW_CLIENT_NAME }, + expiresAt: "Apr 15, 2027", grantId: "grt_longview01", issuedAt: "Apr 15, 2026", - status: "active", - client: { clientId: LONGVIEW_CLIENT_ID, name: LONGVIEW_CLIENT_NAME }, purposeCode: LONGVIEW_PURPOSE_CODE, purposeDescription: LONGVIEW_PURPOSE_DESCRIPTION, - accessMode: "continuous", - expiresAt: "Apr 15, 2027", retention: { duration: "90 days", onExpiry: "delete" }, + status: "active", streams: [ { - name: "pay_statements", - label: "Pay statements", detail: LONGVIEW_STREAM_DETAILS.payStatements, - view: "summary", fields: [...LONGVIEW_PAY_STATEMENT_GRANTED_FIELDS], + label: "Pay statements", + name: "pay_statements", timeRange: { since: "Jan 1, 2025" }, + view: "summary", }, { - name: "equity_grants", - label: "Equity grants", detail: LONGVIEW_STREAM_DETAILS.equityGrants, - view: "vesting_summary", fields: ["grant_type", "quantity", "vesting_start", "vesting_schedule"], + label: "Equity grants", + name: "equity_grants", + view: "vesting_summary", }, ], }; export const LONGVIEW_ROLLOUT_COPY = { - heroLine: LONGVIEW_SUMMARY, - proofLine: "The app gets the comparison fields and leaves the identity-heavy payroll fields behind.", docsBlurb: "A compensation-planning client that needs payroll, equity, and benefits records under one enforceable consent boundary.", + heroLine: LONGVIEW_SUMMARY, + proofLine: "The app gets the comparison fields and leaves the identity-heavy payroll fields behind.", syncLine: "Each payroll cycle adds one new pay statement, so sync returns only the new record.", } as const; diff --git a/apps/site/src/lib/mock-server.ts b/apps/site/src/lib/mock-server.ts index b6d600cbd..24be4e029 100644 --- a/apps/site/src/lib/mock-server.ts +++ b/apps/site/src/lib/mock-server.ts @@ -112,30 +112,30 @@ export class MockPDPPServer { query(grantId: string, streamName: string): QueryResult { const grant = this.grants.get(grantId); if (!grant) { - return { status: 403, error: "grant_invalid", records: [], has_more: false }; + return { error: "grant_invalid", has_more: false, records: [], status: 403 }; } if (grant.status === "revoked") { - return { status: 403, error: "grant_revoked", records: [], has_more: false }; + return { error: "grant_revoked", has_more: false, records: [], status: 403 }; } if (grant.status === "expired") { - return { status: 403, error: "grant_expired", records: [], has_more: false }; + return { error: "grant_expired", has_more: false, records: [], status: 403 }; } // Check stream is in grant const grantStream = grant.streams.find((s) => s.name === streamName); if (!grantStream) { - return { status: 403, error: "insufficient_scope", records: [], has_more: false }; + return { error: "insufficient_scope", has_more: false, records: [], status: 403 }; } const stream = this.streams.get(streamName); if (!stream) { - return { status: 404, error: "stream_not_found", records: [], has_more: false }; + return { error: "stream_not_found", has_more: false, records: [], status: 404 }; } // Apply field projection const records = stream.records.map((r) => this.projectRecord(r, grantStream.fields, stream.schema_fields)); - return { status: 200, records, has_more: false }; + return { has_more: false, records, status: 200 }; } // ── Incremental sync (changes_since) ── @@ -143,36 +143,36 @@ export class MockPDPPServer { queryChangesSince(grantId: string, streamName: string, cursor?: string): QueryResult { const grant = this.grants.get(grantId); if (!grant) { - return { status: 403, error: "grant_invalid", records: [], has_more: false }; + return { error: "grant_invalid", has_more: false, records: [], status: 403 }; } if (grant.status === "revoked") { - return { status: 403, error: "grant_revoked", records: [], has_more: false }; + return { error: "grant_revoked", has_more: false, records: [], status: 403 }; } const grantStream = grant.streams.find((s) => s.name === streamName); if (!grantStream) { - return { status: 403, error: "insufficient_scope", records: [], has_more: false }; + return { error: "insufficient_scope", has_more: false, records: [], status: 403 }; } const stream = this.streams.get(streamName); if (!stream) { - return { status: 404, error: "stream_not_found", records: [], has_more: false }; + return { error: "stream_not_found", has_more: false, records: [], status: 404 }; } // Parse cursor (index into records array) const startIdx = cursor ? Number.parseInt(cursor, 10) : 0; if (Number.isNaN(startIdx)) { - return { status: 410, error: "cursor_expired", records: [], has_more: false }; + return { error: "cursor_expired", has_more: false, records: [], status: 410 }; } const newRecords = stream.records.slice(startIdx); const projected = newRecords.map((r) => this.projectRecord(r, grantStream.fields, stream.schema_fields)); return { - status: 200, - records: projected, has_more: false, next_changes_since: String(stream.records.length), + records: projected, + status: 200, }; } @@ -181,11 +181,11 @@ export class MockPDPPServer { selfExport(streamName: string): QueryResult { const stream = this.streams.get(streamName); if (!stream) { - return { status: 404, error: "stream_not_found", records: [], has_more: false }; + return { error: "stream_not_found", has_more: false, records: [], status: 404 }; } // Owner sees all fields, no projection - return { status: 200, records: [...stream.records], has_more: false }; + return { has_more: false, records: [...stream.records], status: 200 }; } // ── Field projection ── @@ -203,9 +203,9 @@ export class MockPDPPServer { } return { - key: record.key, data: projected, emitted_at: record.emitted_at, + key: record.key, }; } @@ -239,12 +239,12 @@ export class MockPDPPServer { const grant = this.grants.get(grantId); return { active: grant?.status === "active", - pdpp_token_kind: "client", + client_id: grant?.client_id ?? null, grant_id: grantId, grant_status: grant?.status ?? "unknown", - client_id: grant?.client_id ?? null, - subject_id: "user_abc123", + pdpp_token_kind: "client", scope_streams: grant?.streams.map((s) => s.name) ?? [], + subject_id: "user_abc123", }; } @@ -252,9 +252,9 @@ export class MockPDPPServer { getStreamStats(): { name: string; recordCount: number; fields: string[] }[] { return Array.from(this.streams.values()).map((s) => ({ + fields: s.schema_fields, name: s.name, recordCount: s.records.length, - fields: s.schema_fields, })); } } @@ -284,66 +284,66 @@ export function createSeededServer(): MockPDPPServer { server.addStream({ name: "pay_statements", - semantics: "append_only", - schema_fields: payStatementFields, records: Array.from({ length: 24 }, (_, i) => { const payDate = new Date(Date.UTC(2025, 0, 15 + i * 14)); const grossPay = 6150 + (i % 4) * 120; const netPay = grossPay - 1510 - (i % 3) * 35; return { - key: `pay_${i}`, data: { + bank_account_last4: "4821", + employee_id: `emp_${String(4100 + i).padStart(4, "0")}`, employer: "Northstar Labs", - pay_period: payDate.toISOString().slice(0, 10), gross_pay: grossPay, - net_pay: netPay, - employee_id: `emp_${String(4100 + i).padStart(4, "0")}`, home_address: "1207 W Maple Ave, Chicago, IL", - bank_account_last4: "4821", + net_pay: netPay, + pay_period: payDate.toISOString().slice(0, 10), tax_id_fragment: "2487", }, emitted_at: payDate.toISOString(), + key: `pay_${i}`, }; }), + schema_fields: payStatementFields, + semantics: "append_only", }); const grantTypes = ["ISO", "RSU", "NSO"] as const; const grantQuantities = [8000, 2400, 1200] as const; server.addStream({ name: "equity_grants", - semantics: "mutable_state", - schema_fields: equityGrantFields, records: Array.from({ length: 3 }, (_, i) => ({ - key: `grant_${i}`, data: { + beneficiary_name: "Primary beneficiary", + brokerage_account_last4: `71${i}4`, grant_type: grantTypes[i] ?? "NSO", quantity: grantQuantities[i] ?? 1200, - vesting_start: `202${i + 4}-05-01`, vesting_schedule: i === 1 ? "4y monthly after 1y cliff" : "4y quarterly after 1y cliff", - brokerage_account_last4: `71${i}4`, - beneficiary_name: "Primary beneficiary", + vesting_start: `202${i + 4}-05-01`, }, emitted_at: "2026-04-15T12:00:00Z", + key: `grant_${i}`, })), + schema_fields: equityGrantFields, + semantics: "mutable_state", }); server.addStream({ name: "benefits_enrollments", - semantics: "mutable_state", - schema_fields: benefitsFields, records: Array.from({ length: 1 }, () => ({ - key: "benefits_0", data: { - plan_name: "Blue Horizon PPO", + claims_vendor: "Northstar Health Services", coverage_tier: "Employee + spouse", - employer_contribution: 840, - effective_date: "2026-01-01", dependent_count: 1, - claims_vendor: "Northstar Health Services", + effective_date: "2026-01-01", + employer_contribution: 840, + plan_name: "Blue Horizon PPO", }, emitted_at: "2026-01-01T12:00:00Z", + key: "benefits_0", })), + schema_fields: benefitsFields, + semantics: "mutable_state", }); return server; diff --git a/apps/site/src/lib/openspec/parse.ts b/apps/site/src/lib/openspec/parse.ts index 691a5ccca..085a538b0 100644 --- a/apps/site/src/lib/openspec/parse.ts +++ b/apps/site/src/lib/openspec/parse.ts @@ -71,7 +71,7 @@ function readParagraph(lines: string[], from: number): { paragraph: string[]; ne paragraph.push((lines[i] ?? "").trim()); i++; } - return { paragraph, next: i }; + return { next: i, paragraph }; } export function extractExcerpt(markdown: string): string | null { diff --git a/apps/site/src/lib/purpose-docs.ts b/apps/site/src/lib/purpose-docs.ts index b9ea50361..97e83d8fa 100644 --- a/apps/site/src/lib/purpose-docs.ts +++ b/apps/site/src/lib/purpose-docs.ts @@ -20,10 +20,27 @@ export interface PurposeDoc { } export const PDPP_PURPOSE_DOCS: Record = { + "https://pdpp.org/purpose/ai_training": { + description: + "Training or fine-tuning machine learning models. Requires explicit affirmative consent and may persist beyond a single session.", + label: "ai_training", + max_retention: "P3Y", + permitted_uses: [ + "Training or fine-tuning ML recommendation models", + "Improving content ranking and personalization", + ], + prohibited_uses: [ + "Advertising targeting", + "Resale or transfer to third parties", + "Identity verification or background checks", + ], + requires_explicit_consent: true, + uri: "https://pdpp.org/purpose/ai_training", + }, "https://pdpp.org/purpose/research": { - uri: "https://pdpp.org/purpose/research", - label: "research", description: "Academic or market research that produces aggregate, non-identifying findings.", + label: "research", + max_retention: "P1Y", permitted_uses: [ "Aggregate statistical analysis", "Network topology and influence studies", @@ -35,24 +52,7 @@ export const PDPP_PURPOSE_DOCS: Record = { "Resale or transfer to third parties", "Individual-level profiling", ], - max_retention: "P1Y", requires_explicit_consent: false, - }, - "https://pdpp.org/purpose/ai_training": { - uri: "https://pdpp.org/purpose/ai_training", - label: "ai_training", - description: - "Training or fine-tuning machine learning models. Requires explicit affirmative consent and may persist beyond a single session.", - permitted_uses: [ - "Training or fine-tuning ML recommendation models", - "Improving content ranking and personalization", - ], - prohibited_uses: [ - "Advertising targeting", - "Resale or transfer to third parties", - "Identity verification or background checks", - ], - max_retention: "P3Y", - requires_explicit_consent: true, + uri: "https://pdpp.org/purpose/research", }, }; diff --git a/apps/site/src/lib/seed-data.ts b/apps/site/src/lib/seed-data.ts index dc4e16c60..636a9ab7d 100644 --- a/apps/site/src/lib/seed-data.ts +++ b/apps/site/src/lib/seed-data.ts @@ -34,112 +34,112 @@ export interface SeedAdTargeting { // ~106 realistic following accounts — mix of creators, brands, personal contacts export const SEED_FOLLOWING: SeedAccount[] = [ - { id: "25025320", username: "natgeo", full_name: "National Geographic", is_verified: true }, - { id: "9527", username: "instagram", full_name: "Instagram", is_verified: true }, - { id: "3", username: "kevin", full_name: "Kevin Systrom", is_verified: true }, - { id: "302577", username: "taylorswift", full_name: "Taylor Swift", is_verified: true }, - { id: "12281", username: "snoopdogg", full_name: "Snoop Dogg", is_verified: true }, - { id: "200428992", username: "nasa", full_name: "NASA", is_verified: true }, - { id: "167009509", username: "nytimes", full_name: "The New York Times", is_verified: true }, - { id: "5765028", username: "stripe", full_name: "Stripe", is_verified: true }, - { id: "29187493", username: "linear", full_name: "Linear", is_verified: false }, - { id: "183012668", username: "figma", full_name: "Figma", is_verified: true }, - { id: "258061269", username: "vercel", full_name: "Vercel", is_verified: false }, - { id: "422931568", username: "anthropic", full_name: "Anthropic", is_verified: false }, - { id: "544030737", username: "openai", full_name: "OpenAI", is_verified: false }, - { id: "7320543", username: "paulg", full_name: "Paul Graham", is_verified: false }, - { id: "58611", username: "sama", full_name: "Sam Altman", is_verified: true }, - { id: "9129", username: "jack", full_name: "Jack Dorsey", is_verified: true }, - { id: "1284161", username: "elonmusk", full_name: "Elon Musk", is_verified: true }, - { id: "1166413", username: "sundarpichai", full_name: "Sundar Pichai", is_verified: true }, - { id: "174119690", username: "jessicaalba", full_name: "Jessica Alba", is_verified: true }, - { id: "25025368", username: "kyliejenner", full_name: "Kylie Jenner", is_verified: true }, - { id: "460563723", username: "mrbeast", full_name: "MrBeast", is_verified: true }, - { id: "7824459", username: "mkbhd", full_name: "Marques Brownlee", is_verified: true }, - { id: "21938291", username: "lexfridman", full_name: "Lex Fridman", is_verified: true }, - { id: "44278982", username: "hubermanlab", full_name: "Andrew Huberman", is_verified: true }, - { id: "320695120", username: "timferriss", full_name: "the owner Ferriss", is_verified: true }, - { id: "79299064", username: "paulocoelho", full_name: "Paulo Coelho", is_verified: true }, - { id: "55793", username: "neiltyson", full_name: "Neil deGrasse Tyson", is_verified: true }, - { id: "7536", username: "obama", full_name: "Barack Obama", is_verified: true }, - { id: "47269647", username: "espn", full_name: "ESPN", is_verified: true }, - { id: "29639178", username: "nba", full_name: "NBA", is_verified: true }, - { id: "19610161", username: "fcbarcelona", full_name: "FC Barcelona", is_verified: true }, - { id: "18479", username: "realmadrid", full_name: "Real Madrid C.F.", is_verified: true }, - { id: "48029851", username: "cristiano", full_name: "Cristiano Ronaldo", is_verified: true }, - { id: "197807526", username: "leomessi", full_name: "Leo Messi", is_verified: true }, - { id: "8539862", username: "kingjames", full_name: "LeBron James", is_verified: true }, - { id: "29524236", username: "wired", full_name: "WIRED", is_verified: true }, - { id: "1642911", username: "theverge", full_name: "The Verge", is_verified: true }, - { id: "4177889", username: "techcrunch", full_name: "TechCrunch", is_verified: false }, - { id: "9617533", username: "arstechnica", full_name: "Ars Technica", is_verified: false }, - { id: "188201606", username: "hbomax", full_name: "Max", is_verified: true }, - { id: "25024955", username: "netflix", full_name: "Netflix", is_verified: true }, - { id: "5765822", username: "spotify", full_name: "Spotify", is_verified: true }, - { id: "17841400008460746", username: "apple", full_name: "Apple", is_verified: true }, - { id: "429047592", username: "google", full_name: "Google", is_verified: true }, - { id: "3214607", username: "microsoft", full_name: "Microsoft", is_verified: true }, - { id: "244994040", username: "notion", full_name: "Notion", is_verified: false }, - { id: "1234567891", username: "alex.chen.sf", full_name: "Alex Chen", is_verified: false }, - { id: "1234567892", username: "priya.dev", full_name: "Priya Nair", is_verified: false }, - { id: "1234567893", username: "marco.builds", full_name: "Marco Rossi", is_verified: false }, - { id: "1234567894", username: "sarah.ux", full_name: "Sarah Kim", is_verified: false }, - { id: "1234567895", username: "david.writes", full_name: "David Osei", is_verified: false }, - { id: "1234567896", username: "julia.codes", full_name: "Julia Santos", is_verified: false }, - { id: "1234567897", username: "raj.ventures", full_name: "Raj Patel", is_verified: false }, - { id: "1234567898", username: "emma.photo", full_name: "Emma Wilson", is_verified: false }, - { id: "1234567899", username: "carlos.design", full_name: "Carlos Rivera", is_verified: false }, - { id: "1234567900", username: "nina.creates", full_name: "Nina Johansson", is_verified: false }, - { id: "1234567901", username: "tom.ships", full_name: "Tom Bradley", is_verified: false }, - { id: "1234567902", username: "yuki.art", full_name: "Yuki Tanaka", is_verified: false }, - { id: "1234567903", username: "kate.pm", full_name: "Kate Morrison", is_verified: false }, - { id: "1234567904", username: "felix.ai", full_name: "Felix Weber", is_verified: false }, - { id: "1234567905", username: "a person.vc", full_name: "a person Lindström", is_verified: false }, - { id: "1234567906", username: "ben.infra", full_name: "Ben Okafor", is_verified: false }, - { id: "1234567907", username: "chloe.writes", full_name: "Chloe Martin", is_verified: false }, - { id: "1234567908", username: "james.growth", full_name: "James Park", is_verified: false }, - { id: "1234567909", username: "sofia.founder", full_name: "Sofia García", is_verified: false }, - { id: "1234567910", username: "luke.ml", full_name: "Luke Thompson", is_verified: false }, - { id: "1234567911", username: "mia.product", full_name: "Mia Schneider", is_verified: false }, - { id: "1234567912", username: "oliver.sec", full_name: "Oliver Hughes", is_verified: false }, - { id: "1234567913", username: "zoe.research", full_name: "Zoe Dubois", is_verified: false }, - { id: "1234567914", username: "amir.eng", full_name: "Amir Hassan", is_verified: false }, - { id: "1234567915", username: "lily.content", full_name: "Lily Chen", is_verified: false }, - { id: "1234567916", username: "noah.startup", full_name: "Noah Björk", is_verified: false }, - { id: "1234567917", username: "eva.data", full_name: "Eva Müller", is_verified: false }, - { id: "1234567918", username: "ivan.builds", full_name: "Ivan Petrov", is_verified: false }, - { id: "1234567919", username: "grace.ux", full_name: "Grace Yamamoto", is_verified: false }, - { id: "1234567920", username: "henry.protocol", full_name: "Henry Adeyemi", is_verified: false }, - { id: "1234567921", username: "iris.photo", full_name: "Iris Larsson", is_verified: false }, - { id: "1234567922", username: "daniel.crypto", full_name: "Daniel Nkosi", is_verified: false }, - { id: "1234567923", username: "maya.bio", full_name: "Maya Krishnan", is_verified: false }, - { id: "1234567924", username: "ethan.web3", full_name: "Ethan Clarke", is_verified: false }, - { id: "1234567925", username: "leila.ops", full_name: "Leila Ahmadi", is_verified: false }, - { id: "1234567926", username: "oscar.3d", full_name: "Oscar Fernández", is_verified: false }, - { id: "1234567927", username: "nora.climate", full_name: "Nora Andersen", is_verified: false }, - { id: "1234567928", username: "ryan.devrel", full_name: "Ryan O'Brien", is_verified: false }, - { id: "1234567929", username: "sophie.brand", full_name: "Sophie Leclerc", is_verified: false }, - { id: "1234567930", username: "max.infosec", full_name: "Max Bergmann", is_verified: false }, - { id: "1234567931", username: "ava.writes", full_name: "Ava Johansson", is_verified: false }, - { id: "1234567932", username: "leo.api", full_name: "Leo Tremblay", is_verified: false }, - { id: "1234567933", username: "isla.design", full_name: "Isla MacLeod", is_verified: false }, - { id: "1234567934", username: "kai.devops", full_name: "Kai Nakamura", is_verified: false }, - { id: "1234567935", username: "violet.pm", full_name: "Violet Kowalski", is_verified: false }, - { id: "1234567936", username: "sam.infra", full_name: "Sam Okonkwo", is_verified: false }, - { id: "1234567937", username: "mira.ml", full_name: "Mira Svensson", is_verified: false }, - { id: "1234567938", username: "caleb.open", full_name: "Caleb Figueiredo", is_verified: false }, - { id: "1234567939", username: "luna.artist", full_name: "Luna external person", is_verified: false }, - { id: "1234567940", username: "eli.embedded", full_name: "Eli Shapiro", is_verified: false }, - { id: "1234567941", username: "freya.health", full_name: "Freya Nilsson", is_verified: false }, - { id: "1234567942", username: "hugo.lang", full_name: "Hugo Renaud", is_verified: false }, - { id: "1234567943", username: "sera.vc", full_name: "Sera Inoue", is_verified: false }, - { id: "1234567944", username: "finn.builds", full_name: "Finn Larsen", is_verified: false }, - { id: "1234567945", username: "stella.edu", full_name: "Stella Moreau", is_verified: false }, - { id: "1234567946", username: "jasper.sound", full_name: "Jasper van der Berg", is_verified: false }, - { id: "1234567947", username: "penny.ops", full_name: "Penny Nguyen", is_verified: false }, - { id: "1234567948", username: "remy.systems", full_name: "Rémy Dupont", is_verified: false }, - { id: "1234567949", username: "wren.photo", full_name: "Wren Callahan", is_verified: false }, - { id: "1234567950", username: "cole.protocol", full_name: "Cole Adebayo", is_verified: false }, + { full_name: "National Geographic", id: "25025320", is_verified: true, username: "natgeo" }, + { full_name: "Instagram", id: "9527", is_verified: true, username: "instagram" }, + { full_name: "Kevin Systrom", id: "3", is_verified: true, username: "kevin" }, + { full_name: "Taylor Swift", id: "302577", is_verified: true, username: "taylorswift" }, + { full_name: "Snoop Dogg", id: "12281", is_verified: true, username: "snoopdogg" }, + { full_name: "NASA", id: "200428992", is_verified: true, username: "nasa" }, + { full_name: "The New York Times", id: "167009509", is_verified: true, username: "nytimes" }, + { full_name: "Stripe", id: "5765028", is_verified: true, username: "stripe" }, + { full_name: "Linear", id: "29187493", is_verified: false, username: "linear" }, + { full_name: "Figma", id: "183012668", is_verified: true, username: "figma" }, + { full_name: "Vercel", id: "258061269", is_verified: false, username: "vercel" }, + { full_name: "Anthropic", id: "422931568", is_verified: false, username: "anthropic" }, + { full_name: "OpenAI", id: "544030737", is_verified: false, username: "openai" }, + { full_name: "Paul Graham", id: "7320543", is_verified: false, username: "paulg" }, + { full_name: "Sam Altman", id: "58611", is_verified: true, username: "sama" }, + { full_name: "Jack Dorsey", id: "9129", is_verified: true, username: "jack" }, + { full_name: "Elon Musk", id: "1284161", is_verified: true, username: "elonmusk" }, + { full_name: "Sundar Pichai", id: "1166413", is_verified: true, username: "sundarpichai" }, + { full_name: "Jessica Alba", id: "174119690", is_verified: true, username: "jessicaalba" }, + { full_name: "Kylie Jenner", id: "25025368", is_verified: true, username: "kyliejenner" }, + { full_name: "MrBeast", id: "460563723", is_verified: true, username: "mrbeast" }, + { full_name: "Marques Brownlee", id: "7824459", is_verified: true, username: "mkbhd" }, + { full_name: "Lex Fridman", id: "21938291", is_verified: true, username: "lexfridman" }, + { full_name: "Andrew Huberman", id: "44278982", is_verified: true, username: "hubermanlab" }, + { full_name: "the owner Ferriss", id: "320695120", is_verified: true, username: "timferriss" }, + { full_name: "Paulo Coelho", id: "79299064", is_verified: true, username: "paulocoelho" }, + { full_name: "Neil deGrasse Tyson", id: "55793", is_verified: true, username: "neiltyson" }, + { full_name: "Barack Obama", id: "7536", is_verified: true, username: "obama" }, + { full_name: "ESPN", id: "47269647", is_verified: true, username: "espn" }, + { full_name: "NBA", id: "29639178", is_verified: true, username: "nba" }, + { full_name: "FC Barcelona", id: "19610161", is_verified: true, username: "fcbarcelona" }, + { full_name: "Real Madrid C.F.", id: "18479", is_verified: true, username: "realmadrid" }, + { full_name: "Cristiano Ronaldo", id: "48029851", is_verified: true, username: "cristiano" }, + { full_name: "Leo Messi", id: "197807526", is_verified: true, username: "leomessi" }, + { full_name: "LeBron James", id: "8539862", is_verified: true, username: "kingjames" }, + { full_name: "WIRED", id: "29524236", is_verified: true, username: "wired" }, + { full_name: "The Verge", id: "1642911", is_verified: true, username: "theverge" }, + { full_name: "TechCrunch", id: "4177889", is_verified: false, username: "techcrunch" }, + { full_name: "Ars Technica", id: "9617533", is_verified: false, username: "arstechnica" }, + { full_name: "Max", id: "188201606", is_verified: true, username: "hbomax" }, + { full_name: "Netflix", id: "25024955", is_verified: true, username: "netflix" }, + { full_name: "Spotify", id: "5765822", is_verified: true, username: "spotify" }, + { full_name: "Apple", id: "17841400008460746", is_verified: true, username: "apple" }, + { full_name: "Google", id: "429047592", is_verified: true, username: "google" }, + { full_name: "Microsoft", id: "3214607", is_verified: true, username: "microsoft" }, + { full_name: "Notion", id: "244994040", is_verified: false, username: "notion" }, + { full_name: "Alex Chen", id: "1234567891", is_verified: false, username: "alex.chen.sf" }, + { full_name: "Priya Nair", id: "1234567892", is_verified: false, username: "priya.dev" }, + { full_name: "Marco Rossi", id: "1234567893", is_verified: false, username: "marco.builds" }, + { full_name: "Sarah Kim", id: "1234567894", is_verified: false, username: "sarah.ux" }, + { full_name: "David Osei", id: "1234567895", is_verified: false, username: "david.writes" }, + { full_name: "Julia Santos", id: "1234567896", is_verified: false, username: "julia.codes" }, + { full_name: "Raj Patel", id: "1234567897", is_verified: false, username: "raj.ventures" }, + { full_name: "Emma Wilson", id: "1234567898", is_verified: false, username: "emma.photo" }, + { full_name: "Carlos Rivera", id: "1234567899", is_verified: false, username: "carlos.design" }, + { full_name: "Nina Johansson", id: "1234567900", is_verified: false, username: "nina.creates" }, + { full_name: "Tom Bradley", id: "1234567901", is_verified: false, username: "tom.ships" }, + { full_name: "Yuki Tanaka", id: "1234567902", is_verified: false, username: "yuki.art" }, + { full_name: "Kate Morrison", id: "1234567903", is_verified: false, username: "kate.pm" }, + { full_name: "Felix Weber", id: "1234567904", is_verified: false, username: "felix.ai" }, + { full_name: "a person Lindström", id: "1234567905", is_verified: false, username: "a person.vc" }, + { full_name: "Ben Okafor", id: "1234567906", is_verified: false, username: "ben.infra" }, + { full_name: "Chloe Martin", id: "1234567907", is_verified: false, username: "chloe.writes" }, + { full_name: "James Park", id: "1234567908", is_verified: false, username: "james.growth" }, + { full_name: "Sofia García", id: "1234567909", is_verified: false, username: "sofia.founder" }, + { full_name: "Luke Thompson", id: "1234567910", is_verified: false, username: "luke.ml" }, + { full_name: "Mia Schneider", id: "1234567911", is_verified: false, username: "mia.product" }, + { full_name: "Oliver Hughes", id: "1234567912", is_verified: false, username: "oliver.sec" }, + { full_name: "Zoe Dubois", id: "1234567913", is_verified: false, username: "zoe.research" }, + { full_name: "Amir Hassan", id: "1234567914", is_verified: false, username: "amir.eng" }, + { full_name: "Lily Chen", id: "1234567915", is_verified: false, username: "lily.content" }, + { full_name: "Noah Björk", id: "1234567916", is_verified: false, username: "noah.startup" }, + { full_name: "Eva Müller", id: "1234567917", is_verified: false, username: "eva.data" }, + { full_name: "Ivan Petrov", id: "1234567918", is_verified: false, username: "ivan.builds" }, + { full_name: "Grace Yamamoto", id: "1234567919", is_verified: false, username: "grace.ux" }, + { full_name: "Henry Adeyemi", id: "1234567920", is_verified: false, username: "henry.protocol" }, + { full_name: "Iris Larsson", id: "1234567921", is_verified: false, username: "iris.photo" }, + { full_name: "Daniel Nkosi", id: "1234567922", is_verified: false, username: "daniel.crypto" }, + { full_name: "Maya Krishnan", id: "1234567923", is_verified: false, username: "maya.bio" }, + { full_name: "Ethan Clarke", id: "1234567924", is_verified: false, username: "ethan.web3" }, + { full_name: "Leila Ahmadi", id: "1234567925", is_verified: false, username: "leila.ops" }, + { full_name: "Oscar Fernández", id: "1234567926", is_verified: false, username: "oscar.3d" }, + { full_name: "Nora Andersen", id: "1234567927", is_verified: false, username: "nora.climate" }, + { full_name: "Ryan O'Brien", id: "1234567928", is_verified: false, username: "ryan.devrel" }, + { full_name: "Sophie Leclerc", id: "1234567929", is_verified: false, username: "sophie.brand" }, + { full_name: "Max Bergmann", id: "1234567930", is_verified: false, username: "max.infosec" }, + { full_name: "Ava Johansson", id: "1234567931", is_verified: false, username: "ava.writes" }, + { full_name: "Leo Tremblay", id: "1234567932", is_verified: false, username: "leo.api" }, + { full_name: "Isla MacLeod", id: "1234567933", is_verified: false, username: "isla.design" }, + { full_name: "Kai Nakamura", id: "1234567934", is_verified: false, username: "kai.devops" }, + { full_name: "Violet Kowalski", id: "1234567935", is_verified: false, username: "violet.pm" }, + { full_name: "Sam Okonkwo", id: "1234567936", is_verified: false, username: "sam.infra" }, + { full_name: "Mira Svensson", id: "1234567937", is_verified: false, username: "mira.ml" }, + { full_name: "Caleb Figueiredo", id: "1234567938", is_verified: false, username: "caleb.open" }, + { full_name: "Luna external person", id: "1234567939", is_verified: false, username: "luna.artist" }, + { full_name: "Eli Shapiro", id: "1234567940", is_verified: false, username: "eli.embedded" }, + { full_name: "Freya Nilsson", id: "1234567941", is_verified: false, username: "freya.health" }, + { full_name: "Hugo Renaud", id: "1234567942", is_verified: false, username: "hugo.lang" }, + { full_name: "Sera Inoue", id: "1234567943", is_verified: false, username: "sera.vc" }, + { full_name: "Finn Larsen", id: "1234567944", is_verified: false, username: "finn.builds" }, + { full_name: "Stella Moreau", id: "1234567945", is_verified: false, username: "stella.edu" }, + { full_name: "Jasper van der Berg", id: "1234567946", is_verified: false, username: "jasper.sound" }, + { full_name: "Penny Nguyen", id: "1234567947", is_verified: false, username: "penny.ops" }, + { full_name: "Rémy Dupont", id: "1234567948", is_verified: false, username: "remy.systems" }, + { full_name: "Wren Callahan", id: "1234567949", is_verified: false, username: "wren.photo" }, + { full_name: "Cole Adebayo", id: "1234567950", is_verified: false, username: "cole.protocol" }, ]; // Generate posts over the past 6 months (before consent date — all should be time-gated away) @@ -151,219 +151,206 @@ function daysAgo(n: number): string { export const SEED_POSTS: SeedPost[] = [ { - id: "p001", - shortcode: "CxA1b2c3d4", caption: "Building something that makes data work for people, not the other way around.", - like_count: 847, comment_count: 42, - taken_at: daysAgo(180), + id: "p001", + like_count: 847, media_type: "IMAGE", + shortcode: "CxA1b2c3d4", + taken_at: daysAgo(180), }, { - id: "p002", - shortcode: "CxB2c3d4e5", caption: "Late night debugging sessions hit different when the bug turns out to be a semicolon 🙃", - like_count: 1203, comment_count: 67, - taken_at: daysAgo(172), + id: "p002", + like_count: 1203, media_type: "IMAGE", + shortcode: "CxB2c3d4e5", + taken_at: daysAgo(172), }, { - id: "p003", - shortcode: "CxC3d4e5f6", caption: "If you own your data, you own your narrative. That's the whole point.", - like_count: 2891, comment_count: 134, - taken_at: daysAgo(161), + id: "p003", + like_count: 2891, media_type: "IMAGE", + shortcode: "CxC3d4e5f6", + taken_at: daysAgo(161), }, { - id: "p004", - shortcode: "CxD4e5f6g7", caption: null, - like_count: 445, comment_count: 12, - taken_at: daysAgo(154), + id: "p004", + like_count: 445, media_type: "VIDEO", + shortcode: "CxD4e5f6g7", + taken_at: daysAgo(154), }, { - id: "p005", - shortcode: "CxE5f6g7h8", caption: "The internet was supposed to empower individuals. We're not done building that.", - like_count: 3102, comment_count: 208, - taken_at: daysAgo(143), + id: "p005", + like_count: 3102, media_type: "CAROUSEL_ALBUM", + shortcode: "CxE5f6g7h8", + taken_at: daysAgo(143), }, { - id: "p006", - shortcode: "CxF6g7h8i9", caption: "Shipped something I'm genuinely proud of today. More soon.", - like_count: 1876, comment_count: 91, - taken_at: daysAgo(132), + id: "p006", + like_count: 1876, media_type: "IMAGE", + shortcode: "CxF6g7h8i9", + taken_at: daysAgo(132), }, { - id: "p007", - shortcode: "CxG7h8i9j0", caption: "The best privacy isn't hiding — it's having control.", - like_count: 4210, comment_count: 317, - taken_at: daysAgo(121), + id: "p007", + like_count: 4210, media_type: "IMAGE", + shortcode: "CxG7h8i9j0", + taken_at: daysAgo(121), }, { - id: "p008", - shortcode: "CxH8i9j0k1", caption: null, - like_count: 672, comment_count: 28, - taken_at: daysAgo(113), + id: "p008", + like_count: 672, media_type: "VIDEO", + shortcode: "CxH8i9j0k1", + taken_at: daysAgo(113), }, { - id: "p009", - shortcode: "CxI9j0k1l2", caption: "Consent that means something. Working on it.", - like_count: 2341, comment_count: 156, - taken_at: daysAgo(102), + id: "p009", + like_count: 2341, media_type: "IMAGE", + shortcode: "CxI9j0k1l2", + taken_at: daysAgo(102), }, { - id: "p010", - shortcode: "CxJ0k1l2m3", caption: "Three principles: transparency, control, portability. Simple to say. Hard to build.", - like_count: 5678, comment_count: 421, - taken_at: daysAgo(94), + id: "p010", + like_count: 5678, media_type: "CAROUSEL_ALBUM", + shortcode: "CxJ0k1l2m3", + taken_at: daysAgo(94), }, { - id: "p011", - shortcode: "CxK1l2m3n4", caption: null, - like_count: 523, comment_count: 19, - taken_at: daysAgo(87), + id: "p011", + like_count: 523, media_type: "IMAGE", + shortcode: "CxK1l2m3n4", + taken_at: daysAgo(87), }, { - id: "p012", - shortcode: "CxL2m3n4o5", caption: "Your data tells a story. You should be the author.", - like_count: 3891, comment_count: 267, - taken_at: daysAgo(79), + id: "p012", + like_count: 3891, media_type: "IMAGE", + shortcode: "CxL2m3n4o5", + taken_at: daysAgo(79), }, { - id: "p013", - shortcode: "CxM3n4o5p6", caption: "Protocol design is hard. Not because of the technical parts.", - like_count: 1123, comment_count: 78, - taken_at: daysAgo(71), + id: "p013", + like_count: 1123, media_type: "VIDEO", + shortcode: "CxM3n4o5p6", + taken_at: daysAgo(71), }, { - id: "p014", - shortcode: "CxN4o5p6q7", caption: "Standards matter because they outlast any single product.", - like_count: 2456, comment_count: 189, - taken_at: daysAgo(62), + id: "p014", + like_count: 2456, media_type: "IMAGE", + shortcode: "CxN4o5p6q7", + taken_at: daysAgo(62), }, { - id: "p015", - shortcode: "CxO5p6q7r8", caption: null, - like_count: 891, comment_count: 34, - taken_at: daysAgo(55), + id: "p015", + like_count: 891, media_type: "CAROUSEL_ALBUM", + shortcode: "CxO5p6q7r8", + taken_at: daysAgo(55), }, { - id: "p016", - shortcode: "CxP6q7r8s9", caption: "Data sovereignty is a technical problem. Glad people are finally treating it that way.", - like_count: 6234, comment_count: 512, - taken_at: daysAgo(46), + id: "p016", + like_count: 6234, media_type: "IMAGE", + shortcode: "CxP6q7r8s9", + taken_at: daysAgo(46), }, { - id: "p017", - shortcode: "CxQ7r8s9t0", caption: "Ship. Learn. Repeat. The protocol is getting sharper.", - like_count: 1567, comment_count: 93, - taken_at: daysAgo(38), + id: "p017", + like_count: 1567, media_type: "IMAGE", + shortcode: "CxQ7r8s9t0", + taken_at: daysAgo(38), }, { - id: "p018", - shortcode: "CxR8s9t0u1", caption: null, - like_count: 734, comment_count: 22, - taken_at: daysAgo(31), + id: "p018", + like_count: 734, media_type: "VIDEO", + shortcode: "CxR8s9t0u1", + taken_at: daysAgo(31), }, { - id: "p019", - shortcode: "CxS9t0u1v2", caption: "The consent screen is the most important UX on the internet. Nobody treats it that way.", - like_count: 8901, comment_count: 678, - taken_at: daysAgo(23), + id: "p019", + like_count: 8901, media_type: "IMAGE", + shortcode: "CxS9t0u1v2", + taken_at: daysAgo(23), }, { - id: "p020", - shortcode: "CxT0u1v2w3", caption: "v0.1. Rough around the edges. But the core is solid.", - like_count: 3214, comment_count: 241, - taken_at: daysAgo(15), + id: "p020", + like_count: 3214, media_type: "IMAGE", + shortcode: "CxT0u1v2w3", + taken_at: daysAgo(15), }, { - id: "p021", - shortcode: "CxU1v2w3x4", caption: null, - like_count: 912, comment_count: 41, - taken_at: daysAgo(8), + id: "p021", + like_count: 912, media_type: "CAROUSEL_ALBUM", + shortcode: "CxU1v2w3x4", + taken_at: daysAgo(8), }, { - id: "p022", - shortcode: "CxV2w3x4y5", caption: "Every data request should come with: who, what, why, how long. Non-negotiable.", - like_count: 4523, comment_count: 334, - taken_at: daysAgo(3), + id: "p022", + like_count: 4523, media_type: "IMAGE", + shortcode: "CxV2w3x4y5", + taken_at: daysAgo(3), }, ]; export const SEED_AD_TARGETING: SeedAdTargeting = { - record_id: "ad_record_1", - topics: [ - "Technology & Computing", - "Software Development", - "Artificial Intelligence", - "Open Source", - "Entrepreneurship", - "Privacy & Security", - "Venture Capital", - "Product Design", - "Cryptocurrency", - "Science & Innovation", - ], advertisers: [ "GitHub", "JetBrains", @@ -400,15 +387,28 @@ export const SEED_AD_TARGETING: SeedAdTargeting = { "Health & Fitness", "News & Current Events", ], + record_id: "ad_record_1", + topics: [ + "Technology & Computing", + "Software Development", + "Artificial Intelligence", + "Open Source", + "Entrepreneurship", + "Privacy & Security", + "Venture Capital", + "Product Design", + "Cryptocurrency", + "Science & Innovation", + ], }; // Profile data export const SEED_PROFILE = { - username: "dondochaka", - full_name: "the owner", bio: "Building the personal data stack. Protocol nerd.", follower_count: 3847, following_count: SEED_FOLLOWING.length, + full_name: "the owner", is_verified: false, profile_pic_url: "", + username: "dondochaka", }; diff --git a/apps/site/src/lib/seed-gmail.ts b/apps/site/src/lib/seed-gmail.ts index 7e96aac92..f9f403332 100644 --- a/apps/site/src/lib/seed-gmail.ts +++ b/apps/site/src/lib/seed-gmail.ts @@ -16,224 +16,224 @@ function daysAgo(n: number): string { export const SEED_GMAIL_THREADS = [ // ── Commerce ────────────────────────────────────────────────────────────── { - id: "g001", - subject: "Your order has shipped — #13849", from: "no-reply@amazon.com", from_name: "Amazon", + id: "g001", labels: ["inbox", "commerce"], received_at: daysAgo(1), + subject: "Your order has shipped — #13849", thread_count: 2, }, { - id: "g002", - subject: "Your receipt from Stripe", from: "receipts@stripe.com", from_name: "Stripe", + id: "g002", labels: ["inbox", "commerce"], received_at: daysAgo(3), + subject: "Your receipt from Stripe", thread_count: 1, }, { - id: "g003", - subject: "Your Figma invoice #INV-2841", from: "billing@figma.com", from_name: "Figma", + id: "g003", labels: ["commerce"], received_at: daysAgo(8), + subject: "Your Figma invoice #INV-2841", thread_count: 1, }, { - id: "g004", - subject: "New order confirmation — Vercel", from: "billing@vercel.com", from_name: "Vercel", + id: "g004", labels: ["commerce"], received_at: daysAgo(12), + subject: "New order confirmation — Vercel", thread_count: 1, }, { - id: "g005", - subject: "Your GitHub subscription", from: "billing@github.com", from_name: "GitHub", + id: "g005", labels: ["commerce"], received_at: daysAgo(22), + subject: "Your GitHub subscription", thread_count: 2, }, // ── Work / collaboration ─────────────────────────────────────────────────── { - id: "g006", - subject: "PR review: feat/pdpp-consent-flow", from: "noreply@github.com", from_name: "GitHub", + id: "g006", labels: ["inbox", "work"], received_at: daysAgo(0), + subject: "PR review: feat/pdpp-consent-flow", thread_count: 5, }, { - id: "g007", - subject: "Linear: Sprint 14 planning", from: "notifications@linear.app", from_name: "Linear", + id: "g007", labels: ["inbox", "work"], received_at: daysAgo(2), + subject: "Linear: Sprint 14 planning", thread_count: 3, }, { - id: "g008", - subject: 'Loom recording shared: "Demo walkthrough"', from: "notify@loom.com", from_name: "Loom", + id: "g008", labels: ["inbox", "work"], received_at: daysAgo(4), + subject: 'Loom recording shared: "Demo walkthrough"', thread_count: 1, }, { - id: "g009", - subject: "[Figma] Alex Rowe commented on Design tokens", from: "noreply@figma.com", from_name: "Figma", + id: "g009", labels: ["work"], received_at: daysAgo(6), + subject: "[Figma] Alex Rowe commented on Design tokens", thread_count: 4, }, { - id: "g010", - subject: "Invite to join the Acme workspace", from: "hi@acme.example", from_name: "Acme", + id: "g010", labels: ["inbox", "work"], received_at: daysAgo(9), + subject: "Invite to join the Acme workspace", thread_count: 1, }, // ── Auth / security ──────────────────────────────────────────────────────── { - id: "g011", - subject: "New sign-in from Chrome on macOS", from: "no-reply@accounts.google.com", from_name: "Google", + id: "g011", labels: ["security"], received_at: daysAgo(1), + subject: "New sign-in from Chrome on macOS", thread_count: 1, }, { - id: "g012", - subject: "Your verification code: 847 291", from: "security@instagram.com", from_name: "Instagram", + id: "g012", labels: ["security"], received_at: daysAgo(5), + subject: "Your verification code: 847 291", thread_count: 1, }, { - id: "g013", - subject: "Someone tried to sign in to your account", from: "noreply@github.com", from_name: "GitHub", + id: "g013", labels: ["security"], received_at: daysAgo(14), + subject: "Someone tried to sign in to your account", thread_count: 1, }, // ── Newsletters ─────────────────────────────────────────────────────────── { - id: "g014", - subject: "The Pragmatic Engineer: AI and the software job market", from: "pragmatic@substack.com", from_name: "The Pragmatic Engineer", + id: "g014", labels: ["newsletters"], received_at: daysAgo(2), + subject: "The Pragmatic Engineer: AI and the software job market", thread_count: 1, }, { - id: "g015", - subject: "TLDR: OpenAI launches new reasoning model", from: "dan@tldrnewsletter.com", from_name: "TLDR Newsletter", + id: "g015", labels: ["newsletters"], received_at: daysAgo(3), + subject: "TLDR: OpenAI launches new reasoning model", thread_count: 1, }, { - id: "g016", - subject: "Lenny's Newsletter: How Stripe builds product", from: "lenny@substack.com", from_name: "Lenny's Newsletter", + id: "g016", labels: ["newsletters"], received_at: daysAgo(7), + subject: "Lenny's Newsletter: How Stripe builds product", thread_count: 1, }, { - id: "g017", - subject: "Bytes.dev: What's new in React 19", from: "ui@bytes.dev", from_name: "Bytes.dev", + id: "g017", labels: ["newsletters"], received_at: daysAgo(10), + subject: "Bytes.dev: What's new in React 19", thread_count: 1, }, // ── Travel ──────────────────────────────────────────────────────────────── { - id: "g018", - subject: "Your Airbnb booking confirmed — San Francisco", from: "automated@airbnb.com", from_name: "Airbnb", + id: "g018", labels: ["travel"], received_at: daysAgo(15), + subject: "Your Airbnb booking confirmed — San Francisco", thread_count: 3, }, { - id: "g019", - subject: "Booking confirmation: SF → NYC, May 3", from: "noreply@united.com", from_name: "United Airlines", + id: "g019", labels: ["travel"], received_at: daysAgo(18), + subject: "Booking confirmation: SF → NYC, May 3", thread_count: 2, }, // ── Personal ────────────────────────────────────────────────────────────── { - id: "g020", - subject: "Re: dinner Saturday?", from: "alex.chen@gmail.com", from_name: "Alex Chen", + id: "g020", labels: ["inbox"], received_at: daysAgo(1), + subject: "Re: dinner Saturday?", thread_count: 6, }, { - id: "g021", - subject: "Photo album: ski trip 🎿", from: "photos@google.com", from_name: "Google Photos", + id: "g021", labels: ["inbox"], received_at: daysAgo(4), + subject: "Photo album: ski trip 🎿", thread_count: 1, }, { - id: "g022", - subject: "Re: apartment lease renewal", from: "landlord@realty.com", from_name: "Pacific Realty", + id: "g022", labels: ["inbox"], received_at: daysAgo(20), + subject: "Re: apartment lease renewal", thread_count: 4, }, ]; /** Summary of what the connector collected — displayed in the reference fixture */ export const GMAIL_SUMMARY = { - total_threads: SEED_GMAIL_THREADS.length, label_counts: { - inbox: SEED_GMAIL_THREADS.filter((t) => t.labels.includes("inbox")).length, commerce: SEED_GMAIL_THREADS.filter((t) => t.labels.includes("commerce")).length, - work: SEED_GMAIL_THREADS.filter((t) => t.labels.includes("work")).length, + inbox: SEED_GMAIL_THREADS.filter((t) => t.labels.includes("inbox")).length, newsletters: SEED_GMAIL_THREADS.filter((t) => t.labels.includes("newsletters")).length, - travel: SEED_GMAIL_THREADS.filter((t) => t.labels.includes("travel")).length, security: SEED_GMAIL_THREADS.filter((t) => t.labels.includes("security")).length, + travel: SEED_GMAIL_THREADS.filter((t) => t.labels.includes("travel")).length, + work: SEED_GMAIL_THREADS.filter((t) => t.labels.includes("work")).length, }, + total_threads: SEED_GMAIL_THREADS.length, }; diff --git a/apps/site/src/lib/spec-refs.ts b/apps/site/src/lib/spec-refs.ts index 058f731b2..4c7c9797d 100644 --- a/apps/site/src/lib/spec-refs.ts +++ b/apps/site/src/lib/spec-refs.ts @@ -9,19 +9,19 @@ export interface SpecRef { } export const SPEC = { - introduction: { label: "§1 Introduction", url: `${BASE}/spec-core#introduction` }, - selectionRequest: { label: "§5 Selection Request", url: `${BASE}/spec-core#selection-request` }, + accessModes: { label: "§6.2 Access modes", url: `${BASE}/spec-core#access-modes` }, aiTrainingConsent: { label: "§5.1 AI Training Consent", url: `${BASE}/spec-core#ai-training-consent` }, + collectionProfile: { label: "§Collection Profile", url: `${BASE}/spec-collection-profile` }, + conformance: { label: "§9 Conformance", url: `${BASE}/spec-core#conformance` }, + dataMinimization: { label: "§10.3 Data minimization", url: `${BASE}/spec-core#data-minimization` }, + errors: { label: "§8.3 Errors", url: `${BASE}/spec-core#errors` }, grant: { label: "§6 Grant", url: `${BASE}/spec-core#grant` }, - accessModes: { label: "§6.2 Access modes", url: `${BASE}/spec-core#access-modes` }, + introduction: { label: "§1 Introduction", url: `${BASE}/spec-core#introduction` }, + listRecords: { label: "§8.1 List records", url: `${BASE}/spec-core#list-records` }, manifestFormat: { label: "§7 Manifest Format", url: `${BASE}/spec-core#manifest-format` }, - views: { label: "§7.3 Views", url: `${BASE}/spec-core#views` }, resourceServer: { label: "§8 Resource Server", url: `${BASE}/spec-core#resource-server-interface` }, - listRecords: { label: "§8.1 List records", url: `${BASE}/spec-core#list-records` }, - streamMetadata: { label: "§8.2 Stream metadata", url: `${BASE}/spec-core#stream-metadata` }, - errors: { label: "§8.3 Errors", url: `${BASE}/spec-core#errors` }, - conformance: { label: "§9 Conformance", url: `${BASE}/spec-core#conformance` }, - dataMinimization: { label: "§10.3 Data minimization", url: `${BASE}/spec-core#data-minimization` }, revocation: { label: "§10 Revocation", url: `${BASE}/spec-core#revocation` }, - collectionProfile: { label: "§Collection Profile", url: `${BASE}/spec-collection-profile` }, + selectionRequest: { label: "§5 Selection Request", url: `${BASE}/spec-core#selection-request` }, + streamMetadata: { label: "§8.2 Stream metadata", url: `${BASE}/spec-core#stream-metadata` }, + views: { label: "§7.3 Views", url: `${BASE}/spec-core#views` }, } as const satisfies Record; diff --git a/apps/site/src/lib/use-protocol.ts b/apps/site/src/lib/use-protocol.ts index f0a5ed9fb..1196a1bce 100644 --- a/apps/site/src/lib/use-protocol.ts +++ b/apps/site/src/lib/use-protocol.ts @@ -36,26 +36,26 @@ export interface ProtocolState { } const GRANT_TEMPLATE = { + access_mode: "continuous" as const, + client_id: LONGVIEW_CLIENT_ID, + expires_at: "2027-04-15T00:00:00Z", grant_id: "grt_longview01", issued_at: "2026-04-15T15:00:00Z", - client_id: LONGVIEW_CLIENT_ID, purpose_code: LONGVIEW_PURPOSE_CODE, purpose_description: LONGVIEW_PURPOSE_DESCRIPTION, - access_mode: "continuous" as const, - expires_at: "2027-04-15T00:00:00Z", retention: { max_duration: "P90D", on_expiry: "delete" as const }, streams: [ { - name: "pay_statements", fields: [...LONGVIEW_PAY_STATEMENT_GRANTED_FIELDS], - view: "summary", + name: "pay_statements", time_range: { since: "2025-01-01" }, + view: "summary", }, { - name: "equity_grants", fields: ["grant_type", "quantity", "vesting_start", "vesting_schedule"], - view: "vesting_summary", + name: "equity_grants", time_range: null, + view: "vesting_summary", }, ], }; @@ -126,18 +126,18 @@ export function useProtocol() { const netPay = grossPay - 1540; server.addRecord("pay_statements", { - key: `pay_new_${idx}`, data: { + bank_account_last4: "4821", + employee_id: `emp_${String(5124 + i).padStart(4, "0")}`, employer: "Northstar Labs", - pay_period: payDate.toISOString().slice(0, 10), gross_pay: grossPay, - net_pay: netPay, - employee_id: `emp_${String(5124 + i).padStart(4, "0")}`, home_address: "1207 W Maple Ave, Chicago, IL", - bank_account_last4: "4821", + net_pay: netPay, + pay_period: payDate.toISOString().slice(0, 10), tax_id_fragment: "2487", }, emitted_at: payDate.toISOString(), + key: `pay_new_${idx}`, }); } @@ -176,19 +176,19 @@ export function useProtocol() { const introspection: ClientIntrospection = server.introspectClientToken(grant?.grant_id ?? GRANT_TEMPLATE.grant_id); return { - phase, + addNewPayStatements, + approve, + deny, + exportResult, grant, introspection, + phase, queryResult, - syncResult, - syncCursor, - exportResult, - serverStats, - approve, - deny, + reset, revoke, - addNewPayStatements, selfExport, - reset, + serverStats, + syncCursor, + syncResult, }; } diff --git a/deploy/railway/core-first-boot.mjs b/deploy/railway/core-first-boot.mjs index 54075df2e..2f419223d 100644 --- a/deploy/railway/core-first-boot.mjs +++ b/deploy/railway/core-first-boot.mjs @@ -42,7 +42,9 @@ const LOG_PREFIX = "[railway-core]"; const BANNER_RULE = "─".repeat(64); function trimmedValue(value) { - if (typeof value !== "string") return null; + if (typeof value !== "string") { + return null; + } const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : null; } @@ -63,13 +65,19 @@ export function resolveDataDir(env = process.env) { // PDPP_STORAGE_BACKEND wins, otherwise a database URL selects Postgres. function usesPostgresStorage(env) { const explicit = trimmedValue(env.PDPP_STORAGE_BACKEND)?.toLowerCase(); - if (explicit === "postgres") return true; - if (explicit === "sqlite") return false; + if (explicit === "postgres") { + return true; + } + if (explicit === "sqlite") { + return false; + } return Boolean(trimmedValue(env.PDPP_DATABASE_URL) ?? trimmedValue(env.DATABASE_URL)); } function readPersistedSecret(file) { - if (!existsSync(file)) return null; + if (!existsSync(file)) { + return null; + } try { return trimmedValue(readFileSync(file, "utf8")); } catch { @@ -145,9 +153,11 @@ export function prepareFirstBoot({ } if ( - !usesPostgresStorage(env) && - !trimmedValue(env.PDPP_CREDENTIAL_ENCRYPTION_KEY) && - !trimmedValue(env.PDPP_CREDENTIAL_ENCRYPTION_KEY_FILE) + !( + usesPostgresStorage(env) || + trimmedValue(env.PDPP_CREDENTIAL_ENCRYPTION_KEY) || + trimmedValue(env.PDPP_CREDENTIAL_ENCRYPTION_KEY_FILE) + ) ) { const keyFile = path.join(dataDir, CREDENTIAL_KEY_FILENAME); try { @@ -165,5 +175,5 @@ export function prepareFirstBoot({ } } - return { env: envAdditions, bannerLines }; + return { bannerLines, env: envAdditions }; } diff --git a/deploy/railway/core-supervisor.mjs b/deploy/railway/core-supervisor.mjs index a69ecabda..027d964a5 100644 --- a/deploy/railway/core-supervisor.mjs +++ b/deploy/railway/core-supervisor.mjs @@ -65,18 +65,18 @@ const referenceEnv = { ...process.env, ...firstBoot.env, AS_PORT: "7662", - RS_PORT: "7663", PDPP_AS_URL: "http://127.0.0.1:7662", PDPP_RS_URL: "http://127.0.0.1:7663", + RS_PORT: "7663", }; const consoleEnv = { ...process.env, ...firstBoot.env, HOSTNAME: process.env.HOSTNAME || "0.0.0.0", - PORT: process.env.PORT || "3000", PDPP_AS_URL: "http://127.0.0.1:7662", PDPP_RS_URL: "http://127.0.0.1:7663", + PORT: process.env.PORT || "3000", }; for (const line of firstBoot.bannerLines) { diff --git a/docs/design-system/ink-carbon/project/Ink Carbon - System Sheet.html b/docs/design-system/ink-carbon/project/Ink Carbon - System Sheet.html index 0f7443570..93a020417 100644 --- a/docs/design-system/ink-carbon/project/Ink Carbon - System Sheet.html +++ b/docs/design-system/ink-carbon/project/Ink Carbon - System Sheet.html @@ -7,31 +7,31 @@ @@ -64,8 +64,9 @@

Ink Carbon

The committed PDPP design system. The carbon duplicate marks what the server retains for you; - the typed voice separates what the protocol writes from what humans read; color is spent, never - sprayed; paper is square. Full doctrine in README.md — these are the live cards. + the typed voice + separates what the protocol writes from what humans read; color is spent, never sprayed; + paper is square. Full doctrine in README.md — these are the live cards.

diff --git a/docs/design-system/ink-carbon/project/PDPP Round 3.html b/docs/design-system/ink-carbon/project/PDPP Round 3.html index e8e51a81e..8a0b7b28a 100644 --- a/docs/design-system/ink-carbon/project/PDPP Round 3.html +++ b/docs/design-system/ink-carbon/project/PDPP Round 3.html @@ -29,9 +29,9 @@ diff --git a/docs/design-system/ink-carbon/project/PDPP Vibe Studies v2.html b/docs/design-system/ink-carbon/project/PDPP Vibe Studies v2.html index b6e45471a..e51e38707 100644 --- a/docs/design-system/ink-carbon/project/PDPP Vibe Studies v2.html +++ b/docs/design-system/ink-carbon/project/PDPP Vibe Studies v2.html @@ -30,9 +30,9 @@ diff --git a/docs/design-system/ink-carbon/project/PDPP Vibe Studies.html b/docs/design-system/ink-carbon/project/PDPP Vibe Studies.html index 0ad6d44ff..473837761 100644 --- a/docs/design-system/ink-carbon/project/PDPP Vibe Studies.html +++ b/docs/design-system/ink-carbon/project/PDPP Vibe Studies.html @@ -30,9 +30,9 @@ diff --git a/docs/design-system/ink-carbon/project/Recordroom - Console.html b/docs/design-system/ink-carbon/project/Recordroom - Console.html index 13a15d859..268ae695d 100644 --- a/docs/design-system/ink-carbon/project/Recordroom - Console.html +++ b/docs/design-system/ink-carbon/project/Recordroom - Console.html @@ -47,9 +47,9 @@ diff --git a/docs/design-system/ink-carbon/project/Watcher - What Longview Can See.html b/docs/design-system/ink-carbon/project/Watcher - What Longview Can See.html index 23319ce49..db80233fd 100644 --- a/docs/design-system/ink-carbon/project/Watcher - What Longview Can See.html +++ b/docs/design-system/ink-carbon/project/Watcher - What Longview Can See.html @@ -10,13 +10,13 @@ - +
diff --git a/docs/design-system/ink-carbon/project/assets/logo-mark.svg b/docs/design-system/ink-carbon/project/assets/logo-mark.svg index 9d14f92d2..90f62fb9c 100644 --- a/docs/design-system/ink-carbon/project/assets/logo-mark.svg +++ b/docs/design-system/ink-carbon/project/assets/logo-mark.svg @@ -1,14 +1,14 @@ - - + + P diff --git a/docs/design-system/ink-carbon/project/assets/wordmark.svg b/docs/design-system/ink-carbon/project/assets/wordmark.svg index 1f8e6d794..38400dc50 100644 --- a/docs/design-system/ink-carbon/project/assets/wordmark.svg +++ b/docs/design-system/ink-carbon/project/assets/wordmark.svg @@ -1,34 +1,34 @@ - - + + P PDPP v0.1.0 diff --git a/docs/design-system/ink-carbon/project/colors_and_type.css b/docs/design-system/ink-carbon/project/colors_and_type.css index 841f926db..b53bc9a91 100644 --- a/docs/design-system/ink-carbon/project/colors_and_type.css +++ b/docs/design-system/ink-carbon/project/colors_and_type.css @@ -171,20 +171,20 @@ /* ─── Base ────────────────────────────────────────────────────── */ html { - background: var(--background); - color: var(--foreground); + scrollbar-gutter: stable; font-family: var(--font-sans); -webkit-font-smoothing: antialiased; + color: var(--foreground); + background: var(--background); text-rendering: optimizeLegibility; - scrollbar-gutter: stable; } body { margin: 0; + color: var(--foreground); /* Flat paper. No corner washes, no top-of-page gradient — the page is a document, and documents do not glow. */ background: var(--background); - color: var(--foreground); } *, @@ -233,13 +233,13 @@ samp { colored fingernail. */ [data-surface="human"] { - border: 1px solid var(--border); background-color: color-mix(in oklab, var(--card) 95%, var(--human)); + border: 1px solid var(--border); } [data-surface="protocol"] { - border: 1px solid var(--border); background-color: color-mix(in oklab, var(--card) 95%, var(--primary)); + border: 1px solid var(--border); } [data-surface="stage"] { @@ -262,13 +262,13 @@ samp { position: relative; } .pdpp-carbon::before { - content: ""; position: absolute; inset: 0; - transform: translate(var(--carbon-offset), var(--carbon-offset)); + pointer-events: none; + content: ""; background: var(--carbon-fill); border: 1px solid var(--carbon-outline); - pointer-events: none; + transform: translate(var(--carbon-offset), var(--carbon-offset)); } .pdpp-carbon > * { position: relative; @@ -340,25 +340,25 @@ samp { font-family: var(--font-mono); font-size: 0.8125rem; /* 13px */ font-weight: 500; - line-height: 1.55; font-variant-numeric: tabular-nums; + line-height: 1.55; } .pdpp-typed-sm { font-family: var(--font-mono); font-size: 0.6875rem; /* 11px */ font-weight: 400; - line-height: 1.5; font-variant-numeric: tabular-nums; + line-height: 1.5; } .pdpp-eyebrow { font-family: var(--font-mono); font-size: 0.67rem; font-weight: 500; - letter-spacing: 0.12em; - text-transform: uppercase; color: var(--primary); + text-transform: uppercase; + letter-spacing: 0.12em; } /* ─── Helpers ─────────────────────────────────────────────────── */ diff --git a/docs/design-system/ink-carbon/project/explorer/app.jsx b/docs/design-system/ink-carbon/project/explorer/app.jsx index 74c568958..d8de9eb68 100644 --- a/docs/design-system/ink-carbon/project/explorer/app.jsx +++ b/docs/design-system/ink-carbon/project/explorer/app.jsx @@ -16,8 +16,8 @@ const { fmtRelative, CAP_GLYPH, CAP_LABEL, NOW } = window.PDPPPrim; const APP_DEFAULTS = /*EDITMODE-BEGIN*/ { - projectionDefault: false, defaultMode: "lex", + projectionDefault: false, } /*EDITMODE-END*/; function App() { @@ -42,11 +42,8 @@ e.preventDefault(); document.querySelector(".exp-query__text")?.focus(); } - if (e.key === "Escape") { - if (selected) { - setSelected(null); - return; - } + if (e.key === "Escape" && selected) { + setSelected(null); } } window.addEventListener("keydown", onKey); @@ -61,7 +58,9 @@ // or multi-stream (route to unified feed). const uniqueStreams = useMemo(() => { const s = new Map(); - for (const h of hits) s.set(`${h.stream.connection_id}::${h.stream.name}`, h.stream); + for (const h of hits) { + s.set(`${h.stream.connection_id}::${h.stream.name}`, h.stream); + } return [...s.values()]; }, [hits]); @@ -84,7 +83,7 @@ // ── Handlers ───────────────────────────────────────────────────── function openRecord(stream, record) { - setSelected({ stream, record }); + setSelected({ record, stream }); } function clearQuery() { setQuery({ chips: [], text: "" }); @@ -121,10 +120,10 @@ records: singleStream.records.filter((r) => hitIds.has(r.id)), }; const viewProps = { - stream: filteredStream, - selectedId: selected?.record?.id, onSelect: (r) => openRecord(filteredStream, r), projection, + selectedId: selected?.record?.id, + stream: filteredStream, }; switch (activeView) { case "table": @@ -235,9 +234,9 @@ setMode(v); }} options={[ - { value: "lex", label: "Lexical" }, - { value: "sem", label: "Semantic" }, - { value: "hyb", label: "Hybrid" }, + { label: "Lexical", value: "lex" }, + { label: "Semantic", value: "sem" }, + { label: "Hybrid", value: "hyb" }, ]} value={tweaks.defaultMode} /> @@ -264,12 +263,16 @@ // — there's nothing useful to switch to, so hide the chrome entirely. // The switcher only appears once the user has narrowed to one stream // and the stream's capability views can light up. - if (isFeed) return null; + if (isFeed) { + return null; + } return ( -
+
{window.PDPP_DISPATCH.VIEW_ORDER.map((cap) => { - if (!available.includes(cap)) return null; + if (!available.includes(cap)) { + return null; + } return ( @@ -312,13 +323,13 @@ function TweakSlider({ label, value, min = 0, max = 100, step = 1, unit = "", on return ( onChange(Number(e.target.value))} step={step} + type="range" value={value} - onChange={(e) => onChange(Number(e.target.value))} /> ); @@ -331,12 +342,12 @@ function TweakToggle({ label, value, onChange }) { {label}
@@ -367,9 +378,9 @@ function TweakRadio({ label, value, options, onChange }) { const m = options.find((o) => String(typeof o === "object" ? o.value : o) === s); return m === undefined ? s : typeof m === "object" ? m.value : m; }; - return onChange(resolve(s))} />; + return onChange(resolve(s))} options={options} value={value} />; } - const opts = options.map((o) => (typeof o === "object" ? o : { value: o, label: o })); + const opts = options.map((o) => (typeof o === "object" ? o : { label: o, value: o })); const idx = Math.max( 0, opts.findIndex((o) => o.value === value) @@ -386,11 +397,17 @@ function TweakRadio({ label, value, options, onChange }) { const onPointerDown = (e) => { setDragging(true); const v0 = segAt(e.clientX); - if (v0 !== valueRef.current) onChange(v0); + if (v0 !== valueRef.current) { + onChange(v0); + } const move = (ev) => { - if (!trackRef.current) return; + if (!trackRef.current) { + return; + } const v = segAt(ev.clientX); - if (v !== valueRef.current) onChange(v); + if (v !== valueRef.current) { + onChange(v); + } }; const up = () => { setDragging(false); @@ -404,17 +421,17 @@ function TweakRadio({ label, value, options, onChange }) { return (
{opts.map((o) => ( - ))} @@ -426,7 +443,7 @@ function TweakRadio({ label, value, options, onChange }) { function TweakSelect({ label, value, options, onChange }) { return ( - onChange(e.target.value)} value={value}> {options.map((o) => { const v = typeof o === "object" ? o.value : o; const l = typeof o === "object" ? o.label : o; @@ -446,10 +463,10 @@ function TweakText({ label, value, placeholder, onChange }) { onChange(e.target.value)} + placeholder={placeholder} type="text" value={value} - placeholder={placeholder} - onChange={(e) => onChange(e.target.value)} /> ); @@ -457,14 +474,18 @@ function TweakText({ label, value, placeholder, onChange }) { function TweakNumber({ label, value, min, max, step = 1, unit = "", onChange }) { const clamp = (n) => { - if (min != null && n < min) return min; - if (max != null && n > max) return max; + if (min != null && n < min) { + return min; + } + if (max != null && n > max) { + return max; + } return n; }; - const startRef = React.useRef({ x: 0, val: 0 }); + const startRef = React.useRef({ val: 0, x: 0 }); const onScrubStart = (e) => { e.preventDefault(); - startRef.current = { x: e.clientX, val: value }; + startRef.current = { val: value, x: e.clientX }; const decimals = (String(step).split(".")[1] || "").length; const move = (ev) => { const dx = ev.clientX - startRef.current.x; @@ -485,12 +506,12 @@ function TweakNumber({ label, value, min, max, step = 1, unit = "", onChange }) {label} onChange(clamp(Number(e.target.value)))} + step={step} + type="number" + value={value} /> {unit && {unit}}
@@ -503,23 +524,25 @@ function TweakNumber({ label, value, min, max, step = 1, unit = "", onChange }) function __twkIsLight(hex) { const h = String(hex).replace("#", ""); const x = h.length === 3 ? h.replace(/./g, (c) => c + c) : h.padEnd(6, "0"); - const n = parseInt(x.slice(0, 6), 16); - if (Number.isNaN(n)) return true; + const n = Number.parseInt(x.slice(0, 6), 16); + if (Number.isNaN(n)) { + return true; + } const r = (n >> 16) & 255, g = (n >> 8) & 255, b = n & 255; - return r * 299 + g * 587 + b * 114 > 148000; + return r * 299 + g * 587 + b * 114 > 148_000; } const __TwkCheck = ({ light }) => ( -
{label}
- onChange(e.target.value)} /> + onChange(e.target.value)} type="color" value={value} />
); } @@ -556,16 +579,16 @@ function TweakColor({ label, value, options, onChange }) { const on = key(o) === cur; return ( ); } Object.assign(window, { - useTweaks, - TweaksPanel, - TweakSection, - TweakRow, - TweakSlider, - TweakToggle, + TweakButton, + TweakColor, + TweakNumber, TweakRadio, + TweakRow, + TweakSection, TweakSelect, + TweakSlider, + TweaksPanel, TweakText, - TweakNumber, - TweakColor, - TweakButton, + TweakToggle, + useTweaks, }); diff --git a/docs/design-system/ink-carbon/project/explorer/views-1.jsx b/docs/design-system/ink-carbon/project/explorer/views-1.jsx index 13c1484b4..d90994a53 100644 --- a/docs/design-system/ink-carbon/project/explorer/views-1.jsx +++ b/docs/design-system/ink-carbon/project/explorer/views-1.jsx @@ -32,13 +32,27 @@ // Column priority: timestamp first, then text/enum/person/currency/number, // then booleans, then anything else. const priority = (f) => { - if (f.type === "timestamp") return 0; - if (f.type === "text" && /title|subject|merchant|name/i.test(f.name)) return 1; - if (f.type === "person") return 2; - if (f.type === "text") return 3; - if (f.type === "enum") return 4; - if (f.type === "currency") return 5; - if (f.type === "number") return 6; + if (f.type === "timestamp") { + return 0; + } + if (f.type === "text" && /title|subject|merchant|name/i.test(f.name)) { + return 1; + } + if (f.type === "person") { + return 2; + } + if (f.type === "text") { + return 3; + } + if (f.type === "enum") { + return 4; + } + if (f.type === "currency") { + return 5; + } + if (f.type === "number") { + return 6; + } return 9; }; const cols = [...visibleFields] @@ -50,7 +64,9 @@ function renderCell(field, record) { const v = record[field.name]; - if (v == null) return ; + if (v == null) { + return ; + } if (field.type === "timestamp") { return {fmtRelative(v)}; } @@ -59,16 +75,21 @@ } if (field.type === "number") { let display = v.toLocaleString(); - if (field.unit === "meters") display = fmtDistance(v); - else if (field.unit === "seconds") display = fmtDuration(v); + if (field.unit === "meters") { + display = fmtDistance(v); + } else if (field.unit === "seconds") { + display = fmtDuration(v); + } return {display}; } - if (field.type === "boolean") return {v ? "yes" : "—"}; + if (field.type === "boolean") { + return {v ? "yes" : "—"}; + } if (field.type === "enum" || field.type === "enum[]") { const arr = Array.isArray(v) ? v : [v]; return {arr.slice(0, 2).join(", ")}; } - if (field.type === "person") + if (field.type === "person") { return ( {String(v) @@ -76,6 +97,7 @@ .trim()} ); + } return ( {String(v)} @@ -95,7 +117,7 @@

{stream.records.map((r) => ( - onSelect(r)}> + onSelect(r)}> {cols.map((c) => ( ))} @@ -114,7 +136,9 @@ */ function TimelineView({ stream, selectedId, onSelect }) { const tf = getTimeField(stream); - if (!tf) return
No timestamp field in schema.
; + if (!tf) { + return
No timestamp field in schema.
; + } const sorted = [...stream.records].sort((a, b) => new Date(b[tf]) - new Date(a[tf])); // Group by day @@ -164,7 +188,7 @@
{months.map(([m, count]) => ( - + {new Date(m + "-15").toLocaleDateString("en-US", { month: "short" })}{" "} {count} @@ -195,7 +219,9 @@ const groups = new Map(); for (const r of stream.records) { const key = r[channelField] ?? r[subjectField] ?? (recipField ? `to:${(r[recipField] ?? []).join(",")}` : "—"); - if (!groups.has(key)) groups.set(key, []); + if (!groups.has(key)) { + groups.set(key, []); + } groups.get(key).push(r); } const channels = [...groups.entries()].sort((a, b) => { @@ -290,5 +316,5 @@ ); } - Object.assign(window, { TableView, TimelineView, ConversationView, ReaderView }); + Object.assign(window, { ConversationView, ReaderView, TableView, TimelineView }); })(); diff --git a/docs/design-system/ink-carbon/project/explorer/views-2.jsx b/docs/design-system/ink-carbon/project/explorer/views-2.jsx index c809c3a80..6b6aca805 100644 --- a/docs/design-system/ink-carbon/project/explorer/views-2.jsx +++ b/docs/design-system/ink-carbon/project/explorer/views-2.jsx @@ -105,8 +105,12 @@ const buckets = new Map(); for (const r of records) { const k = r[timeField]?.slice(0, 7); - if (!k) continue; - if (!buckets.has(k)) buckets.set(k, { key: k, net: 0, count: 0 }); + if (!k) { + continue; + } + if (!buckets.has(k)) { + buckets.set(k, { count: 0, key: k, net: 0 }); + } buckets.get(k).net += r.amount ?? 0; buckets.get(k).count += 1; } @@ -156,10 +160,14 @@ const labelField = fields.find((f) => /title|caption|subject/i.test(f.name))?.name; const timeField = getTimeField(stream); - if (!latField || !lngField) return
Stream carries no usable geo fields.
; + if (!(latField && lngField)) { + return
Stream carries no usable geo fields.
; + } const pts = stream.records.filter((r) => r[latField] != null && r[lngField] != null); - if (!pts.length) return
No records carry coordinates in this window.
; + if (!pts.length) { + return
No records carry coordinates in this window.
; + } const lats = pts.map((p) => p[latField]); const lngs = pts.map((p) => p[lngField]); @@ -231,9 +239,13 @@ const byDate = new Map(); for (const r of stream.records) { - if (!r[startField]) continue; + if (!r[startField]) { + continue; + } const key = r[startField].slice(0, 10); - if (!byDate.has(key)) byDate.set(key, []); + if (!byDate.has(key)) { + byDate.set(key, []); + } byDate.get(key).push(r); } @@ -243,10 +255,10 @@ <>
{today.toLocaleDateString("en-US", { month: "long", year: "numeric" })} @@ -300,10 +312,12 @@ const dayCounts = new Map(); for (const r of stream.records) { const d = r[timeField]?.slice(0, 10); - if (!d) continue; + if (!d) { + continue; + } dayCounts.set(d, (dayCounts.get(d) ?? 0) + 1); } - const heatValues = [...dayCounts.entries()].map(([date, count]) => ({ date, count })); + const heatValues = [...dayCounts.entries()].map(([date, count]) => ({ count, date })); // Sparklines: for each measure, sort by time and take last 30 values function valuesForMeasure(f) { @@ -326,7 +340,9 @@
{measures.map((f) => { const values = valuesForMeasure(f); - if (!values.length) return null; + if (!values.length) { + return null; + } const avg = values.reduce((s, v) => s + v, 0) / values.length; const latest = values[values.length - 1]; return ( @@ -347,11 +363,17 @@ } function formatMeasure(f, v) { - if (f.unit === "meters") return fmtDistance(v); - if (f.unit === "seconds") return fmtDuration(v); - if (typeof v === "number") return v.toFixed(v % 1 === 0 ? 0 : 1); + if (f.unit === "meters") { + return fmtDistance(v); + } + if (f.unit === "seconds") { + return fmtDuration(v); + } + if (typeof v === "number") { + return v.toFixed(v % 1 === 0 ? 0 : 1); + } return String(v); } - Object.assign(window, { LedgerView, GalleryView, MapView, CalendarView, ChartView }); + Object.assign(window, { CalendarView, ChartView, GalleryView, LedgerView, MapView }); })(); diff --git a/docs/design-system/ink-carbon/project/labs/LivingGrant.jsx b/docs/design-system/ink-carbon/project/labs/LivingGrant.jsx index 43fdb7db1..16c107757 100644 --- a/docs/design-system/ink-carbon/project/labs/LivingGrant.jsx +++ b/docs/design-system/ink-carbon/project/labs/LivingGrant.jsx @@ -8,14 +8,14 @@ const { useState, useEffect, useRef } = React; // A small deterministic fake record stream const RECORD_STREAM = [ - { e: "Acme Co", p: "2025-09-16→30", g: "$4,812.50", n: "$3,622.18" }, - { e: "Acme Co", p: "2025-09-01→15", g: "$4,812.50", n: "$3,622.18" }, - { e: "Acme Co", p: "2025-08-16→31", g: "$4,812.50", n: "$3,624.42" }, - { e: "Acme Co", p: "2025-08-01→15", g: "$4,812.50", n: "$3,624.42" }, - { e: "Acme Co", p: "2025-07-16→31", g: "$4,812.50", n: "$3,624.42" }, - { e: "Acme Co", p: "2025-07-01→15", g: "$4,756.00", n: "$3,580.12" }, - { e: "Acme Co", p: "2025-06-16→30", g: "$4,756.00", n: "$3,580.12" }, - { e: "Acme Co", p: "2025-06-01→15", g: "$4,756.00", n: "$3,580.12" }, + { e: "Acme Co", g: "$4,812.50", n: "$3,622.18", p: "2025-09-16→30" }, + { e: "Acme Co", g: "$4,812.50", n: "$3,622.18", p: "2025-09-01→15" }, + { e: "Acme Co", g: "$4,812.50", n: "$3,624.42", p: "2025-08-16→31" }, + { e: "Acme Co", g: "$4,812.50", n: "$3,624.42", p: "2025-08-01→15" }, + { e: "Acme Co", g: "$4,812.50", n: "$3,624.42", p: "2025-07-16→31" }, + { e: "Acme Co", g: "$4,756.00", n: "$3,580.12", p: "2025-07-01→15" }, + { e: "Acme Co", g: "$4,756.00", n: "$3,580.12", p: "2025-06-16→30" }, + { e: "Acme Co", g: "$4,756.00", n: "$3,580.12", p: "2025-06-01→15" }, ]; const LivingGrant = () => { @@ -24,7 +24,9 @@ const LivingGrant = () => { const [thermal, setThermal] = useState(0.62); // 0 = pure human, 1 = pure protocol useEffect(() => { - if (paused) return; + if (paused) { + return; + } const t = setInterval(() => setCursor((c) => (c + 1) % RECORD_STREAM.length), 1800); return () => clearInterval(t); }, [paused]); @@ -34,38 +36,38 @@ const LivingGrant = () => { return (
{/* The thermal rule across the top */}
{/* LEFT: Human side — the holder's view */}
-
+
§2 · HOLDER @@ -81,16 +83,16 @@ const LivingGrant = () => { . They see the employer, period, and gross and net pay. They cannot see your bank, address, or anything else.
-
+
{["pay_statements.read", "employment.read"].map((s) => ( -
+
@@ -102,8 +104,8 @@ const LivingGrant = () => {
))} {["tax_docs.read", "identity.read", "transactions.read"].map((s) => ( -
- +
+ {s} @@ -113,11 +115,11 @@ const LivingGrant = () => {
))}
-
- -
@@ -126,47 +128,47 @@ const LivingGrant = () => { {/* RIGHT: Protocol side — the machine's view */}
-
+
grt_longview01 ISSUER · §5
-
+
Longview holds a grant, not a key.
{/* The live stream pane */}
{["employer", "pay_period", "gross", "net"].map((h) => ( - + {h} ))}
-
+
{RECORD_STREAM.map((r, i) => { const offset = (i - cursor + RECORD_STREAM.length) % RECORD_STREAM.length; const y = offset * 20 - 10; @@ -175,14 +177,14 @@ const LivingGrant = () => {
@@ -204,32 +206,32 @@ const LivingGrant = () => { {/* Bottom fade */}
cursor: {String(cursor).padStart(3, "0")} / ∞ @@ -245,42 +247,42 @@ const LivingGrant = () => { {/* The thermal slider across the bottom — the metaphor made interactive */}
thermal →
{ + const rect = e.currentTarget.getBoundingClientRect(); + setThermal(Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width))); + }} style={{ - flex: 1, - position: "relative", - height: 2, background: "var(--thermal)", borderRadius: 999, cursor: "pointer", - }} - onClick={(e) => { - const rect = e.currentTarget.getBoundingClientRect(); - setThermal(Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width))); + flex: 1, + height: 2, + position: "relative", }} >
diff --git a/docs/design-system/ink-carbon/project/labs/SpecElements.jsx b/docs/design-system/ink-carbon/project/labs/SpecElements.jsx index db23c4008..f9c22eb24 100644 --- a/docs/design-system/ink-carbon/project/labs/SpecElements.jsx +++ b/docs/design-system/ink-carbon/project/labs/SpecElements.jsx @@ -11,16 +11,16 @@ const NightToggle = () => { }, [night]); return (
diff --git a/docs/design-system/ink-carbon/project/labs/labs.css b/docs/design-system/ink-carbon/project/labs/labs.css index 5502fcb81..21e2ec593 100644 --- a/docs/design-system/ink-carbon/project/labs/labs.css +++ b/docs/design-system/ink-carbon/project/labs/labs.css @@ -83,21 +83,21 @@ } html { - background: var(--paper); - color: var(--ink); font-family: var(--font-sans); + font-feature-settings: "ss01", "cv11"; -webkit-font-smoothing: antialiased; + color: var(--ink); + background: var(--paper); text-rendering: optimizeLegibility; - font-feature-settings: "ss01", "cv11"; } body { + min-height: 100vh; margin: 0; background: radial-gradient(ellipse at 15% 0%, var(--human-wash), transparent 45%), radial-gradient(ellipse at 85% 10%, var(--protocol-wash), transparent 50%), var(--paper); - min-height: 100vh; transition: background 400ms var(--ease-read), color 200ms; @@ -115,19 +115,19 @@ body { /* Selection — copper ink */ ::selection { - background: var(--human-wash); color: var(--ink); + background: var(--human-wash); } /* ═══ Type — serif display, sans body, mono machine ═══ */ .t-display { font-family: var(--font-serif); - font-weight: 400; font-size: clamp(48px, 7vw, 96px); + font-variation-settings: "opsz" 144; + font-weight: 400; line-height: 0.98; letter-spacing: -0.03em; - font-variation-settings: "opsz" 144; } .t-display em { @@ -138,34 +138,34 @@ body { .t-section { font-family: var(--font-serif); - font-weight: 400; font-size: clamp(32px, 4vw, 52px); + font-variation-settings: "opsz" 72; + font-weight: 400; line-height: 1.05; letter-spacing: -0.02em; - font-variation-settings: "opsz" 72; } .t-lede { font-family: var(--font-serif); - font-weight: 300; font-size: 22px; - line-height: 1.45; font-variation-settings: "opsz" 36; + font-weight: 300; + line-height: 1.45; color: var(--ink-soft); } .t-body { font-family: var(--font-sans); - font-weight: 400; font-size: 15px; + font-weight: 400; line-height: 1.65; color: var(--ink-soft); } .t-small { font-family: var(--font-sans); - font-weight: 400; font-size: 13px; + font-weight: 400; line-height: 1.55; color: var(--ink-faint); } @@ -182,17 +182,17 @@ body { font-family: var(--font-mono); font-size: 10.5px; font-weight: 500; - letter-spacing: 0.08em; - text-transform: uppercase; - color: var(--ink-faint); font-feature-settings: "tnum"; + color: var(--ink-faint); + text-transform: uppercase; + letter-spacing: 0.08em; } /* A numeric mono for tabular figures */ .num { font-family: var(--font-mono); - font-variant-numeric: tabular-nums; font-feature-settings: "tnum", "cv02"; + font-variant-numeric: tabular-nums; } /* ═══ Structural elements ═══ */ @@ -216,22 +216,22 @@ body { /* Buttons — flat, inked, no shadows, press-feedback only */ .btn { display: inline-flex; - align-items: center; gap: 8px; + align-items: center; height: 40px; padding: 0 18px; font-family: var(--font-sans); font-size: 14px; font-weight: 500; - border-radius: 2px; - border: 1px solid transparent; + letter-spacing: 0.01em; cursor: pointer; + border: 1px solid transparent; + border-radius: 2px; transition: all 150ms var(--ease-read); - letter-spacing: 0.01em; } .btn-ink { - background: var(--ink); color: var(--paper); + background: var(--ink); } .btn-ink:hover { background: var(--ink-soft); @@ -240,8 +240,8 @@ body { transform: translateY(1px); } .btn-paper { - background: transparent; color: var(--ink); + background: transparent; border-color: var(--rule-deep); } .btn-paper:hover { @@ -249,8 +249,8 @@ body { border-color: var(--ink-faint); } .btn-ghost { - background: transparent; color: var(--ink-soft); + background: transparent; } .btn-ghost:hover { color: var(--ink); @@ -259,30 +259,30 @@ body { /* Chips — the pill-in-prose */ .chip { display: inline-flex; - align-items: center; gap: 4px; + align-items: center; padding: 1px 7px; - border-radius: 2px; font-family: var(--font-mono); font-size: 11.5px; - background: var(--paper-warm); color: var(--ink-soft); + background: var(--paper-warm); border: 1px solid var(--rule); + border-radius: 2px; } .chip-human { color: var(--human); - border-color: oklch(from var(--human) l c h / 0.25); background: var(--human-wash); + border-color: oklch(from var(--human) l c h / 0.25); } .chip-protocol { color: var(--protocol); - border-color: oklch(from var(--protocol) l c h / 0.25); background: var(--protocol-wash); + border-color: oklch(from var(--protocol) l c h / 0.25); } .chip-live { color: var(--live); - border-color: oklch(from var(--live) l c h / 0.3); background: oklch(from var(--live) l c h / 0.08); + border-color: oklch(from var(--live) l c h / 0.3); } /* ═══ Animations ═══ */ @@ -345,8 +345,8 @@ body { *, *::before, *::after { + transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; - transition-duration: 0.01ms !important; } } diff --git a/docs/design-system/ink-carbon/project/preview/brand-logo.html b/docs/design-system/ink-carbon/project/preview/brand-logo.html index c7a49aaaa..0729be41a 100644 --- a/docs/design-system/ink-carbon/project/preview/brand-logo.html +++ b/docs/design-system/ink-carbon/project/preview/brand-logo.html @@ -8,35 +8,35 @@ diff --git a/docs/design-system/ink-carbon/project/preview/colors-primary.html b/docs/design-system/ink-carbon/project/preview/colors-primary.html index ba6050a04..2470674f7 100644 --- a/docs/design-system/ink-carbon/project/preview/colors-primary.html +++ b/docs/design-system/ink-carbon/project/preview/colors-primary.html @@ -7,8 +7,8 @@ diff --git a/docs/design-system/ink-carbon/project/preview/colors-semantic.html b/docs/design-system/ink-carbon/project/preview/colors-semantic.html index dc223e315..f64cfe125 100644 --- a/docs/design-system/ink-carbon/project/preview/colors-semantic.html +++ b/docs/design-system/ink-carbon/project/preview/colors-semantic.html @@ -7,8 +7,8 @@ diff --git a/docs/design-system/ink-carbon/project/preview/colors-washes.html b/docs/design-system/ink-carbon/project/preview/colors-washes.html index f8864c8c9..4174acd8c 100644 --- a/docs/design-system/ink-carbon/project/preview/colors-washes.html +++ b/docs/design-system/ink-carbon/project/preview/colors-washes.html @@ -7,10 +7,10 @@ diff --git a/docs/design-system/ink-carbon/project/preview/components-buttons.html b/docs/design-system/ink-carbon/project/preview/components-buttons.html index dffd70219..1eedb2373 100644 --- a/docs/design-system/ink-carbon/project/preview/components-buttons.html +++ b/docs/design-system/ink-carbon/project/preview/components-buttons.html @@ -8,21 +8,21 @@ diff --git a/docs/design-system/ink-carbon/project/preview/components-cards.html b/docs/design-system/ink-carbon/project/preview/components-cards.html index b15fb5236..c93bdd06b 100644 --- a/docs/design-system/ink-carbon/project/preview/components-cards.html +++ b/docs/design-system/ink-carbon/project/preview/components-cards.html @@ -8,25 +8,25 @@ diff --git a/docs/design-system/ink-carbon/project/preview/components-code.html b/docs/design-system/ink-carbon/project/preview/components-code.html index a2ce41af6..b6f60a5c6 100644 --- a/docs/design-system/ink-carbon/project/preview/components-code.html +++ b/docs/design-system/ink-carbon/project/preview/components-code.html @@ -7,18 +7,18 @@ diff --git a/docs/design-system/ink-carbon/project/preview/components-dataflow.html b/docs/design-system/ink-carbon/project/preview/components-dataflow.html index eb8c89d1d..546772db8 100644 --- a/docs/design-system/ink-carbon/project/preview/components-dataflow.html +++ b/docs/design-system/ink-carbon/project/preview/components-dataflow.html @@ -8,8 +8,8 @@ diff --git a/docs/design-system/ink-carbon/project/preview/components-grant-card.html b/docs/design-system/ink-carbon/project/preview/components-grant-card.html index 446cd6521..1fac788cf 100644 --- a/docs/design-system/ink-carbon/project/preview/components-grant-card.html +++ b/docs/design-system/ink-carbon/project/preview/components-grant-card.html @@ -8,14 +8,14 @@ diff --git a/docs/design-system/ink-carbon/project/preview/components-inputs.html b/docs/design-system/ink-carbon/project/preview/components-inputs.html index b7c707e99..ee7b4c946 100644 --- a/docs/design-system/ink-carbon/project/preview/components-inputs.html +++ b/docs/design-system/ink-carbon/project/preview/components-inputs.html @@ -8,13 +8,13 @@ diff --git a/docs/design-system/ink-carbon/project/preview/iconography.html b/docs/design-system/ink-carbon/project/preview/iconography.html index 63172261e..6a208d5ed 100644 --- a/docs/design-system/ink-carbon/project/preview/iconography.html +++ b/docs/design-system/ink-carbon/project/preview/iconography.html @@ -7,10 +7,10 @@ diff --git a/docs/design-system/ink-carbon/project/preview/layout-grid.html b/docs/design-system/ink-carbon/project/preview/layout-grid.html index 8cbdbaf7c..224542e89 100644 --- a/docs/design-system/ink-carbon/project/preview/layout-grid.html +++ b/docs/design-system/ink-carbon/project/preview/layout-grid.html @@ -8,12 +8,12 @@ diff --git a/docs/design-system/ink-carbon/project/preview/shadows.html b/docs/design-system/ink-carbon/project/preview/shadows.html index 7b73a065c..2aabcfd0f 100644 --- a/docs/design-system/ink-carbon/project/preview/shadows.html +++ b/docs/design-system/ink-carbon/project/preview/shadows.html @@ -8,30 +8,30 @@ diff --git a/docs/design-system/ink-carbon/project/preview/spacing-scale.html b/docs/design-system/ink-carbon/project/preview/spacing-scale.html index 5671bca14..1ec59a865 100644 --- a/docs/design-system/ink-carbon/project/preview/spacing-scale.html +++ b/docs/design-system/ink-carbon/project/preview/spacing-scale.html @@ -7,12 +7,12 @@ diff --git a/docs/design-system/ink-carbon/project/preview/type-display.html b/docs/design-system/ink-carbon/project/preview/type-display.html index 0e4b9903c..7190dfdc0 100644 --- a/docs/design-system/ink-carbon/project/preview/type-display.html +++ b/docs/design-system/ink-carbon/project/preview/type-display.html @@ -7,12 +7,12 @@ diff --git a/docs/design-system/ink-carbon/project/recordroom/console.css b/docs/design-system/ink-carbon/project/recordroom/console.css index bcf9392cf..4bea91957 100644 --- a/docs/design-system/ink-carbon/project/recordroom/console.css +++ b/docs/design-system/ink-carbon/project/recordroom/console.css @@ -14,47 +14,47 @@ display: grid; grid-template-columns: var(--pdpp-sidebar-width) 1fr; height: 100vh; - background: var(--background); - color: var(--foreground); - font-family: var(--font-sans); overflow: hidden; + font-family: var(--font-sans); + color: var(--foreground); + background: var(--background); } /* ─── Sidebar — the rack; content is the paper ─── */ .rr-side { - background: color-mix(in oklab, var(--background) 88%, oklch(0 0 0)); - border-right: 1px solid var(--border); display: flex; flex-direction: column; - padding: 18px 0 14px; min-height: 0; + padding: 18px 0 14px; + background: color-mix(in oklab, var(--background) 88%, oklch(0 0 0)); + border-right: 1px solid var(--border); } .rr-side__brand { display: flex; - align-items: center; gap: 12px; + align-items: center; padding: 2px 18px 16px; border-bottom: 1px solid var(--border); } .rr-side__mark { - width: 18px; - height: 18px; position: relative; flex-shrink: 0; + width: 18px; + height: 18px; } /* the mark is the doctrine: a sheet casting its carbon */ .rr-side__mark::before { - content: ""; position: absolute; inset: 0; - transform: translate(4px, 4px); + content: ""; background: var(--carbon-fill); border: 1px solid var(--carbon-outline); + transform: translate(4px, 4px); } .rr-side__mark::after { - content: ""; position: absolute; inset: 0; + content: ""; background: var(--foreground); } .rr-side__name { @@ -65,22 +65,22 @@ .rr-side__nav { display: flex; flex-direction: column; - padding: 12px 10px; gap: 2px; + padding: 12px 10px; } .rr-nav-item { display: grid; grid-template-columns: 1fr auto; - align-items: center; gap: 10px; + align-items: center; padding: 9px 10px; font-size: 13.5px; font-weight: 600; color: var(--muted-foreground); + text-align: left; background: transparent; border: 0; border-radius: var(--radius-control); - text-align: left; transition: color var(--motion-state), background var(--motion-state); @@ -90,44 +90,44 @@ background: var(--muted); } .rr-nav-item.is-active { + font-weight: 700; color: var(--foreground); background: var(--muted); - font-weight: 700; } .rr-nav-item__count { font-family: var(--font-mono); font-size: 10px; - color: var(--muted-foreground); font-variant-numeric: tabular-nums; + color: var(--muted-foreground); } .rr-side__spacer { flex: 1; } .rr-side__foot { - padding: 12px 18px 0; - border-top: 1px solid var(--border); display: flex; flex-direction: column; gap: 3px; + padding: 12px 18px 0; + border-top: 1px solid var(--border); } .rr-side__owner { font-size: 12.5px; font-weight: 600; } .rr-side__host { + overflow: hidden; + text-overflow: ellipsis; font-family: var(--font-mono); font-size: 9.5px; color: var(--muted-foreground); - overflow: hidden; - text-overflow: ellipsis; white-space: nowrap; } .rr-side__motto { + padding-top: 3px; font-family: var(--font-mono); font-size: 9.5px; color: var(--primary); letter-spacing: 0.06em; - padding-top: 3px; } /* ─── Main column ─── */ @@ -139,26 +139,26 @@ } .rr-head { display: flex; - justify-content: space-between; - align-items: center; + flex-shrink: 0; gap: 16px; + align-items: center; + justify-content: space-between; padding: 10px 32px; border-bottom: 1px solid var(--border); - flex-shrink: 0; } .rr-head__crumb { + overflow: hidden; + text-overflow: ellipsis; font-family: var(--font-mono); font-size: 10.5px; color: var(--muted-foreground); white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; } .rr-head__title { + margin: 0; font-size: 17px; font-weight: 700; letter-spacing: -0.012em; - margin: 0; } .rr-head__sub { font-family: var(--font-mono); @@ -168,9 +168,9 @@ } .rr-content { flex: 1; - overflow-y: auto; - padding: 28px 32px 56px; min-height: 0; + padding: 28px 32px 56px; + overflow-y: auto; } .rr-page { max-width: 1200px; @@ -183,9 +183,9 @@ } .rr-page-head { display: flex; - justify-content: space-between; - align-items: baseline; gap: 16px; + align-items: baseline; + justify-content: space-between; margin: 0 0 24px; } .rr-page--split > .rr-page-head { @@ -193,10 +193,10 @@ margin-bottom: 4px; } .rr-page-head__t { + margin: 0; font-size: 21px; font-weight: 800; letter-spacing: -0.02em; - margin: 0; } .rr-page-head__s { font-family: var(--font-mono); @@ -217,9 +217,9 @@ } .rr-incoming__sheet { display: flex; - justify-content: space-between; - align-items: center; gap: 16px; + align-items: center; + justify-content: space-between; padding: 13px 18px; background: var(--card); border: 1px solid var(--border-strong); @@ -235,24 +235,24 @@ font-weight: 700; } .rr-incoming__meta { + overflow: hidden; + text-overflow: ellipsis; font-family: var(--font-mono); font-size: 10px; color: var(--muted-foreground); white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; } /* ─── Grants ledger ─── */ .rr-row-btn { display: block; width: 100%; - background: none; - border: 0; padding: 0; - text-align: left; - color: inherit; font: inherit; + color: inherit; + text-align: left; + background: none; + border: 0; } .rr-row-btn:hover .pdpp-data-row { background: var(--muted); @@ -274,30 +274,30 @@ font-size: 15px; } .rr-inspector__empty { - border: 1px dashed var(--border-strong); padding: 28px 22px; font-size: 12.5px; color: var(--muted-foreground); text-align: center; + border: 1px dashed var(--border-strong); } /* Inspector portrait sections — server-derivable facts only */ .rr-insp-label { + padding-bottom: 5px; + margin: 16px 0 2px; font-family: var(--font-mono); font-size: 9.5px; - letter-spacing: 0.12em; - text-transform: uppercase; color: var(--muted-foreground); + text-transform: uppercase; + letter-spacing: 0.12em; border-bottom: 1px solid var(--border-strong); - padding-bottom: 5px; - margin: 16px 0 2px; } .rr-insp-item { - padding: 9px 0; - border-bottom: 1px solid var(--border); display: flex; flex-direction: column; gap: 2px; + padding: 9px 0; + border-bottom: 1px solid var(--border); } .rr-insp-item__t { font-size: 0.875rem; @@ -315,11 +315,11 @@ overflow-wrap: anywhere; } .rr-insp-keep { - padding: 8px 0; - border-bottom: 1px solid var(--border); display: flex; flex-direction: column; gap: 2px; + padding: 8px 0; + border-bottom: 1px solid var(--border); } .rr-insp-keep__what { font-family: var(--font-mono); @@ -336,10 +336,10 @@ color: var(--muted-foreground); } .rr-insp-pulse { + margin-top: 12px; font-family: var(--font-mono); font-size: 0.69rem; color: var(--muted-foreground); - margin-top: 12px; } /* Revocation strike — the line draws, the record stays */ @@ -349,17 +349,17 @@ max-width: 100%; overflow: hidden; text-overflow: ellipsis; - white-space: nowrap; vertical-align: bottom; + white-space: nowrap; } .rr-strikeable__line { position: absolute; - left: 0; top: 56%; - height: 1.5px; + left: 0; width: 0; - background: currentColor; + height: 1.5px; pointer-events: none; + background: currentColor; } .rr-strike-go .rr-strikeable__line { width: 100%; @@ -377,37 +377,37 @@ padding: 12px 18px 14px; } .rr-field-chip { + padding: 4px 9px; font-family: var(--font-mono); font-size: 10.5px; - padding: 4px 9px; + white-space: nowrap; box-shadow: inset 0 0 0 1px var(--border-strong); transition: opacity var(--motion-state), color var(--motion-state), box-shadow var(--motion-state); - white-space: nowrap; } .rr-field-chip--dropped { color: var(--muted-foreground); - box-shadow: inset 0 0 0 1px var(--border); text-decoration: line-through; text-decoration-thickness: 1px; + box-shadow: inset 0 0 0 1px var(--border); opacity: 0.55; } .rr-lens { + padding: 5px 10px; font-family: var(--font-mono); font-size: 10px; + color: var(--muted-foreground); letter-spacing: 0.04em; - padding: 5px 10px; + white-space: nowrap; background: transparent; - color: var(--muted-foreground); border: 1px solid var(--input); border-radius: var(--radius-control); transition: color var(--motion-state), border-color var(--motion-state), background var(--motion-state); - white-space: nowrap; } .rr-lens:hover { color: var(--foreground); @@ -415,8 +415,8 @@ } .rr-lens.is-on { color: var(--primary); - border-color: var(--primary); background: var(--primary-tint); + border-color: var(--primary); } /* ─── Activity view ─── */ @@ -427,16 +427,16 @@ display: grid; grid-template-columns: 150px 70px minmax(0, 1fr) auto; gap: 14px; + align-items: baseline; padding: 8px 2px; - border-bottom: 1px solid var(--border); font-family: var(--font-mono); font-size: 11px; - align-items: baseline; + border-bottom: 1px solid var(--border); } .rr-log__t { + font-variant-numeric: tabular-nums; color: var(--muted-foreground); white-space: nowrap; - font-variant-numeric: tabular-nums; } .rr-log__verb { font-weight: 600; @@ -467,12 +467,12 @@ .rr-overlay { position: fixed; inset: 0; - background: oklch(0 0 0 / 0.55); + z-index: 50; display: flex; align-items: center; justify-content: center; padding: 28px; - z-index: 50; + background: oklch(0 0 0 / 0.55); animation: fade-in var(--duration-base) var(--ease-enter) both; } @@ -520,9 +520,9 @@ } .rr-ceremony__kicker-row { display: flex; - justify-content: space-between; - align-items: baseline; gap: 12px; + align-items: baseline; + justify-content: space-between; padding: 12px 22px; border-bottom: 1px solid var(--border); } @@ -535,23 +535,23 @@ opacity: 0; } .rr-ceremony__title { + margin: 0; font-size: 19px; font-weight: 700; letter-spacing: -0.015em; - margin: 0; } .rr-ceremony__sub { - font-size: 13px; - color: var(--muted-foreground); margin: 6px 0 0; + font-size: 13px; line-height: 1.55; + color: var(--muted-foreground); } .rr-ceremony__body { padding: 16px 22px 6px; } .rr-ceremony__scopes { - border-top: 1px solid var(--border-strong); margin-top: 14px; + border-top: 1px solid var(--border-strong); } .rr-scope-decide { @@ -568,52 +568,52 @@ font-weight: 500; } .rr-scope-decide__desc { + grid-column: 1; font-size: 11.5px; color: var(--muted-foreground); - grid-column: 1; } .rr-scope-decide__terms { + grid-row: 1; + grid-column: 2; font-family: var(--font-mono); font-size: 10px; color: var(--muted-foreground); white-space: nowrap; - grid-row: 1; - grid-column: 2; } .rr-scope-decide--off .rr-scope-decide__name { - color: var(--muted-foreground); font-weight: 400; + color: var(--muted-foreground); text-decoration: line-through; text-decoration-thickness: 1px; } .rr-allow { + grid-row: 1 / span 2; + grid-column: 3; + padding: 5px 10px; font-family: var(--font-mono); font-size: 10px; font-weight: 500; + color: var(--muted-foreground); letter-spacing: 0.04em; - padding: 5px 10px; - border: 1px solid var(--input); + white-space: nowrap; background: transparent; - color: var(--muted-foreground); + border: 1px solid var(--input); border-radius: var(--radius-control); - grid-row: 1 / span 2; - grid-column: 3; transition: color var(--motion-state), border-color var(--motion-state), background var(--motion-state); - white-space: nowrap; } .rr-allow.is-on { color: var(--success); - border-color: var(--success); background: var(--success-tint); + border-color: var(--success); } .rr-ceremony__foot { display: flex; - justify-content: space-between; - align-items: center; gap: 12px; + align-items: center; + justify-content: space-between; padding: 14px 22px 16px; border-top: 1px solid var(--border-strong); } @@ -640,9 +640,9 @@ } @media (prefers-reduced-motion: reduce) { .rr-ceremony.is-pressing .pdpp-carbon::before { - animation: none; opacity: 1; transform: translate(var(--carbon-offset), var(--carbon-offset)); + animation: none; } .rr-ceremony.is-pressing .rr-press-reveal { animation: none; diff --git a/docs/design-system/ink-carbon/project/recordroom/console2.css b/docs/design-system/ink-carbon/project/recordroom/console2.css index 6a68311e4..40cac6e16 100644 --- a/docs/design-system/ink-carbon/project/recordroom/console2.css +++ b/docs/design-system/ink-carbon/project/recordroom/console2.css @@ -76,13 +76,13 @@ margin-bottom: 4px; } .rr-hero__sheet { - background: var(--card); - border: 1px solid var(--border-strong); - padding: 20px 24px; display: flex; - justify-content: space-between; - align-items: flex-end; gap: 24px; + align-items: flex-end; + justify-content: space-between; + padding: 20px 24px; + background: var(--card); + border: 1px solid var(--border-strong); } .rr-hero__text { display: flex; @@ -91,19 +91,19 @@ min-width: 0; } .rr-hero__title { + margin: 0; font-size: 22px; font-weight: 700; - letter-spacing: -0.018em; line-height: 1.2; - margin: 0; + letter-spacing: -0.018em; } .rr-hero__meta { + overflow: hidden; + text-overflow: ellipsis; font-family: var(--font-mono); font-size: 11px; color: var(--muted-foreground); white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; } /* ─── Shared column templates — one axis per view ─── */ @@ -126,32 +126,32 @@ /* ─── Topbar actions ─── */ .rr-head__actions { display: flex; - align-items: center; gap: 8px; + align-items: center; } .rr-kbd { + padding: 2px 5px; font-family: var(--font-mono); font-size: 9.5px; - padding: 2px 5px; - box-shadow: inset 0 0 0 1px var(--border-strong); color: var(--muted-foreground); + box-shadow: inset 0 0 0 1px var(--border-strong); } .rr-chrome-btn { display: inline-flex; - align-items: center; gap: 7px; + align-items: center; + padding: 5px 10px; font-family: var(--font-sans); font-size: 12px; font-weight: 600; color: var(--muted-foreground); + white-space: nowrap; background: transparent; border: 1px solid var(--input); border-radius: var(--radius-control); - padding: 5px 10px; transition: color var(--motion-state), border-color var(--motion-state); - white-space: nowrap; } .rr-chrome-btn:hover { color: var(--foreground); @@ -160,13 +160,13 @@ /* ─── Sidebar groups + env footer ─── */ .rr-side__group { + padding: 14px 10px 4px; font-family: var(--font-mono); font-size: 9px; font-weight: 500; - letter-spacing: 0.14em; - text-transform: uppercase; color: var(--muted-foreground); - padding: 14px 10px 4px; + text-transform: uppercase; + letter-spacing: 0.14em; } .rr-env { display: flex; @@ -176,27 +176,27 @@ } .rr-env__row { display: flex; - align-items: center; gap: 7px; + align-items: center; + overflow: hidden; font-family: var(--font-mono); font-size: 9.5px; color: var(--muted-foreground); white-space: nowrap; - overflow: hidden; } .rr-env__dot { + flex-shrink: 0; width: 6px; height: 6px; - border-radius: 50%; background: var(--success); - flex-shrink: 0; + border-radius: 50%; } .rr-env__dot--down { background: var(--destructive); } .rr-env__label { - width: 18px; flex-shrink: 0; + width: 18px; color: var(--foreground); } .rr-env__url { @@ -206,10 +206,10 @@ /* ─── Empty states — the record speaks even when blank ─── */ .rr-empty { - padding: 36px 0; display: flex; flex-direction: column; gap: 6px; + padding: 36px 0; border-bottom: 1px solid var(--border); } .rr-empty__line { @@ -225,9 +225,9 @@ /* ─── Terminal affordance — every record ends with a quiet act ─── */ .rr-end { display: flex; - justify-content: space-between; - align-items: baseline; gap: 16px; + align-items: baseline; + justify-content: space-between; padding: 14px 0 0; } .rr-end__note { @@ -240,12 +240,12 @@ .rr-palette-overlay { position: fixed; inset: 0; - background: oklch(0 0 0 / 0.45); + z-index: 60; display: flex; - justify-content: center; align-items: flex-start; + justify-content: center; padding-top: 14vh; - z-index: 60; + background: oklch(0 0 0 / 0.45); animation: fade-in var(--duration-fast) var(--ease-enter) both; } .rr-palette { @@ -256,23 +256,23 @@ } .rr-palette__input { width: 100%; + padding: 13px 16px; font-family: var(--font-mono); font-size: 13.5px; - padding: 13px 16px; - background: transparent; color: var(--foreground); + outline: none; + background: transparent; border: 0; border-bottom: 1px solid var(--border); - outline: none; } .rr-palette__input::placeholder { - color: var(--muted-foreground); font-family: var(--font-sans); + color: var(--muted-foreground); } .rr-palette__list { max-height: 320px; - overflow-y: auto; padding: 6px 0; + overflow-y: auto; } .rr-palette__item { display: grid; @@ -280,13 +280,13 @@ gap: 12px; align-items: baseline; width: 100%; + padding: 8px 16px; + font-family: var(--font-sans); + font-size: 13px; + color: var(--foreground); text-align: left; background: transparent; border: 0; - color: var(--foreground); - font-family: var(--font-sans); - font-size: 13px; - padding: 8px 16px; } .rr-palette__item.is-hl { background: var(--primary-tint); @@ -310,17 +310,17 @@ grid-template-columns: var(--cols); gap: 0 18px; align-items: baseline; - padding: 9px 0; - border-bottom: 1px solid var(--border); width: 100%; + padding: 9px 0; + font-family: var(--font-mono); + font-size: 11.5px; + color: inherit; text-align: left; background: transparent; - border-left: 0; - border-right: 0; border-top: 0; - color: inherit; - font-family: var(--font-mono); - font-size: 11.5px; + border-right: 0; + border-bottom: 1px solid var(--border); + border-left: 0; transition: background var(--motion-state); } .rr-trace-row:focus { @@ -339,20 +339,20 @@ .rr-trace-row__who { overflow: hidden; text-overflow: ellipsis; - white-space: nowrap; font-family: var(--font-sans); font-size: 0.875rem; font-weight: 600; + white-space: nowrap; } .rr-trace-row__what { - color: var(--muted-foreground); overflow: hidden; text-overflow: ellipsis; + color: var(--muted-foreground); white-space: nowrap; } .rr-trace-row__n { - text-align: right; font-variant-numeric: tabular-nums; + text-align: right; } .rr-decide { font-weight: 600; @@ -370,28 +370,28 @@ flex-direction: column; } .rr-step { + position: relative; display: grid; grid-template-columns: 86px 1fr; gap: 14px; padding: 9px 0 11px 14px; - position: relative; } .rr-step::before { - content: ""; position: absolute; - left: 0; top: 16px; + left: 0; width: 7px; height: 7px; + content: ""; background: var(--primary); } .rr-step::after { - content: ""; position: absolute; - left: 3px; top: 25px; bottom: -7px; + left: 3px; width: 1px; + content: ""; background: var(--border-strong); } .rr-step:last-child::after { @@ -401,12 +401,12 @@ background: var(--destructive); } .rr-step__k { + padding-top: 1px; font-family: var(--font-mono); font-size: 10px; - letter-spacing: 0.08em; - text-transform: uppercase; color: var(--muted-foreground); - padding-top: 1px; + text-transform: uppercase; + letter-spacing: 0.08em; } .rr-step__v { font-family: var(--font-mono); @@ -416,10 +416,10 @@ } .rr-copyid { - background: none; - border: 0; padding: 0; cursor: pointer; + background: none; + border: 0; transition: color var(--motion-state); } .rr-copyid:hover { @@ -439,8 +439,8 @@ font-family: var(--font-mono); font-size: 9px; font-weight: 600; - letter-spacing: 0.1em; text-transform: uppercase; + letter-spacing: 0.1em; } .rr-check--ok .rr-check__glyph { color: var(--success); @@ -475,25 +475,25 @@ white-space: nowrap; } .rr-feed-row__stream { + overflow: hidden; + text-overflow: ellipsis; font-family: var(--font-mono); font-size: 12px; font-weight: 600; white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; } .rr-feed-row__body { - font-size: 13px; overflow: hidden; text-overflow: ellipsis; + font-size: 13px; white-space: nowrap; } .rr-feed-row__id { font-family: var(--font-mono); font-size: 10px; color: var(--muted-foreground); - white-space: nowrap; text-align: right; + white-space: nowrap; } /* ─── Overview composition ─── */ @@ -533,49 +533,49 @@ font-weight: 500; } .rr-attn__detail { + grid-column: 1; font-family: var(--font-mono); font-size: 0.72rem; color: var(--muted-foreground); - grid-column: 1; overflow-wrap: anywhere; } .rr-attn__side { - grid-row: 1 / span 2; - grid-column: 2; display: flex; flex-direction: column; - align-items: flex-end; + grid-row: 1 / span 2; + grid-column: 2; gap: 4px; + align-items: flex-end; } .rr-attn__meta { font-family: var(--font-mono); font-size: 0.66rem; + font-variant-numeric: tabular-nums; color: var(--muted-foreground); white-space: nowrap; - font-variant-numeric: tabular-nums; } .rr-mini-head { display: flex; - justify-content: space-between; - align-items: baseline; gap: 12px; + align-items: baseline; + justify-content: space-between; margin-bottom: 8px; } .rr-mini-head__t { + margin: 0; font-size: 13px; font-weight: 700; - margin: 0; white-space: nowrap; } .rr-mini-head a, .rr-link { + padding: 0; font-family: var(--font-mono); font-size: 10px; color: var(--primary); + letter-spacing: 0.04em; background: none; border: 0; - padding: 0; - letter-spacing: 0.04em; } .rr-link:hover { text-decoration: underline; @@ -602,19 +602,19 @@ font-weight: 700; } .rr-cell__detail { + grid-column: 1; font-family: var(--font-mono); font-size: 10.5px; color: var(--muted-foreground); - grid-column: 1; overflow-wrap: anywhere; } .rr-cell__side { - grid-row: 1 / span 2; - grid-column: 2; display: flex; flex-direction: column; - align-items: flex-end; + grid-row: 1 / span 2; + grid-column: 2; gap: 5px; + align-items: flex-end; } .rr-cell__meta { font-family: var(--font-mono); diff --git a/docs/design-system/ink-carbon/project/recordroom/explore.css b/docs/design-system/ink-carbon/project/recordroom/explore.css index 35e5185d8..cd7c6d3da 100644 --- a/docs/design-system/ink-carbon/project/recordroom/explore.css +++ b/docs/design-system/ink-carbon/project/recordroom/explore.css @@ -24,14 +24,14 @@ flex-direction: column; } .rr-x-facets__label { + padding-bottom: 7px; + margin-bottom: 4px; font-family: var(--font-mono); font-size: 10px; - letter-spacing: 0.1em; - text-transform: uppercase; color: var(--muted-foreground); - padding-bottom: 7px; + text-transform: uppercase; + letter-spacing: 0.1em; border-bottom: 1px solid var(--border-strong); - margin-bottom: 4px; } .rr-x-facet { display: grid; @@ -39,25 +39,25 @@ gap: 8px; align-items: baseline; padding: 5px 0; - background: none; - border: 0; color: var(--muted-foreground); text-align: left; cursor: pointer; + background: none; + border: 0; transition: color var(--motion-state); } .rr-x-facet:hover { color: var(--foreground); } .rr-x-facet.is-on { - color: var(--foreground); font-weight: 600; + color: var(--foreground); } .rr-x-facet__name { - font-size: 12.5px; - white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + font-size: 12.5px; + white-space: nowrap; } .rr-x-facet__name--mono { font-family: var(--font-mono); @@ -70,23 +70,23 @@ .rr-x-facet__flag { font-family: var(--font-mono); font-size: 9px; - letter-spacing: 0.06em; - text-transform: uppercase; color: var(--muted-foreground); + text-transform: uppercase; + letter-spacing: 0.06em; } .rr-x-facet__flag.is-warn { color: var(--warning); } .rr-x-mark { + padding: 1px 5px; + margin-right: 7px; font-family: var(--font-mono); font-size: 0.6rem; - letter-spacing: 0.06em; - text-transform: uppercase; + vertical-align: 1px; color: var(--muted-foreground); + text-transform: uppercase; + letter-spacing: 0.06em; box-shadow: inset 0 0 0 1px var(--border-strong); - padding: 1px 5px; - margin-right: 7px; - vertical-align: 1px; } .rr-x-facet__n { font-family: var(--font-mono); @@ -96,10 +96,10 @@ /* ─── Controls ─── */ .rr-x-main { - min-width: 0; display: flex; flex-direction: column; gap: 16px; + min-width: 0; } .rr-x-controls { display: flex; @@ -111,9 +111,9 @@ } .rr-x-ranges { display: flex; - align-items: center; - gap: 8px; flex-wrap: wrap; + gap: 8px; + align-items: center; } .rr-x-jump { margin-left: auto; @@ -127,21 +127,21 @@ } .rr-x-search { flex: 1 1 auto; - min-width: 0; width: auto; + min-width: 0; } .rr-x-sort { display: flex; - align-items: center; - gap: 6px; flex-shrink: 0; + gap: 6px; + align-items: center; } .rr-x-sort__label { font-family: var(--font-mono); font-size: 9px; - letter-spacing: 0.08em; - text-transform: uppercase; color: var(--muted-foreground); + text-transform: uppercase; + letter-spacing: 0.08em; } .rr-x-help { position: relative; @@ -158,23 +158,23 @@ } .rr-x-help__body { position: absolute; - z-index: 5; top: 20px; left: 0; + z-index: 5; width: 330px; padding: 10px 12px; - background: var(--card); - border: 1px solid var(--border-strong); font-size: 11px; line-height: 1.9; color: var(--muted-foreground); + background: var(--card); + border: 1px solid var(--border-strong); } .rr-x-help__body code { + padding: 1px 5px; font-family: var(--font-mono); font-size: 10px; color: var(--foreground); background: var(--muted); - padding: 1px 5px; } .rr-x-active { display: flex; @@ -184,16 +184,16 @@ } .rr-x-chip { display: inline-flex; - align-items: center; gap: 7px; + align-items: center; + padding: 4px 6px 4px 9px; font-family: var(--font-mono); font-size: 10.5px; - padding: 4px 6px 4px 9px; - background: var(--primary-tint); color: var(--primary); + cursor: pointer; + background: var(--primary-tint); border: 1px solid var(--primary); border-radius: var(--radius-control); - cursor: pointer; } .rr-x-chip__x { font-size: 13px; @@ -204,13 +204,13 @@ opacity: 1; } .rr-x-clearall { + padding: 4px; font-family: var(--font-mono); font-size: 10px; color: var(--muted-foreground); + cursor: pointer; background: none; border: 0; - cursor: pointer; - padding: 4px; } .rr-x-clearall:hover { color: var(--foreground); @@ -225,26 +225,26 @@ /* ─── Partial fan-in, in the system's voice ─── */ .rr-x-partial { - border: 1px solid var(--border-strong); background: var(--card); + border: 1px solid var(--border-strong); } .rr-x-partial__head { display: flex; - align-items: baseline; gap: 10px; + align-items: baseline; width: 100%; + padding: 10px 14px; + color: inherit; text-align: left; + cursor: pointer; background: none; border: 0; - color: inherit; - padding: 10px 14px; - cursor: pointer; } .rr-x-partial__line { - font-size: 12.5px; - font-weight: 600; flex: 1; min-width: 0; + font-size: 12.5px; + font-weight: 600; } .rr-x-partial__toggle { font-family: var(--font-mono); @@ -253,26 +253,26 @@ white-space: nowrap; } .rr-x-partial__body { - padding: 0 14px 12px 36px; display: flex; flex-direction: column; gap: 8px; align-items: flex-start; + padding: 0 14px 12px 36px; } .rr-x-partial__expl { + max-width: 60ch; margin: 0; font-size: 12.5px; line-height: 1.55; color: var(--muted-foreground); - max-width: 60ch; } .rr-x-partial__raw { + padding: 5px 8px; font-family: var(--font-mono); font-size: 10px; color: var(--muted-foreground); - background: var(--muted); - padding: 5px 8px; overflow-wrap: anywhere; + background: var(--muted); } /* ─── Day groups ─── */ @@ -282,9 +282,9 @@ } .rr-x-day__head { display: flex; - justify-content: space-between; - align-items: baseline; gap: 12px; + align-items: baseline; + justify-content: space-between; padding-bottom: 6px; border-bottom: 1px solid var(--border-strong); } @@ -296,8 +296,8 @@ .rr-x-day__n { font-family: var(--font-mono); font-size: 10.5px; - color: var(--muted-foreground); font-variant-numeric: tabular-nums; + color: var(--muted-foreground); } /* ─── Record rows ─── */ @@ -306,15 +306,15 @@ flex-direction: column; gap: 2px; width: 100%; + min-width: 0; + padding: 9px 0 10px; + color: inherit; text-align: left; + cursor: pointer; background: none; border: 0; border-bottom: 1px solid var(--border); - color: inherit; - padding: 9px 0 10px; - cursor: pointer; transition: background var(--motion-state); - min-width: 0; } .rr-x-row:hover { background: var(--muted); @@ -325,8 +325,8 @@ } .rr-x-row__attr { display: flex; - align-items: baseline; gap: 10px; + align-items: baseline; min-width: 0; } .rr-x-row__stream { @@ -336,14 +336,14 @@ white-space: nowrap; } .rr-x-row__con { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; font-family: var(--font-mono); font-size: 10px; color: var(--muted-foreground); white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - flex: 1; - min-width: 0; } .rr-x-row__rel { font-family: var(--font-mono); @@ -352,11 +352,11 @@ white-space: nowrap; } .rr-x-row__title { + overflow: hidden; + text-overflow: ellipsis; font-size: 0.875rem; font-weight: 600; white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; } .rr-x-row__title.is-derived { font-family: var(--font-mono); @@ -365,38 +365,38 @@ color: var(--muted-foreground); } .rr-x-row.is-fold .rr-x-row__title { - color: var(--muted-foreground); font-weight: 500; + color: var(--muted-foreground); } .rr-x-row__snippet { - font-size: 12px; - color: var(--muted-foreground); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; display: flex; - align-items: baseline; gap: 7px; + align-items: baseline; min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + font-size: 12px; + color: var(--muted-foreground); + white-space: nowrap; } .rr-x-role { + flex-shrink: 0; + padding: 1px 5px; font-family: var(--font-mono); font-size: 9px; - letter-spacing: 0.06em; + color: var(--muted-foreground); text-transform: uppercase; - padding: 1px 5px; + letter-spacing: 0.06em; box-shadow: inset 0 0 0 1px var(--border-strong); - color: var(--muted-foreground); - flex-shrink: 0; } /* ─── Empty / notes ─── */ .rr-x-empty { - padding: 32px 0; display: flex; flex-direction: column; gap: 8px; align-items: flex-start; + padding: 32px 0; border-bottom: 1px solid var(--border); } .rr-x-empty__line { @@ -410,23 +410,23 @@ color: var(--muted-foreground); } .rr-x-sheet-title { - white-space: nowrap; + min-width: 0; overflow: hidden; text-overflow: ellipsis; - min-width: 0; + white-space: nowrap; } /* ─── The record type system ─── */ /* Derived-title kicker — one grammar, never shaming */ .rr-x-kicker { + margin-right: 8px; font-family: var(--font-mono); font-size: 0.62rem; - letter-spacing: 0.04em; - text-transform: uppercase; - color: var(--muted-foreground); - margin-right: 8px; font-weight: 400; + color: var(--muted-foreground); + text-transform: uppercase; + letter-spacing: 0.04em; } .rr-x-row__title.is-derived .rr-x-kicker { margin-right: 6px; @@ -444,16 +444,16 @@ font-size: 2rem; font-weight: 600; font-variant-numeric: tabular-nums; - letter-spacing: -0.02em; line-height: 1; + letter-spacing: -0.02em; } .rr-hero__amount.is-neg { color: var(--foreground); } .rr-hero__cap { font-size: 0.8rem; - color: var(--foreground); font-weight: 500; + color: var(--foreground); } .rr-hero__wire { font-size: 0.66rem; @@ -462,9 +462,9 @@ /* Inline image field */ .rr-rec-image { + display: block; width: 100%; height: 150px; - display: block; margin-bottom: 14px; } @@ -474,15 +474,15 @@ flex-direction: column; gap: 6px; padding: 12px 14px; - background: var(--muted); margin-bottom: 12px; + background: var(--muted); } .rr-bodytext__label { font-size: 0.66rem; font-weight: 600; - letter-spacing: 0.04em; - text-transform: uppercase; color: var(--muted-foreground); + text-transform: uppercase; + letter-spacing: 0.04em; } .rr-bodytext__text { margin: 0; @@ -524,40 +524,40 @@ color: var(--muted-foreground); } .rr-fld__val { + min-width: 0; font-family: var(--font-mono); font-size: 0.8rem; text-align: right; overflow-wrap: anywhere; - min-width: 0; } .rr-fld__val.is-num { - font-variant-numeric: tabular-nums; font-weight: 600; + font-variant-numeric: tabular-nums; white-space: nowrap; } /* ─── Lens & keep (carried from vault view) ─── */ .rr-ex-lens { display: flex; - align-items: center; flex-wrap: wrap; gap: 8px; + align-items: center; padding: 12px 18px 0; } .rr-ex-lens__label { font-family: var(--font-mono); font-size: 10px; - letter-spacing: 0.1em; - text-transform: uppercase; color: var(--muted-foreground); + text-transform: uppercase; + letter-spacing: 0.1em; } .rr-ex-keep { - margin-top: 10px; - border-top: 1px solid var(--border); - padding-top: 10px; display: flex; flex-direction: column; gap: 3px; + padding-top: 10px; + margin-top: 10px; + border-top: 1px solid var(--border); } .rr-ex-keep__label { font-size: 11.5px; @@ -574,15 +574,15 @@ color: var(--muted-foreground); } .rr-ex-alone { - margin: 0; padding: 12px 18px 0; + margin: 0; font-size: 12.5px; line-height: 1.55; color: var(--muted-foreground); } .rr-ex-alone b { - color: var(--foreground); font-weight: 600; + color: var(--foreground); } /* ─── Responsive ─── */ diff --git a/docs/design-system/ink-carbon/project/recordroom/image-slot.js b/docs/design-system/ink-carbon/project/recordroom/image-slot.js index e87f23e35..069e2665a 100644 --- a/docs/design-system/ink-carbon/project/recordroom/image-slot.js +++ b/docs/design-system/ink-carbon/project/recordroom/image-slot.js @@ -77,14 +77,16 @@ let loadP = null; function load() { - if (loadP) return loadP; + if (loadP) { + return loadP; + } loadP = fetch(STATE_FILE) .then((r) => (r.ok ? r.json() : null)) .then((j) => { // Merge: sidecar loses to any in-memory change that raced ahead of // the fetch (drop or clear) so neither is clobbered by hydration. if (j && typeof j === "object") { - const merged = Object.assign({}, j, slots); + const merged = { ...j, ...slots }; // A framing-only write that raced ahead of hydration must not // drop a user image that's only on disk — inherit u from the // sidecar for any in-memory entry that lacks one. @@ -93,7 +95,9 @@ merged[k].u = typeof j[k] === "string" ? j[k] : j[k].u; } } - for (const id of tombstones) delete merged[id]; + for (const id of tombstones) { + delete merged[id]; + } slots = merged; } tombstones.clear(); @@ -118,7 +122,9 @@ return; } const w = window.omelette && window.omelette.writeFile; - if (!w) return; + if (!w) { + return; + } saving = true; Promise.resolve(w(STATE_FILE, JSON.stringify(slots))) .catch(() => {}) @@ -138,25 +144,34 @@ // data-URL string; newer ones store {u, s, x, y}. Either shape is valid. function getSlot(id) { const v = slots[id]; - if (!v) return null; - return typeof v === "string" ? { u: v, s: 1, x: 0, y: 0 } : v; + if (!v) { + return null; + } + return typeof v === "string" ? { s: 1, u: v, x: 0, y: 0 } : v; } function setSlot(id, val) { - if (!id) return; + if (!id) { + return; + } if (val) { slots[id] = val; tombstones.delete(id); } else { delete slots[id]; - if (!loaded) tombstones.add(id); + if (!loaded) { + tombstones.add(id); + } } subs.forEach((fn) => fn()); // A drop is rare + high-value — write immediately so nav-away can't lose // it. Gate on the initial read so we don't overwrite a sidecar we haven't // merged yet; the merge in load() keeps this change once the read lands. - if (loaded) save(); - else load().then(save); + if (loaded) { + save(); + } else { + load().then(save); + } } // ── Image downscale ───────────────────────────────────────────────────── @@ -305,13 +320,18 @@ this._exitReframe(false); this._gen++; this._local = null; - if (this.id) setSlot(this.id, null); - else this._render(); + if (this.id) { + setSlot(this.id, null); + } else { + this._render(); + } } }); this._input.addEventListener("change", () => { const f = this._input.files && this._input.files[0]; - if (f) this._ingest(f); + if (f) { + this._ingest(f); + } this._input.value = ""; }); // naturalWidth/Height aren't known until load — re-apply so the cover @@ -320,17 +340,24 @@ // Gated on editable + fit=cover so share links and contain/fill slots // stay static. this.addEventListener("dblclick", (e) => { - if (!this.hasAttribute("data-editable") || !this._reframes()) return; + if (!(this.hasAttribute("data-editable") && this._reframes())) { + return; + } e.preventDefault(); - if (this.hasAttribute("data-reframe")) this._exitReframe(true); - else this._enterReframe(); + if (this.hasAttribute("data-reframe")) { + this._exitReframe(true); + } else { + this._enterReframe(); + } }); // Pan + resize both originate on the spill layer. A handle pointerdown // drives an aspect-locked resize anchored at the opposite corner; any // other pointerdown on the spill pans. Offsets are frame-% so a // reframed slot survives responsive resize / PPTX export. this._spill.addEventListener("pointerdown", (e) => { - if (e.button !== 0 || !this.hasAttribute("data-reframe")) return; + if (e.button !== 0 || !this.hasAttribute("data-reframe")) { + return; + } e.preventDefault(); e.stopPropagation(); this._spill.setPointerCapture(e.pointerId); @@ -400,14 +427,18 @@ this.addEventListener( "wheel", (e) => { - if (!this.hasAttribute("data-reframe")) return; + if (!this.hasAttribute("data-reframe")) { + return; + } e.preventDefault(); const r = this.getBoundingClientRect(); const cx = ((e.clientX - r.left) / r.width) * 100 - 50; const cy = ((e.clientY - r.top) / r.height) * 100 - 50; const prev = this._view.s; - const next = clampS(prev * Math.pow(1.0015, -e.deltaY)); - if (next === prev) return; + const next = clampS(prev * 1.0015 ** -e.deltaY); + if (next === prev) { + return; + } const k = next / prev; this._view.s = next; this._view.x = cx * (1 - k) + this._view.x * k; @@ -422,7 +453,7 @@ connectedCallback() { // Warn once per page — an id-less slot works for the session but // cannot persist, and two id-less slots would share nothing. - if (!this.id && !ImageSlot._warned) { + if (!(this.id || ImageSlot._warned)) { ImageSlot._warned = true; console.warn(" without an id will not persist its dropped image."); } @@ -457,7 +488,9 @@ } _enterReframe() { - if (this.hasAttribute("data-reframe")) return; + if (this.hasAttribute("data-reframe")) { + return; + } this.setAttribute("data-reframe", ""); this._applyView(); // Close on click outside (the spill handler stopPropagation()s so @@ -465,29 +498,45 @@ // on the instance so _exitReframe / disconnectedCallback can detach // exactly what was attached. this._outside = (e) => { - if (e.composedPath && e.composedPath().includes(this)) return; + if (e.composedPath && e.composedPath().includes(this)) { + return; + } this._exitReframe(true); }; this._esc = (e) => { - if (e.key === "Escape") this._exitReframe(true); + if (e.key === "Escape") { + this._exitReframe(true); + } }; document.addEventListener("pointerdown", this._outside, true); document.addEventListener("keydown", this._esc, true); } _exitReframe(commit) { - if (!this.hasAttribute("data-reframe")) return; - if (this._dragUp) this._dragUp(); + if (!this.hasAttribute("data-reframe")) { + return; + } + if (this._dragUp) { + this._dragUp(); + } this.removeAttribute("data-reframe"); this.removeAttribute("data-panning"); - if (this._outside) document.removeEventListener("pointerdown", this._outside, true); - if (this._esc) document.removeEventListener("keydown", this._esc, true); + if (this._outside) { + document.removeEventListener("pointerdown", this._outside, true); + } + if (this._esc) { + document.removeEventListener("keydown", this._esc, true); + } this._outside = this._esc = null; - if (commit) this._commitView(); + if (commit) { + this._commitView(); + } } attributeChangedCallback() { - if (this.shadowRoot) this._render(); + if (this.shadowRoot) { + this._render(); + } } // handleEvent — one listener object for all four drag events keeps the @@ -497,8 +546,12 @@ // Without preventDefault the browser never fires 'drop'. e.preventDefault(); e.stopPropagation(); - if (e.dataTransfer) e.dataTransfer.dropEffect = "copy"; - if (e.type === "dragenter") this._depth++; + if (e.dataTransfer) { + e.dataTransfer.dropEffect = "copy"; + } + if (e.type === "dragenter") { + this._depth++; + } this.setAttribute("data-over", ""); } else if (e.type === "dragleave") { // dragenter/leave fire for every descendant crossing — count depth @@ -513,7 +566,9 @@ this._depth = 0; this.removeAttribute("data-over"); const f = e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0]; - if (f) this._ingest(f); + if (f) { + this._ingest(f); + } } } @@ -530,11 +585,13 @@ try { const w = this.clientWidth || this.offsetWidth || MAX_DIM; const url = await toDataUrl(file, w); - if (gen !== this._gen) return; + if (gen !== this._gen) { + return; + } // Only exit reframe once the new image is in hand — a rejected type // or decode failure leaves the in-progress crop untouched. this._exitReframe(false); - const val = { u: url, s: 1, x: 0, y: 0 }; + const val = { s: 1, u: url, x: 0, y: 0 }; setSlot(this.id || "", val); // Keep a session-local copy for id-less slots so the drop still // shows, even though it cannot persist. @@ -543,7 +600,9 @@ this._render(); } } catch (err) { - if (gen !== this._gen) return; + if (gen !== this._gen) { + return; + } this._setError("Could not read that image."); console.warn(" ingest failed:", err); } @@ -554,7 +613,9 @@ this._err.remove(); this._err = null; } - if (!msg) return; + if (!msg) { + return; + } const d = document.createElement("div"); d.className = "err"; d.textContent = msg; @@ -584,14 +645,18 @@ ih = this._img.naturalHeight; const fw = this.clientWidth, fh = this.clientHeight; - if (!iw || !ih || !fw || !fh) return null; - return { iw, ih, fw, fh, base: Math.max(fw / iw, fh / ih) }; + if (!(iw && ih && fw && fh)) { + return null; + } + return { base: Math.max(fw / iw, fh / ih), fh, fw, ih, iw }; } _clampView() { // Pan range on each axis is half the overflow past the frame edge. const g = this._geom(); - if (!g) return; + if (!g) { + return; + } const mx = Math.max(0, ((g.iw * g.base * this._view.s) / g.fw - 1) * 50); const my = Math.max(0, ((g.ih * g.base * this._view.s) / g.fh - 1) * 50); this._view.x = Math.max(-mx, Math.min(mx, this._view.x)); @@ -634,11 +699,14 @@ _commitView() { const v = { s: this._view.s, x: this._view.x, y: this._view.y }; - if (this._userUrl) v.u = this._userUrl; + if (this._userUrl) { + v.u = this._userUrl; + } // Framing-only (no u) persists too so an author-src slot remembers its // crop; clearing the sidecar still falls through to src=. - if (this.id) setSlot(this.id, v); - else { + if (this.id) { + setSlot(this.id, v); + } else { this._local = v; } } @@ -651,10 +719,12 @@ const mask = this.getAttribute("mask"); const shape = (this.getAttribute("shape") || "rounded").toLowerCase(); let radius = ""; - if (shape === "circle") radius = "50%"; - else if (shape === "pill") radius = "9999px"; - else if (shape === "rounded") { - const n = parseFloat(this.getAttribute("radius")); + if (shape === "circle") { + radius = "50%"; + } else if (shape === "pill") { + radius = "9999px"; + } else if (shape === "rounded") { + const n = Number.parseFloat(this.getAttribute("radius")); radius = (Number.isFinite(n) ? n : 12) + "px"; } this._frame.style.borderRadius = mask ? "" : radius; @@ -672,7 +742,9 @@ // data:image/ URLs from it. The `src` attribute is author-controlled // (the author wrote it into the HTML) so it passes through unchanged. let stored = this.id ? getSlot(this.id) : this._local; - if (stored && stored.u && !/^data:image\//i.test(stored.u)) stored = null; + if (stored && stored.u && !/^data:image\//i.test(stored.u)) { + stored = null; + } const srcAttr = this.getAttribute("src") || ""; this._userUrl = (stored && stored.u) || null; const url = this._userUrl || srcAttr; diff --git a/docs/design-system/ink-carbon/project/recordroom/mobile.css b/docs/design-system/ink-carbon/project/recordroom/mobile.css index eaf257e97..6638524dc 100644 --- a/docs/design-system/ink-carbon/project/recordroom/mobile.css +++ b/docs/design-system/ink-carbon/project/recordroom/mobile.css @@ -16,8 +16,8 @@ display: none; } .rr-head__brand { - align-items: center; gap: 10px; + align-items: center; font-size: 14px; font-weight: 800; letter-spacing: -0.01em; @@ -28,22 +28,22 @@ .rr-drawer-overlay { position: fixed; inset: 0; - background: oklch(0 0 0 / 0.5); z-index: 70; + background: oklch(0 0 0 / 0.5); animation: fade-in var(--duration-fast) var(--ease-enter) both; } .rr-drawer { position: absolute; - left: 0; top: 0; bottom: 0; - width: min(300px, 84vw); - background: color-mix(in oklab, var(--background) 88%, oklch(0 0 0)); - border-right: 1px solid var(--border-strong); - padding: 18px 0 14px; + left: 0; display: flex; flex-direction: column; + width: min(300px, 84vw); + padding: 18px 0 14px; overflow-y: auto; + background: color-mix(in oklab, var(--background) 88%, oklch(0 0 0)); + border-right: 1px solid var(--border-strong); } @media (prefers-reduced-motion: no-preference) { .rr-drawer { @@ -63,8 +63,8 @@ .rr-drawer__nav { display: flex; flex-direction: column; - padding: 10px; gap: 2px; + padding: 10px; } .rr-drawer .rr-nav-item { padding: 12px 12px; @@ -92,8 +92,8 @@ overflow-x: hidden; } .rr-content :is(.pdpp-sheet__serial, .rr-attn__meta, .pdpp-typed-sm) { - white-space: normal; overflow-wrap: anywhere; + white-space: normal; } .rr-hero__meta { white-space: normal; @@ -120,8 +120,8 @@ } .rr-page-head { flex-direction: column; - align-items: flex-start; gap: 4px; + align-items: flex-start; margin-bottom: 18px; } .rr-page-head__s { @@ -159,16 +159,16 @@ } .rr-cols-runs .pdpp-data-row__detail, .rr-cols-schedules .pdpp-data-row__detail { - grid-column: 1 / -1; grid-row: 2; + grid-column: 1 / -1; white-space: normal; } .rr-cols-runs .pdpp-data-row__meta { display: none; } .rr-cols-schedules .pdpp-data-row__meta { - grid-column: 1 / -1; grid-row: 3; + grid-column: 1 / -1; text-align: left; } @@ -243,9 +243,9 @@ grid-template-columns: minmax(0, 1fr); } .rr-attn__side { + flex-direction: row; grid-row: auto; grid-column: 1; - flex-direction: row; align-items: center; justify-content: flex-start; } @@ -259,8 +259,8 @@ /* ── Stat band: 2-up grid ── */ .pdpp-band { - grid-auto-flow: row; grid-template-columns: 1fr 1fr; + grid-auto-flow: row; } .pdpp-band__cell { border-left: 0; @@ -279,8 +279,8 @@ } .rr-hero__sheet { flex-direction: column; - align-items: stretch; gap: 14px; + align-items: stretch; padding: 16px 18px; } .rr-hero__sheet .pdpp-btn { @@ -303,8 +303,8 @@ /* ── The ceremony becomes a bottom sheet ── */ .rr-overlay { - padding: 10px; align-items: flex-end; + padding: 10px; } .rr-ceremony { width: 100%; @@ -348,8 +348,8 @@ /* Incoming banner stacks like the hero */ .rr-incoming__sheet { flex-direction: column; - align-items: stretch; gap: 10px; + align-items: stretch; } .rr-incoming__sheet .pdpp-btn--sm { align-self: flex-start; @@ -380,7 +380,7 @@ padding: 14px 16px; } .pdpp-sheet__foot { - padding: 11px 16px; flex-wrap: wrap; + padding: 11px 16px; } } diff --git a/docs/design-system/ink-carbon/project/recordroom/overview.css b/docs/design-system/ink-carbon/project/recordroom/overview.css index abb408ffb..2dc945226 100644 --- a/docs/design-system/ink-carbon/project/recordroom/overview.css +++ b/docs/design-system/ink-carbon/project/recordroom/overview.css @@ -14,46 +14,46 @@ /* ─── The hero: the one truth, set large ─── */ .rr-stand-hero { + position: relative; display: flex; flex-direction: column; gap: 12px; padding: 30px 32px 28px; border: 1px solid var(--border-strong); - position: relative; } .rr-stand-hero__kicker { font-family: var(--font-mono); font-size: 0.7rem; - letter-spacing: 0.12em; - text-transform: uppercase; color: var(--muted-foreground); + text-transform: uppercase; + letter-spacing: 0.12em; } .rr-stand-hero__line { + max-width: 22ch; + margin: 0; font-family: var(--font-sans); font-size: 2.4rem; font-weight: 700; line-height: 1.1; letter-spacing: -0.025em; - margin: 0; - max-width: 22ch; text-wrap: balance; } .rr-stand-hero__line em { font-style: normal; } .rr-stand-hero__sub { + max-width: 60ch; + margin: 2px 0 0; font-size: 0.95rem; line-height: 1.55; color: var(--muted-foreground); - margin: 2px 0 0; - max-width: 60ch; } .rr-stand-hero__foot { display: flex; - align-items: center; + flex-wrap: wrap; gap: 16px; + align-items: center; margin-top: 8px; - flex-wrap: wrap; } /* Tone: calm — the one place warmth is spent */ @@ -65,18 +65,18 @@ color: var(--human); } .rr-stand-hero__seal { + display: inline-flex; + gap: 8px; + align-items: center; font-family: var(--font-sans); font-size: 0.95rem; font-weight: 700; color: var(--human); - display: inline-flex; - align-items: center; - gap: 8px; } .rr-stand-hero__seal::before { - content: ""; width: 9px; height: 9px; + content: ""; background: var(--human); border-radius: 50%; } @@ -86,19 +86,19 @@ box-shadow: inset 3px 0 0 var(--human); } .rr-stand-hero.is-decide .rr-stand-hero__line em { - color: var(--human); font-weight: 700; + color: var(--human); } /* Tone: alarm — something needs you */ .rr-stand-hero.is-alarm { + background: var(--warning-tint); border-color: var(--warning); box-shadow: inset 3px 0 0 var(--warning); - background: var(--warning-tint); } .rr-stand-hero.is-alarm .rr-stand-hero__line em { - color: var(--warning); font-weight: 700; + color: var(--warning); } /* ─── What can act as you — the bearer tier, weightier on purpose ─── */ @@ -119,32 +119,32 @@ font-weight: 700; } .rr-bearer__tag { + padding: 2px 7px; font-family: var(--font-mono); font-size: 0.6rem; font-weight: 600; - letter-spacing: 0.06em; - text-transform: uppercase; color: var(--human); - padding: 2px 7px; - box-shadow: inset 0 0 0 1px var(--human); + text-transform: uppercase; + letter-spacing: 0.06em; white-space: nowrap; + box-shadow: inset 0 0 0 1px var(--human); } .rr-bearer__how { + grid-row: 2; + grid-column: 1 / 4; font-family: var(--font-mono); font-size: 0.68rem; color: var(--muted-foreground); - grid-column: 1 / 4; - grid-row: 2; } .rr-bearer__row .rr-rel__revoke { grid-row: 1 / span 2; grid-column: 4; - opacity: 1; align-self: center; + opacity: 1; } .rr-bearer__note { - margin: 0; padding: 11px 16px 13px; + margin: 0; font-size: 0.78rem; line-height: 1.55; color: var(--muted-foreground); @@ -163,18 +163,18 @@ } .rr-stand-block__head { display: flex; - justify-content: space-between; - align-items: baseline; gap: 12px; - border-bottom: 1px solid var(--border-strong); + align-items: baseline; + justify-content: space-between; padding-bottom: 8px; + border-bottom: 1px solid var(--border-strong); } .rr-stand-block__title { + margin: 0; font-family: var(--font-sans); font-size: 1.05rem; font-weight: 700; letter-spacing: -0.01em; - margin: 0; } /* Who can read you — relationships, not scope IDs */ @@ -186,9 +186,9 @@ display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 2px 12px; + align-items: baseline; padding: 12px 0; border-bottom: 1px solid var(--border); - align-items: baseline; } .rr-rel:last-child { border-bottom: 0; @@ -200,21 +200,21 @@ .rr-rel__reads { grid-column: 1; font-size: 0.84rem; - color: var(--muted-foreground); line-height: 1.45; + color: var(--muted-foreground); } .rr-rel__revoke { grid-row: 1 / span 2; grid-column: 2; align-self: center; + padding: 4px 2px; font-family: var(--font-sans); font-size: 0.78rem; font-weight: 600; color: var(--destructive); + cursor: pointer; background: none; border: 0; - padding: 4px 2px; - cursor: pointer; opacity: 0; transition: opacity var(--motion-state); } @@ -236,16 +236,16 @@ display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 10px; + align-items: baseline; padding: 11px 0; border-bottom: 1px solid var(--border); - align-items: baseline; } .rr-lately__row:last-child { border-bottom: 0; } .rr-lately__row.is-deny { - box-shadow: inset 2px 0 0 var(--warning); padding-left: 10px; + box-shadow: inset 2px 0 0 var(--warning); } .rr-lately__text { font-size: 0.84rem; @@ -261,8 +261,8 @@ /* All clear */ .rr-allclear { display: flex; - align-items: center; gap: 12px; + align-items: center; padding: 16px 18px; border: 1px solid var(--border); box-shadow: inset 3px 0 0 var(--human); @@ -272,9 +272,9 @@ color: var(--foreground); } .rr-stand-empty { + margin: 8px 0 0; font-size: 0.84rem; color: var(--muted-foreground); - margin: 8px 0 0; } /* ─── Responsive ─── */ diff --git a/docs/design-system/ink-carbon/project/recordroom/rr-app.jsx b/docs/design-system/ink-carbon/project/recordroom/rr-app.jsx index a5d276706..b67072ac3 100644 --- a/docs/design-system/ink-carbon/project/recordroom/rr-app.jsx +++ b/docs/design-system/ink-carbon/project/recordroom/rr-app.jsx @@ -41,9 +41,7 @@ const STREAMS = [ { - id: "pay_statements", connector: "Northstar HR", - records: "312", fields: [ "employer", "period_start", @@ -54,163 +52,165 @@ "benefits_detail", "bank_routing", ], + id: "pay_statements", + records: "312", }, { - id: "employment", connector: "Northstar HR", - records: "4", fields: ["employer", "title", "start_date", "end_date", "manager_contact"], + id: "employment", + records: "4", }, { - id: "transactions", connector: "First Meridian", - records: "41,203", fields: ["date", "amount", "merchant", "category", "account_ref", "memo"], + id: "transactions", + records: "41,203", }, { - id: "listening_history", connector: "Tonal", - records: "6,597", fields: ["track", "artist", "played_at", "device", "playlist_ref"], + id: "listening_history", + records: "6,597", }, { - id: "tax_docs", connector: "Northstar HR", - records: "12", fields: ["doc_type", "tax_year", "employer", "document_blob"], + id: "tax_docs", + records: "12", }, ]; const BASE_GRANTS = [ { - id: "grt_lngvw_01", client: "Longview Planning", + declined: ["tax_docs.read"], + expiresFull: "2026-12-14 09:22Z", + expiry: "exp 2026-12-14", + id: "grt_lngvw_01", + issued: "2025-10-14 09:22Z", + projections: { + employment: ["employer", "start_date", "end_date"], + pay_statements: ["employer", "period_start", "period_end", "gross_pay", "net_pay"], + }, purpose: "long_term_financial_planning", scopes: [ { name: "pay_statements.read", terms: "append only · 2 yrs" }, { name: "employment.read", terms: "current + 5 yrs" }, ], - declined: ["tax_docs.read"], status: "active", - issued: "2025-10-14 09:22Z", - expiry: "exp 2026-12-14", - expiresFull: "2026-12-14 09:22Z", - projections: { - pay_statements: ["employer", "period_start", "period_end", "gross_pay", "net_pay"], - employment: ["employer", "start_date", "end_date"], - }, }, { - id: "grt_cncrt_02", client: "Concert Recommendations", - purpose: "live_event_suggestions", - scopes: [{ name: "listening_history.read", terms: "rolling 12 mo" }], declined: [], - status: "continuous", - issued: "2026-01-30 18:04Z", - expiry: "continuous", expiresFull: "renews monthly · next 2026-07-01", + expiry: "continuous", + id: "grt_cncrt_02", + issued: "2026-01-30 18:04Z", projections: { listening_history: ["track", "artist", "played_at"] }, + purpose: "live_event_suggestions", + scopes: [{ name: "listening_history.read", terms: "rolling 12 mo" }], + status: "continuous", }, { - id: "grt_taxpr_03", client: "TaxPrep Co", - purpose: "annual_filing_2025", - scopes: [{ name: "tax_docs.read", terms: "single use" }], declined: [], - status: "expiring", + expiresFull: "2026-06-12 11:40Z", + expiry: "exp 2026-06-12", hoursLeft: 26, + id: "grt_taxpr_03", issued: "2026-06-09 11:40Z", - expiry: "exp 2026-06-12", - expiresFull: "2026-06-12 11:40Z", projections: { tax_docs: ["doc_type", "tax_year", "employer"] }, + purpose: "annual_filing_2025", + scopes: [{ name: "tax_docs.read", terms: "single use" }], + status: "expiring", }, { - id: "grt_xwise_09", client: "Crosswise Ads", - purpose: "ad_personalization", - scopes: [{ name: "transactions.read", terms: "90 d window" }], declined: [], - status: "revoked", - issued: "2026-02-11 08:15Z", - revokedOn: "2026-05-02", - revokedFull: "2026-05-02 14:40Z · by owner", - expiry: "—", expiresFull: "—", + expiry: "—", + id: "grt_xwise_09", + issued: "2026-02-11 08:15Z", projections: { transactions: ["date", "amount", "merchant"] }, + purpose: "ad_personalization", + revokedFull: "2026-05-02 14:40Z · by owner", + revokedOn: "2026-05-02", + scopes: [{ name: "transactions.read", terms: "90 d window" }], + status: "revoked", }, ]; const BASE_LOG = [ { - t: "2026-06-11 07:58Z", kind: "read", + ref: "grt_lngvw_01", + t: "2026-06-11 07:58Z", verb: "read", what: "pay_statements · 12 records · 5/8 fields", - ref: "grt_lngvw_01", }, { - t: "2026-06-11 06:02Z", kind: "read", + ref: "grt_cncrt_02", + t: "2026-06-11 06:02Z", verb: "read", what: "listening_history · 214 records · 3/5 fields", - ref: "grt_cncrt_02", }, { - t: "2026-06-10 22:17Z", kind: "read", + ref: "grt_lngvw_01", + t: "2026-06-10 22:17Z", verb: "read", what: "employment · 4 records · 3/5 fields", - ref: "grt_lngvw_01", }, { - t: "2026-06-10 22:17Z", kind: "deny", + ref: "grt_lngvw_01", + t: "2026-06-10 22:17Z", verb: "deny", what: "tax_docs read attempt · scope not granted", - ref: "grt_lngvw_01", }, { - t: "2026-06-09 11:40Z", kind: "consent", + ref: "grt_taxpr_03", + t: "2026-06-09 11:40Z", verb: "grant", what: "tax_docs.read · single use · TaxPrep Co", - ref: "grt_taxpr_03", }, { - t: "2026-05-02 14:40Z", kind: "revoke", + ref: "grt_xwise_09", + t: "2026-05-02 14:40Z", verb: "revoke", what: "transactions.read · Crosswise Ads · by owner", - ref: "grt_xwise_09", }, { - t: "2026-05-02 14:39Z", kind: "deny", + ref: "grt_xwise_09", + t: "2026-05-02 14:39Z", verb: "deny", what: "transactions read attempt · grant suspended", - ref: "grt_xwise_09", }, ]; const INCOMING = { - id: "req_atlas_7f2k", client: "Atlas Mortgage", + id: "req_atlas_7f2k", purposeHuman: "mortgage pre-approval", scopes: [ { + allowed: true, + desc: "Employer, pay period, gross and net pay", name: "pay_statements.read", terms: "append only · 90 d", - desc: "Employer, pay period, gross and net pay", - allowed: true, }, { + allowed: true, + desc: "Employers and dates — no salary history", name: "employment.read", terms: "current + 5 yrs", - desc: "Employers and dates — no salary history", - allowed: true, }, - { name: "transactions.read", terms: "90 d window", desc: "Spending detail from First Meridian", allowed: false }, + { allowed: false, desc: "Spending detail from First Meridian", name: "transactions.read", terms: "90 d window" }, ], }; @@ -236,9 +236,9 @@ } const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/ { - theme: "dark", - density: "comfortable", carbonOffset: 9, + density: "comfortable", + theme: "dark", } /*EDITMODE-END*/; const NAV = [ @@ -259,16 +259,16 @@ ]; const HEADS = { - overview: ["Overview", "where you stand"], - explore: ["Explore", "the reading room · 10 connections · only you see this"], - sources: ["Sources", "the loading dock · 10 instances · what arrives, from where, configured how"], - traces: ["Traces", "every request, accounted for"], - grants: ["Grants", ""], - syncs: ["Syncs", "is your data arriving · schedule + result, per stream"], connect: ["Connect AI apps", "MCP · reads flow through grants"], deployment: ["Deployment", "readiness · endpoints · owner tokens"], - exporters: ["Device exporters", "your devices, pushing home"], events: ["Event subscriptions", "webhooks on protocol events"], + explore: ["Explore", "the reading room · 10 connections · only you see this"], + exporters: ["Device exporters", "your devices, pushing home"], + grants: ["Grants", ""], + overview: ["Overview", "where you stand"], + sources: ["Sources", "the loading dock · 10 instances · what arrives, from where, configured how"], + syncs: ["Syncs", "is your data arriving · schedule + result, per stream"], + traces: ["Traces", "every request, accounted for"], }; /* ─── App ─── */ @@ -308,9 +308,13 @@ }, []); useEffect(() => { - if (!navOpen) return undefined; + if (!navOpen) { + return; + } function onKey(e) { - if (e.key === "Escape") setNavOpen(false); + if (e.key === "Escape") { + setNavOpen(false); + } } window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); @@ -318,9 +322,13 @@ // Keyboard-first ledger: ↑↓ move selection, Escape backs out of a revoke. useEffect(() => { - if (view !== "grants" || paletteOpen || requestState === "open") return undefined; + if (view !== "grants" || paletteOpen || requestState === "open") { + return; + } function onKey(e) { - if (e.target && /^(INPUT|TEXTAREA|SELECT)$/.test(e.target.tagName)) return; + if (e.target && /^(INPUT|TEXTAREA|SELECT)$/.test(e.target.tagName)) { + return; + } if (e.key === "ArrowDown" || e.key === "ArrowUp") { e.preventDefault(); setSelected((cur) => { @@ -345,9 +353,9 @@ revokedIds.includes(g.id) && g.status !== "revoked" ? { ...g, - status: "revoked", - revokedOn: g.revokedOn || "2026-06-11", revokedFull: g.revokedFull || nowStamp() + " · by owner", + revokedOn: g.revokedOn || "2026-06-11", + status: "revoked", } : g ); @@ -370,20 +378,20 @@ const allowed = reqScopes.filter((s) => s.allowed); const declined = reqScopes.filter((s) => !s.allowed).map((s) => s.name); const newGrant = { - id: "grt_atlas_05", client: INCOMING.client, - purpose: "mortgage_preapproval", - scopes: allowed.map(({ name, terms }) => ({ name, terms })), declined, - status: "active", - issued: nowStamp(), - expiry: "exp 2026-09-09", expiresFull: "2026-09-09 · 90 d term", + expiry: "exp 2026-09-09", + id: "grt_atlas_05", + issued: nowStamp(), justAdded: true, projections: { - pay_statements: ["employer", "period_start", "period_end", "gross_pay", "net_pay"], employment: ["employer", "start_date", "end_date"], + pay_statements: ["employer", "period_start", "period_end", "gross_pay", "net_pay"], }, + purpose: "mortgage_preapproval", + scopes: allowed.map(({ name, terms }) => ({ name, terms })), + status: "active", }; setTimeout(() => { setExtraGrants((cur) => { @@ -392,11 +400,11 @@ return next; }); addLog({ - t: nowStamp(), kind: "consent", + ref: newGrant.id, + t: nowStamp(), verb: "grant", what: `${allowed.length} scopes · ${declined.length} declined · ${INCOMING.client}`, - ref: newGrant.id, }); setRequestState("approved"); setPressing(false); @@ -409,11 +417,11 @@ setRequestState("refused"); saveState({ requestState: "refused" }); addLog({ - t: nowStamp(), kind: "deny", + ref: INCOMING.id, + t: nowStamp(), verb: "refuse", what: `access request refused · ${INCOMING.client}`, - ref: INCOMING.id, }); } @@ -428,11 +436,11 @@ return next; }); addLog({ - t: nowStamp(), kind: "revoke", + ref: g.id, + t: nowStamp(), verb: "revoke", what: `${g.scopes.map((s) => s.name).join(" · ")} · ${g.client} · by owner`, - ref: g.id, }); setTimeout(() => setStriking(false), 520); } @@ -446,26 +454,26 @@ } function browseInExplore(conId, streamName) { - setExploreSeed({ con: conId, stream: streamName, n: Date.now() }); + setExploreSeed({ con: conId, n: Date.now(), stream: streamName }); setView("explore"); } const paletteItems = [ - ...NAV.filter((n) => n.id).map((n) => ({ label: n.label, kind: "view", run: () => setView(n.id) })), + ...NAV.filter((n) => n.id).map((n) => ({ kind: "view", label: n.label, run: () => setView(n.id) })), ...grants.map((g) => ({ - label: g.client + " — " + g.id, kind: "grant", + label: g.client + " — " + g.id, run: () => { setView("grants"); setSelected(g.id); }, })), - ...STREAMS.map((s) => ({ label: s.id, kind: "stream", run: () => setView("sources") })), - { label: "Reauthorize First Meridian", kind: "action", run: () => setView("syncs") }, + ...STREAMS.map((s) => ({ kind: "stream", label: s.id, run: () => setView("sources") })), + { kind: "action", label: "Reauthorize First Meridian", run: () => setView("syncs") }, ...(requestState === "pending" - ? [{ label: "Review Atlas Mortgage request", kind: "action", run: () => setRequestState("open") }] + ? [{ kind: "action", label: "Review Atlas Mortgage request", run: () => setRequestState("open") }] : []), - { label: "Toggle theme", kind: "action", run: () => setTweak("theme", t.theme === "dark" ? "light" : "dark") }, + { kind: "action", label: "Toggle theme", run: () => setTweak("theme", t.theme === "dark" ? "light" : "dark") }, ]; const heads = { @@ -484,7 +492,7 @@
- + Recordroom rs.okafor.recordroom.net · pdpp 0.1.0 @@ -506,7 +514,7 @@
-
+

{heads[view][0]}

{heads[view][1]} @@ -547,7 +555,7 @@ )}
- + client scopes status @@ -594,7 +602,7 @@
setNavOpen(false)}> -
+
M. Okafor rs.okafor.recordroom.net · pdpp 0.1.0 @@ -56,7 +56,7 @@ return (
) : ( @@ -280,10 +280,10 @@

{stream.id}

- + {granted.map((g) => ( {watchers.map((g) => ( + ), kicker: "A request is waiting on you", line: ( @@ -71,22 +85,10 @@ ), sub: "Nothing leaves until you say so — approve it one piece at a time.", - cta: ( - - ), + tone: "decide", }; } else if (hasFailure) { hero = { - tone: "alarm", - kicker: "One thing needs you", - line: ( - - Your bank data stopped arriving on Jun 11. - - ), - sub: "First Meridian's connection expired. Nothing you already have is lost — but nothing new arrives until you reconnect.", cta: (
{lately.map((e) => ( -
+
{e.text} {e.when}
diff --git a/docs/design-system/ink-carbon/project/recordroom/rr-record.jsx b/docs/design-system/ink-carbon/project/recordroom/rr-record.jsx index 5665c74d5..2ead4e56c 100644 --- a/docs/design-system/ink-carbon/project/recordroom/rr-record.jsx +++ b/docs/design-system/ink-carbon/project/recordroom/rr-record.jsx @@ -8,60 +8,60 @@ (() => { /* ── Lexicon: wire key → human label. Fallback prettifies snake_case. ── */ const FIELD_LABELS = { - employer: "Employer", - period_start: "Period start", - period_end: "Period end", - gross_pay: "Gross pay", - net_pay: "Net pay", - taxes_withheld: "Taxes withheld", - benefits_detail: "Benefits", - bank_routing: "Deposited to", - date: "Date", - amount: "Amount", - merchant: "Merchant", - category: "Category", account_ref: "Account", - memo: "Memo", - track: "Track", + amount: "Amount", artist: "Artist", - played_at: "Played", + bank_routing: "Deposited to", + benefits_detail: "Benefits", + breakdown: "Breakdown", + bytes: "Size", + calls: "Calls", + category: "Category", + chars: "Length", + charset: "Encoding", + commits: "Commits", + content: "Message", + content_type: "Type", + date: "Date", device: "Device", - playlist_ref: "Playlist", + doc_type: "Document", + employer: "Employer", + filename: "File", from: "From", - subject: "Subject", - received: "Received", - size: "Size", + gross_pay: "Gross pay", label: "Label", - participants: "Participants", + memo: "Memo", + merchant: "Merchant", + message_ref: "Message", messages: "Messages", - role: "Role", - session: "Session", - chars: "Length", - content: "Message", model: "Model", - charset: "Encoding", - bytes: "Size", - message_ref: "Message", - text: "Body", - repo: "Repository", - visibility: "Visibility", - pushed: "Last push", + net_pay: "Net pay", open_prs: "Open PRs", - commits: "Commits", + participants: "Participants", + period_end: "Period end", + period_start: "Period start", + pings: "Pings", + played_at: "Played", + playlist_ref: "Playlist", + prompt: "Prompt", prs_opened: "PRs opened", + pushed: "Last push", + received: "Received", + repo: "Repository", reviews: "Reviews", - title: "Title", - started: "Started", - prompt: "Prompt", - turns: "Turns", - calls: "Calls", + role: "Role", + session: "Session", + size: "Size", span: "Over", - breakdown: "Breakdown", - pings: "Pings", - filename: "File", - content_type: "Type", - doc_type: "Document", + started: "Started", + subject: "Subject", tax_year: "Tax year", + taxes_withheld: "Taxes withheld", + text: "Body", + title: "Title", + track: "Track", + turns: "Turns", + visibility: "Visibility", }; function prettify(k) { @@ -76,24 +76,24 @@ } const STREAM_NOUN = { - messages: "message", - message_bodies: "message body", - threads: "thread", attachments: "attachment", - sessions: "session", - function_calls: "tool calls", + balances: "balance", conversations: "conversation", - repositories: "repository", - user_stats: "stats snapshot", - pay_statements: "pay statement", - transactions: "transaction", - listening_history: "play", - tax_docs: "document", employment: "record", - balances: "balance", - statements: "statement", + function_calls: "tool calls", + listening_history: "play", + message_bodies: "message body", + messages: "message", + pay_statements: "pay statement", + repositories: "repository", + sessions: "session", skills: "skill", + statements: "statement", + tax_docs: "document", + threads: "thread", + transactions: "transaction", user: "record", + user_stats: "stats snapshot", }; function nounFor(stream) { return STREAM_NOUN[stream] || "record"; @@ -107,13 +107,27 @@ stream is email from Gmail but an agent turn from Codex). ── */ function kindOf(rec) { const k = new Set((rec.fields || []).map((f) => f[0])); - if (rec.image || k.has("filename") || k.has("content_type")) return "attachment"; - if (k.has("amount") || k.has("gross_pay") || k.has("net_pay")) return "money"; - if (k.has("track") || k.has("artist")) return "media"; - if (k.has("charset") && k.has("text")) return "body"; - if (k.has("role")) return "agent"; - if (k.has("from") || k.has("subject") || k.has("participants")) return "email"; - if (k.has("repo") || k.has("commits")) return "code"; + if (rec.image || k.has("filename") || k.has("content_type")) { + return "attachment"; + } + if (k.has("amount") || k.has("gross_pay") || k.has("net_pay")) { + return "money"; + } + if (k.has("track") || k.has("artist")) { + return "media"; + } + if (k.has("charset") && k.has("text")) { + return "body"; + } + if (k.has("role")) { + return "agent"; + } + if (k.has("from") || k.has("subject") || k.has("participants")) { + return "email"; + } + if (k.has("repo") || k.has("commits")) { + return "code"; + } return "generic"; } @@ -127,18 +141,24 @@ /* ── One derived-title grammar. Never "no X" — a quiet kicker + a fact. ── */ function displayTitle(rec) { - if (!rec.degraded) return { primary: rec.title, kicker: null }; + if (!rec.degraded) { + return { kicker: null, primary: rec.title }; + } const f = fieldMap(rec); const noun = nounFor(rec.stream); let hint = ""; - if (f.from) hint = "from " + f.from; - else if (f.role) hint = f.role + " turn"; - else if (f.bytes || f.charset) + if (f.from) { + hint = "from " + f.from; + } else if (f.role) { + hint = f.role + " turn"; + } else if (f.bytes || f.charset) { hint = [f.charset, f.bytes ? Math.round(String(f.bytes).replace(/[^\d]/g, "") / 1024) + " KB" : ""] .filter(Boolean) .join(" · "); - else if (f.date) hint = f.date; - return { primary: hint || noun, kicker: "untitled " + noun }; + } else if (f.date) { + hint = f.date; + } + return { kicker: "untitled " + noun, primary: hint || noun }; } /* ── Dual-key field row ── */ @@ -149,7 +169,7 @@ {labelFor(k)} {k} - {v} + {v}
); } @@ -167,20 +187,26 @@ if (kind === "money") { ["merchant", "employer", "category", "period_end", "date"].forEach((k) => { const p = present(k); - if (p) captionParts.push(p[1]); + if (p) { + captionParts.push(p[1]); + } }); } const skip = new Set(); - if (heroKey) skip.add(heroKey); - if (bodyPair) skip.add(bodyPair[0]); + if (heroKey) { + skip.add(heroKey); + } + if (bodyPair) { + skip.add(bodyPair[0]); + } const rest = pairs.filter(([k]) => !skip.has(k)); return ( {heroKey && (
- {heroVal} + {heroVal} {captionParts.length > 0 && {captionParts.slice(0, 2).join(" · ")}} {labelFor(heroKey)} · {heroKey} @@ -194,7 +220,7 @@ placeholder="Image field — drop the file to render it inline" radius="0" shape="rect" - > + /> )} {bodyPair && (
@@ -216,6 +242,6 @@ } Object.assign(window, { - RRREC: { labelFor, nounFor, kindOf, displayTitle, Field, RecordBody }, + RRREC: { displayTitle, Field, kindOf, labelFor, nounFor, RecordBody }, }); })(); diff --git a/docs/design-system/ink-carbon/project/recordroom/rr-sources.jsx b/docs/design-system/ink-carbon/project/recordroom/rr-sources.jsx index 3b20eee9c..7c6594834 100644 --- a/docs/design-system/ink-carbon/project/recordroom/rr-sources.jsx +++ b/docs/design-system/ink-carbon/project/recordroom/rr-sources.jsx @@ -43,7 +43,7 @@ const st = localRevoked.includes(c.id) ? "revoked" : c.status; return ( )} - {!revoked && !revoking && ( + {!(revoked || revoking) && (
-
+
{failing} {failing ? "need your hand" : "need attention"}
@@ -199,7 +199,7 @@ return (
- + {g.con} {g.cin} @@ -220,7 +220,7 @@ return ( @@ -324,13 +335,13 @@ function TweakSlider({ label, value, min = 0, max = 100, step = 1, unit = "", on return ( onChange(Number(e.target.value))} step={step} + type="range" value={value} - onChange={(e) => onChange(Number(e.target.value))} /> ); @@ -343,12 +354,12 @@ function TweakToggle({ label, value, onChange }) { {label}
@@ -379,9 +390,9 @@ function TweakRadio({ label, value, options, onChange }) { const m = options.find((o) => String(typeof o === "object" ? o.value : o) === s); return m === undefined ? s : typeof m === "object" ? m.value : m; }; - return onChange(resolve(s))} />; + return onChange(resolve(s))} options={options} value={value} />; } - const opts = options.map((o) => (typeof o === "object" ? o : { value: o, label: o })); + const opts = options.map((o) => (typeof o === "object" ? o : { label: o, value: o })); const idx = Math.max( 0, opts.findIndex((o) => o.value === value) @@ -398,11 +409,17 @@ function TweakRadio({ label, value, options, onChange }) { const onPointerDown = (e) => { setDragging(true); const v0 = segAt(e.clientX); - if (v0 !== valueRef.current) onChange(v0); + if (v0 !== valueRef.current) { + onChange(v0); + } const move = (ev) => { - if (!trackRef.current) return; + if (!trackRef.current) { + return; + } const v = segAt(ev.clientX); - if (v !== valueRef.current) onChange(v); + if (v !== valueRef.current) { + onChange(v); + } }; const up = () => { setDragging(false); @@ -416,17 +433,17 @@ function TweakRadio({ label, value, options, onChange }) { return (
{opts.map((o) => ( - ))} @@ -438,7 +455,7 @@ function TweakRadio({ label, value, options, onChange }) { function TweakSelect({ label, value, options, onChange }) { return ( - onChange(e.target.value)} value={value}> {options.map((o) => { const v = typeof o === "object" ? o.value : o; const l = typeof o === "object" ? o.label : o; @@ -458,10 +475,10 @@ function TweakText({ label, value, placeholder, onChange }) { onChange(e.target.value)} + placeholder={placeholder} type="text" value={value} - placeholder={placeholder} - onChange={(e) => onChange(e.target.value)} /> ); @@ -469,14 +486,18 @@ function TweakText({ label, value, placeholder, onChange }) { function TweakNumber({ label, value, min, max, step = 1, unit = "", onChange }) { const clamp = (n) => { - if (min != null && n < min) return min; - if (max != null && n > max) return max; + if (min != null && n < min) { + return min; + } + if (max != null && n > max) { + return max; + } return n; }; - const startRef = React.useRef({ x: 0, val: 0 }); + const startRef = React.useRef({ val: 0, x: 0 }); const onScrubStart = (e) => { e.preventDefault(); - startRef.current = { x: e.clientX, val: value }; + startRef.current = { val: value, x: e.clientX }; const decimals = (String(step).split(".")[1] || "").length; const move = (ev) => { const dx = ev.clientX - startRef.current.x; @@ -497,12 +518,12 @@ function TweakNumber({ label, value, min, max, step = 1, unit = "", onChange }) {label} onChange(clamp(Number(e.target.value)))} + step={step} + type="number" + value={value} /> {unit && {unit}}
@@ -515,23 +536,25 @@ function TweakNumber({ label, value, min, max, step = 1, unit = "", onChange }) function __twkIsLight(hex) { const h = String(hex).replace("#", ""); const x = h.length === 3 ? h.replace(/./g, (c) => c + c) : h.padEnd(6, "0"); - const n = parseInt(x.slice(0, 6), 16); - if (Number.isNaN(n)) return true; + const n = Number.parseInt(x.slice(0, 6), 16); + if (Number.isNaN(n)) { + return true; + } const r = (n >> 16) & 255, g = (n >> 8) & 255, b = n & 255; - return r * 299 + g * 587 + b * 114 > 148000; + return r * 299 + g * 587 + b * 114 > 148_000; } const __TwkCheck = ({ light }) => ( -
{label}
- onChange(e.target.value)} /> + onChange(e.target.value)} type="color" value={value} />
); } @@ -568,16 +591,16 @@ function TweakColor({ label, value, options, onChange }) { const on = key(o) === cur; return ( ); } Object.assign(window, { - useTweaks, - TweaksPanel, - TweakSection, - TweakRow, - TweakSlider, - TweakToggle, + TweakButton, + TweakColor, + TweakNumber, TweakRadio, + TweakRow, + TweakSection, TweakSelect, + TweakSlider, + TweaksPanel, TweakText, - TweakNumber, - TweakColor, - TweakButton, + TweakToggle, + useTweaks, }); diff --git a/docs/design-system/ink-carbon/project/recordroom/watcher.css b/docs/design-system/ink-carbon/project/recordroom/watcher.css index 54d20e172..01d167060 100644 --- a/docs/design-system/ink-carbon/project/recordroom/watcher.css +++ b/docs/design-system/ink-carbon/project/recordroom/watcher.css @@ -10,13 +10,13 @@ ═══════════════════════════════════════════════════════════════════ */ .wp { - min-height: 100vh; - background: var(--background); - color: var(--foreground); - font-family: var(--font-sans); display: grid; grid-template-columns: 340px minmax(0, 1fr); align-items: stretch; + min-height: 100vh; + font-family: var(--font-sans); + color: var(--foreground); + background: var(--background); } .wp * { box-sizing: border-box; @@ -24,56 +24,56 @@ /* ─── Left: the watcher ─── */ .wp-who { - background: color-mix(in oklab, var(--background) 88%, oklch(0 0 0)); - border-right: 1px solid var(--border); - padding: 40px 32px; display: flex; flex-direction: column; gap: 22px; + padding: 40px 32px; + background: color-mix(in oklab, var(--background) 88%, oklch(0 0 0)); + border-right: 1px solid var(--border); } .wp-back { + padding: 0; font-family: var(--font-mono); font-size: 11px; color: var(--muted-foreground); - background: none; - border: 0; - padding: 0; text-align: left; - cursor: pointer; letter-spacing: 0.02em; + cursor: pointer; + background: none; + border: 0; } .wp-back:hover { color: var(--foreground); } .wp-mono-tile { - width: 56px; - height: 56px; - background: var(--foreground); - color: var(--background); + position: relative; display: flex; align-items: center; justify-content: center; + width: 56px; + height: 56px; font-family: var(--font-mono); font-size: 19px; font-weight: 600; + color: var(--background); letter-spacing: 0.02em; - position: relative; + background: var(--foreground); } .wp-mono-tile::before { - content: ""; position: absolute; inset: 0; - transform: translate(7px, 7px); + z-index: -1; + content: ""; background: var(--carbon-fill); border: 1px solid var(--carbon-outline); - z-index: -1; + transform: translate(7px, 7px); } .wp-who__name { + margin: 0; font-size: 25px; font-weight: 800; - letter-spacing: -0.02em; line-height: 1.1; - margin: 0; + letter-spacing: -0.02em; } .wp-who__role { font-family: var(--font-mono); @@ -90,9 +90,9 @@ display: grid; grid-template-columns: 88px 1fr; gap: 14px; + align-items: baseline; padding: 8px 0; border-top: 1px solid var(--border); - align-items: baseline; } .wp-fact__k { font-size: 11.5px; @@ -108,30 +108,30 @@ flex: 1; } .wp-pulse { - border-top: 1px solid var(--border); - padding-top: 16px; display: flex; flex-direction: column; gap: 10px; + padding-top: 16px; + border-top: 1px solid var(--border); } .wp-pulse__label { font-family: var(--font-mono); font-size: 10px; - letter-spacing: 0.1em; - text-transform: uppercase; color: var(--muted-foreground); + text-transform: uppercase; + letter-spacing: 0.1em; } .wp-spark { display: flex; - align-items: flex-end; gap: 3px; + align-items: flex-end; height: 34px; } .wp-spark__bar { flex: 1; + min-height: 2px; background: var(--primary); opacity: 0.55; - min-height: 2px; } .wp-spark__bar.is-now { opacity: 1; @@ -142,16 +142,16 @@ color: var(--muted-foreground); } .wp-revoke { + padding: 9px 14px; + margin-top: 4px; font-family: var(--font-sans); font-size: 12.5px; font-weight: 700; color: var(--destructive); + cursor: pointer; background: transparent; border: 1.5px solid var(--destructive); border-radius: var(--radius-control); - padding: 9px 14px; - cursor: pointer; - margin-top: 4px; } .wp-revoke:hover { background: var(--destructive-tint); @@ -159,23 +159,23 @@ /* ─── Right: the portrait ─── */ .wp-body { - padding: 40px 56px 56px; max-width: 920px; + padding: 40px 56px 56px; } .wp-eyebrow { font-family: var(--font-mono); font-size: 11px; - letter-spacing: 0.12em; - text-transform: uppercase; color: var(--primary); + text-transform: uppercase; + letter-spacing: 0.12em; } .wp-thesis { + max-width: 18ch; + margin: 12px 0 0; font-size: 34px; font-weight: 800; - letter-spacing: -0.025em; line-height: 1.14; - margin: 12px 0 0; - max-width: 18ch; + letter-spacing: -0.025em; text-wrap: balance; } .wp-thesis em { @@ -188,16 +188,16 @@ } .wp-section__head { display: flex; - align-items: baseline; gap: 12px; + align-items: baseline; padding-bottom: 8px; border-bottom: 1px solid var(--border-strong); } .wp-section__title { + margin: 0; font-size: 13px; font-weight: 700; letter-spacing: 0.01em; - margin: 0; } .wp-section__count { font-family: var(--font-mono); @@ -215,9 +215,9 @@ display: grid; grid-template-columns: 1fr auto; gap: 4px 18px; + align-items: baseline; padding: 16px 0; border-bottom: 1px solid var(--border); - align-items: baseline; } .wp-infer__claim { font-size: 18px; @@ -229,17 +229,17 @@ font-weight: 700; } .wp-infer__src { + grid-column: 1; font-family: var(--font-mono); font-size: 10.5px; color: var(--muted-foreground); - grid-column: 1; } .wp-infer__from { font-family: var(--font-mono); font-size: 10px; color: var(--muted-foreground); - white-space: nowrap; text-align: right; + white-space: nowrap; } /* What stays yours — equal weight, calm */ @@ -249,11 +249,11 @@ gap: 0 32px; } .wp-kept { - padding: 15px 0; - border-bottom: 1px solid var(--border); display: flex; flex-direction: column; gap: 3px; + padding: 15px 0; + border-bottom: 1px solid var(--border); } .wp-kept__what { font-size: 16px; @@ -268,25 +268,25 @@ .wp-section--keep .wp-section__title { } .wp-keepline { - font-size: 13.5px; - color: var(--muted-foreground); - margin: 12px 0 0; max-width: 60ch; + margin: 12px 0 0; + font-size: 13.5px; line-height: 1.6; + color: var(--muted-foreground); } .wp-keepline b { - color: var(--foreground); font-weight: 600; + color: var(--foreground); } .wp-foot { - margin-top: 40px; - padding-top: 16px; - border-top: 1px solid var(--border-strong); display: flex; - justify-content: space-between; - align-items: center; gap: 16px; + align-items: center; + justify-content: space-between; + padding-top: 16px; + margin-top: 40px; + border-top: 1px solid var(--border-strong); } .wp-copyline { font-family: var(--font-mono); @@ -300,16 +300,16 @@ position: fixed; top: 18px; right: 22px; - font-family: var(--font-mono); - font-size: 14px; + z-index: 10; width: 32px; height: 32px; - background: var(--card); + font-family: var(--font-mono); + font-size: 14px; color: var(--foreground); + cursor: pointer; + background: var(--card); border: 1px solid var(--border-strong); border-radius: var(--radius-control); - cursor: pointer; - z-index: 10; } @media (max-width: 900px) { diff --git a/docs/design-system/ink-carbon/project/reinvention/boards-envelope.jsx b/docs/design-system/ink-carbon/project/reinvention/boards-envelope.jsx index 2e0640993..ab2a520c6 100644 --- a/docs/design-system/ink-carbon/project/reinvention/boards-envelope.jsx +++ b/docs/design-system/ink-carbon/project/reinvention/boards-envelope.jsx @@ -6,7 +6,7 @@ function EnvIdentity() { return (
-
+

PDPP

Consent-class mail @@ -46,7 +46,7 @@
withheld field - +
@@ -58,15 +58,15 @@
-
+
tint/hatch — withheld
-
+
tint/cross — sealed
-
+
tint/weave — archive
@@ -79,23 +79,23 @@
-
+
tint blue
-
+
paper
-
+
kraft
-
+
receipt green
-
+
return red
@@ -168,11 +168,11 @@ current + 5y
- + tax_docs — sealed
- + identity — sealed
diff --git a/docs/design-system/ink-carbon/project/reinvention/boards-round2.jsx b/docs/design-system/ink-carbon/project/reinvention/boards-round2.jsx index fa9b6e812..7f1247874 100644 --- a/docs/design-system/ink-carbon/project/reinvention/boards-round2.jsx +++ b/docs/design-system/ink-carbon/project/reinvention/boards-round2.jsx @@ -31,7 +31,7 @@ File copy — holder
- + ref — carbon paper,
the duplicate you keep @@ -79,7 +79,7 @@ ["oklch(0.46 0.15 295)", "carbon"], ["oklch(0.46 0.15 295 / 0.12)", "impression"], ]} - labelStyle={{ fontFamily: '"Fragment Mono", monospace', color: "oklch(0.5 0.01 270)" }} + labelStyle={{ color: "oklch(0.5 0.01 270)", fontFamily: '"Fragment Mono", monospace' }} />
); @@ -94,7 +94,7 @@

PDPP

black for the record · red for refusal
- + ref — two-color
typewriter ribbon @@ -194,7 +194,7 @@ ["oklch(0.24 0.02 60)", "ink"], ["oklch(0.43 0.07 165)", "banknote"], ]} - labelStyle={{ fontFamily: '"Spline Sans Mono", monospace', color: "oklch(0.55 0.02 60)" }} + labelStyle={{ color: "oklch(0.55 0.02 60)", fontFamily: '"Spline Sans Mono", monospace' }} />
); @@ -209,7 +209,7 @@

PDPP

col 01–80 · personal data
- + ref — tabulating cards,
a hole is a grant
@@ -222,13 +222,13 @@
{SCOPES.map((s) => (
- + {s.name} {s.terms}
))}
- + tax_docs.read @@ -258,7 +258,7 @@ ["oklch(0.27 0.02 75)", "ink"], ["oklch(0.6 0.1 40)", "column rule"], ]} - labelStyle={{ fontFamily: '"Martian Mono", monospace', fontSize: 8, color: "oklch(0.5 0.04 75)" }} + labelStyle={{ color: "oklch(0.5 0.04 75)", fontFamily: '"Martian Mono", monospace', fontSize: 8 }} />
); @@ -273,7 +273,7 @@

PDPP

protocol, in public
- + ref — civic signage,
public notices @@ -313,7 +313,7 @@ ["oklch(0.16 0 0)", "ink"], ["oklch(0.45 0.12 262)", "stamp blue"], ]} - labelStyle={{ fontFamily: '"Fragment Mono", monospace', color: "oklch(0.45 0 0)" }} + labelStyle={{ color: "oklch(0.45 0 0)", fontFamily: '"Fragment Mono", monospace' }} />
); @@ -323,14 +323,14 @@ function VGreenbar() { return (
-
-
+
+

PDPP

continuous form · holder's printout
- + ref — greenbar paper,
tractor-feed printouts @@ -385,5 +385,5 @@ ); } - Object.assign(window, { VCarbon, VRibbon, VClarendon, VPunch, VCivic, VGreenbar }); + Object.assign(window, { VCarbon, VCivic, VClarendon, VGreenbar, VPunch, VRibbon }); })(); diff --git a/docs/design-system/ink-carbon/project/reinvention/boards-round3.jsx b/docs/design-system/ink-carbon/project/reinvention/boards-round3.jsx index 043663489..bcbb62cf6 100644 --- a/docs/design-system/ink-carbon/project/reinvention/boards-round3.jsx +++ b/docs/design-system/ink-carbon/project/reinvention/boards-round3.jsx @@ -12,7 +12,7 @@ return (
- + PDPP Northstar HR · provider
@@ -26,7 +26,7 @@

- {(isT1 || isT2) &&
} + {(isT1 || isT2) &&
}
Longview Planning @@ -87,33 +87,33 @@ const GRANTS = [ { client: "Longview Planning", + dupe: true, + exp: "exp 2026-12-14", id: "grt_lngvw_01", scopes: "pay_statements.read · employment.read", - exp: "exp 2026-12-14", st: ["st-ok", "active"], - dupe: true, }, { client: "Concert Recommendations", + exp: "continuous", id: "grt_cncrt_02", scopes: "listening_history.read", - exp: "continuous", st: ["st-pro", "continuous"], }, { client: "pdpp CLI — owner export", + exp: "exp 2026-06-11", id: "dev_cli_07", scopes: "* (owner device flow)", - exp: "exp 2026-06-11", st: ["st-warn", "expiring 26h"], }, { client: "Crosswise Ads", + exp: "2026-05-02", id: "grt_xwise_09", + revoked: true, scopes: "browsing.read", - exp: "2026-05-02", st: ["st-off", "revoked"], - revoked: true, }, ]; @@ -136,12 +136,12 @@
- + PDPP
{renderCell(c, r)}