clinkx 0.2.3 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/LICENSE +10 -0
- package/README.md +113 -18
- package/clinkx-workflows/dist/allowed-roots.d.ts +1 -0
- package/clinkx-workflows/dist/allowed-roots.js +5 -0
- package/clinkx-workflows/dist/allowed-roots.js.map +1 -0
- package/clinkx-workflows/dist/artifacts.d.ts +20 -1
- package/clinkx-workflows/dist/artifacts.js +38 -9
- package/clinkx-workflows/dist/artifacts.js.map +1 -1
- package/clinkx-workflows/dist/backend.d.ts +4 -0
- package/clinkx-workflows/dist/citation-check.d.ts +40 -0
- package/clinkx-workflows/dist/citation-check.js +348 -0
- package/clinkx-workflows/dist/citation-check.js.map +1 -0
- package/clinkx-workflows/dist/clink-client.d.ts +2 -1
- package/clinkx-workflows/dist/clink-client.js +17 -4
- package/clinkx-workflows/dist/clink-client.js.map +1 -1
- package/clinkx-workflows/dist/config.d.ts +145 -16
- package/clinkx-workflows/dist/config.js +384 -26
- package/clinkx-workflows/dist/config.js.map +1 -1
- package/clinkx-workflows/dist/contained-file.d.ts +172 -0
- package/clinkx-workflows/dist/contained-file.js +483 -0
- package/clinkx-workflows/dist/contained-file.js.map +1 -0
- package/clinkx-workflows/dist/context-builder-lifecycle-contract.d.ts +416 -0
- package/clinkx-workflows/dist/context-builder-lifecycle-contract.js +328 -0
- package/clinkx-workflows/dist/context-builder-lifecycle-contract.js.map +1 -0
- package/clinkx-workflows/dist/context-builder-lifecycle-router.d.ts +24 -0
- package/clinkx-workflows/dist/context-builder-lifecycle-router.js +39 -0
- package/clinkx-workflows/dist/context-builder-lifecycle-router.js.map +1 -0
- package/clinkx-workflows/dist/context-cache-key.d.ts +53 -0
- package/clinkx-workflows/dist/context-cache-key.js +91 -0
- package/clinkx-workflows/dist/context-cache-key.js.map +1 -0
- package/clinkx-workflows/dist/context-mode-resolver.d.ts +27 -0
- package/clinkx-workflows/dist/context-mode-resolver.js +126 -0
- package/clinkx-workflows/dist/context-mode-resolver.js.map +1 -0
- package/clinkx-workflows/dist/context-preflight.d.ts +184 -0
- package/clinkx-workflows/dist/context-preflight.js +91 -0
- package/clinkx-workflows/dist/context-preflight.js.map +1 -0
- package/clinkx-workflows/dist/council-expander.d.ts +1 -1
- package/clinkx-workflows/dist/council-expander.js +2 -1
- package/clinkx-workflows/dist/council-expander.js.map +1 -1
- package/clinkx-workflows/dist/dashboard/activity-contract.d.ts +165 -0
- package/clinkx-workflows/dist/dashboard/activity-contract.js +105 -0
- package/clinkx-workflows/dist/dashboard/activity-contract.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/activity-lease.d.ts +25 -0
- package/clinkx-workflows/dist/dashboard/activity-lease.js +34 -0
- package/clinkx-workflows/dist/dashboard/activity-lease.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/activity-reader.d.ts +86 -0
- package/clinkx-workflows/dist/dashboard/activity-reader.js +480 -0
- package/clinkx-workflows/dist/dashboard/activity-reader.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/api-routes.d.ts +36 -0
- package/clinkx-workflows/dist/dashboard/api-routes.js +1047 -0
- package/clinkx-workflows/dist/dashboard/api-routes.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/artifact-index.d.ts +37 -0
- package/clinkx-workflows/dist/dashboard/artifact-index.js +145 -0
- package/clinkx-workflows/dist/dashboard/artifact-index.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/auth/challenge-store.d.ts +31 -0
- package/clinkx-workflows/dist/dashboard/auth/challenge-store.js +65 -0
- package/clinkx-workflows/dist/dashboard/auth/challenge-store.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/auth/controller.d.ts +26 -0
- package/clinkx-workflows/dist/dashboard/auth/controller.js +596 -0
- package/clinkx-workflows/dist/dashboard/auth/controller.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/auth/credential-store.d.ts +60 -0
- package/clinkx-workflows/dist/dashboard/auth/credential-store.js +776 -0
- package/clinkx-workflows/dist/dashboard/auth/credential-store.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/auth/enrollment-gate.d.ts +9 -0
- package/clinkx-workflows/dist/dashboard/auth/enrollment-gate.js +44 -0
- package/clinkx-workflows/dist/dashboard/auth/enrollment-gate.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/auth/session-store.d.ts +17 -0
- package/clinkx-workflows/dist/dashboard/auth/session-store.js +77 -0
- package/clinkx-workflows/dist/dashboard/auth/session-store.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/auth/webauthn-service.d.ts +25 -0
- package/clinkx-workflows/dist/dashboard/auth/webauthn-service.js +71 -0
- package/clinkx-workflows/dist/dashboard/auth/webauthn-service.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/context-run-liveness.d.ts +37 -0
- package/clinkx-workflows/dist/dashboard/context-run-liveness.js +49 -0
- package/clinkx-workflows/dist/dashboard/context-run-liveness.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/context-run-reader.d.ts +222 -0
- package/clinkx-workflows/dist/dashboard/context-run-reader.js +1178 -0
- package/clinkx-workflows/dist/dashboard/context-run-reader.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/contracts.d.ts +8936 -0
- package/clinkx-workflows/dist/dashboard/contracts.js +1157 -0
- package/clinkx-workflows/dist/dashboard/contracts.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/generation.d.ts +14 -0
- package/clinkx-workflows/dist/dashboard/generation.js +41 -0
- package/clinkx-workflows/dist/dashboard/generation.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/index.d.ts +28 -0
- package/clinkx-workflows/dist/dashboard/index.js +16 -0
- package/clinkx-workflows/dist/dashboard/index.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/lazy-content.d.ts +101 -0
- package/clinkx-workflows/dist/dashboard/lazy-content.js +748 -0
- package/clinkx-workflows/dist/dashboard/lazy-content.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/progress-report-contract.d.ts +303 -0
- package/clinkx-workflows/dist/dashboard/progress-report-contract.js +221 -0
- package/clinkx-workflows/dist/dashboard/progress-report-contract.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/progress-report-reader.d.ts +50 -0
- package/clinkx-workflows/dist/dashboard/progress-report-reader.js +581 -0
- package/clinkx-workflows/dist/dashboard/progress-report-reader.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/progress-report-status-overlay.d.ts +89 -0
- package/clinkx-workflows/dist/dashboard/progress-report-status-overlay.js +71 -0
- package/clinkx-workflows/dist/dashboard/progress-report-status-overlay.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/progress-report-status-store.d.ts +28 -0
- package/clinkx-workflows/dist/dashboard/progress-report-status-store.js +110 -0
- package/clinkx-workflows/dist/dashboard/progress-report-status-store.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/read-model.d.ts +86 -0
- package/clinkx-workflows/dist/dashboard/read-model.js +440 -0
- package/clinkx-workflows/dist/dashboard/read-model.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/request-admission.d.ts +31 -0
- package/clinkx-workflows/dist/dashboard/request-admission.js +126 -0
- package/clinkx-workflows/dist/dashboard/request-admission.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/scanner.d.ts +75 -0
- package/clinkx-workflows/dist/dashboard/scanner.js +1110 -0
- package/clinkx-workflows/dist/dashboard/scanner.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/server-factory.d.ts +50 -0
- package/clinkx-workflows/dist/dashboard/server-factory.js +431 -0
- package/clinkx-workflows/dist/dashboard/server-factory.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/session-log-contract.d.ts +2611 -0
- package/clinkx-workflows/dist/dashboard/session-log-contract.js +480 -0
- package/clinkx-workflows/dist/dashboard/session-log-contract.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/session-log-identity.d.ts +21 -0
- package/clinkx-workflows/dist/dashboard/session-log-identity.js +101 -0
- package/clinkx-workflows/dist/dashboard/session-log-identity.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/session-log-journal.d.ts +43 -0
- package/clinkx-workflows/dist/dashboard/session-log-journal.js +522 -0
- package/clinkx-workflows/dist/dashboard/session-log-journal.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/session-log-reader.d.ts +61 -0
- package/clinkx-workflows/dist/dashboard/session-log-reader.js +378 -0
- package/clinkx-workflows/dist/dashboard/session-log-reader.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/session-log-redaction.d.ts +6 -0
- package/clinkx-workflows/dist/dashboard/session-log-redaction.js +367 -0
- package/clinkx-workflows/dist/dashboard/session-log-redaction.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/session-log-retention.d.ts +23 -0
- package/clinkx-workflows/dist/dashboard/session-log-retention.js +212 -0
- package/clinkx-workflows/dist/dashboard/session-log-retention.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/static-responses.d.ts +10 -0
- package/clinkx-workflows/dist/dashboard/static-responses.js +94 -0
- package/clinkx-workflows/dist/dashboard/static-responses.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/worker-lock-observer.d.ts +28 -0
- package/clinkx-workflows/dist/dashboard/worker-lock-observer.js +241 -0
- package/clinkx-workflows/dist/dashboard/worker-lock-observer.js.map +1 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/index-DOkMXTfB.js +25 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff +0 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/style-ChJsn0KD.css +1 -0
- package/clinkx-workflows/dist/dashboard-ui/contracts/council-plan-review-input-v1.json +158 -0
- package/clinkx-workflows/dist/dashboard-ui/index.html +15 -0
- package/clinkx-workflows/dist/dashboard-ui/licenses/IBM-Plex-Mono-OFL.txt +93 -0
- package/clinkx-workflows/dist/dashboard-ui/licenses/IBM-Plex-Sans-OFL.txt +93 -0
- package/clinkx-workflows/dist/dashboard-ui/licenses/JetBrains-Mono-OFL.txt +93 -0
- package/clinkx-workflows/dist/dashboard-ui/licenses/NOTICE.md +32 -0
- package/clinkx-workflows/dist/dashboard-ui/theme-init.js +8 -0
- package/clinkx-workflows/dist/definition-normalizer.d.ts +19 -0
- package/clinkx-workflows/dist/definition-normalizer.js.map +1 -1
- package/clinkx-workflows/dist/delegate-validate.d.ts +78 -0
- package/clinkx-workflows/dist/delegate-validate.js +97 -0
- package/clinkx-workflows/dist/delegate-validate.js.map +1 -0
- package/clinkx-workflows/dist/engine.d.ts +141 -0
- package/clinkx-workflows/dist/engine.js +882 -28
- package/clinkx-workflows/dist/engine.js.map +1 -1
- package/clinkx-workflows/dist/errors.d.ts +2 -0
- package/clinkx-workflows/dist/errors.js.map +1 -1
- package/clinkx-workflows/dist/fingerprint.d.ts +1 -1
- package/clinkx-workflows/dist/fingerprint.js +1 -1
- package/clinkx-workflows/dist/index.d.ts +19 -0
- package/clinkx-workflows/dist/index.js +15 -0
- package/clinkx-workflows/dist/index.js.map +1 -1
- package/clinkx-workflows/dist/loader.d.ts +9 -3
- package/clinkx-workflows/dist/loader.js +92 -21
- package/clinkx-workflows/dist/loader.js.map +1 -1
- package/clinkx-workflows/dist/plan-review/admission.d.ts +160 -0
- package/clinkx-workflows/dist/plan-review/admission.js +290 -0
- package/clinkx-workflows/dist/plan-review/admission.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/context-policy.d.ts +55 -0
- package/clinkx-workflows/dist/plan-review/context-policy.js +146 -0
- package/clinkx-workflows/dist/plan-review/context-policy.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/contract.d.ts +763 -0
- package/clinkx-workflows/dist/plan-review/contract.js +289 -0
- package/clinkx-workflows/dist/plan-review/contract.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/digest.d.ts +68 -0
- package/clinkx-workflows/dist/plan-review/digest.js +241 -0
- package/clinkx-workflows/dist/plan-review/digest.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/discovery.d.ts +45 -0
- package/clinkx-workflows/dist/plan-review/discovery.js +36 -0
- package/clinkx-workflows/dist/plan-review/discovery.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/execution-contract.d.ts +298 -0
- package/clinkx-workflows/dist/plan-review/execution-contract.js +194 -0
- package/clinkx-workflows/dist/plan-review/execution-contract.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/expander.d.ts +48 -0
- package/clinkx-workflows/dist/plan-review/expander.js +243 -0
- package/clinkx-workflows/dist/plan-review/expander.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/projector.d.ts +126 -0
- package/clinkx-workflows/dist/plan-review/projector.js +437 -0
- package/clinkx-workflows/dist/plan-review/projector.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/publication.d.ts +95 -0
- package/clinkx-workflows/dist/plan-review/publication.js +182 -0
- package/clinkx-workflows/dist/plan-review/publication.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/ranking-reducer.d.ts +34 -0
- package/clinkx-workflows/dist/plan-review/ranking-reducer.js +139 -0
- package/clinkx-workflows/dist/plan-review/ranking-reducer.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/resume.d.ts +38 -0
- package/clinkx-workflows/dist/plan-review/resume.js +122 -0
- package/clinkx-workflows/dist/plan-review/resume.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/rubric.d.ts +7 -0
- package/clinkx-workflows/dist/plan-review/rubric.js +24 -0
- package/clinkx-workflows/dist/plan-review/rubric.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/run-state.d.ts +398 -0
- package/clinkx-workflows/dist/plan-review/run-state.js +183 -0
- package/clinkx-workflows/dist/plan-review/run-state.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/runtime-policy.d.ts +74 -0
- package/clinkx-workflows/dist/plan-review/runtime-policy.js +186 -0
- package/clinkx-workflows/dist/plan-review/runtime-policy.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/snapshot.d.ts +81 -0
- package/clinkx-workflows/dist/plan-review/snapshot.js +170 -0
- package/clinkx-workflows/dist/plan-review/snapshot.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/stage-error.d.ts +19 -0
- package/clinkx-workflows/dist/plan-review/stage-error.js +20 -0
- package/clinkx-workflows/dist/plan-review/stage-error.js.map +1 -0
- package/clinkx-workflows/dist/post-processors/index.d.ts +20 -0
- package/clinkx-workflows/dist/post-processors/index.js +21 -0
- package/clinkx-workflows/dist/post-processors/index.js.map +1 -0
- package/clinkx-workflows/dist/prompt-budget.d.ts +7 -0
- package/clinkx-workflows/dist/prompt-budget.js +7 -1
- package/clinkx-workflows/dist/prompt-budget.js.map +1 -1
- package/clinkx-workflows/dist/reducers/index.js +2 -0
- package/clinkx-workflows/dist/reducers/index.js.map +1 -1
- package/clinkx-workflows/dist/run-lock-heartbeat.d.ts +2 -0
- package/clinkx-workflows/dist/run-lock-heartbeat.js +3 -0
- package/clinkx-workflows/dist/run-lock-heartbeat.js.map +1 -0
- package/clinkx-workflows/dist/run-summary/cards/council-plan-review.d.ts +34 -0
- package/clinkx-workflows/dist/run-summary/cards/council-plan-review.js +74 -0
- package/clinkx-workflows/dist/run-summary/cards/council-plan-review.js.map +1 -0
- package/clinkx-workflows/dist/run-summary/cards/index.js +5 -0
- package/clinkx-workflows/dist/run-summary/cards/index.js.map +1 -1
- package/clinkx-workflows/dist/run-summary.d.ts +13 -0
- package/clinkx-workflows/dist/run-summary.js +29 -1
- package/clinkx-workflows/dist/run-summary.js.map +1 -1
- package/clinkx-workflows/dist/schema.d.ts +76 -21
- package/clinkx-workflows/dist/schema.js +10 -1
- package/clinkx-workflows/dist/schema.js.map +1 -1
- package/clinkx-workflows/dist/seed-terms.d.ts +30 -0
- package/clinkx-workflows/dist/seed-terms.js +176 -0
- package/clinkx-workflows/dist/seed-terms.js.map +1 -0
- package/clinkx-workflows/dist/server.d.ts +5 -2
- package/clinkx-workflows/dist/server.js +82 -52
- package/clinkx-workflows/dist/server.js.map +1 -1
- package/clinkx-workflows/dist/shutdown.d.ts +18 -20
- package/clinkx-workflows/dist/shutdown.js +43 -61
- package/clinkx-workflows/dist/shutdown.js.map +1 -1
- package/clinkx-workflows/dist/state-schema.d.ts +38 -0
- package/clinkx-workflows/dist/state.d.ts +170 -3
- package/clinkx-workflows/dist/state.js +1275 -28
- package/clinkx-workflows/dist/state.js.map +1 -1
- package/clinkx-workflows/dist/transport.d.ts +15 -0
- package/clinkx-workflows/dist/transport.js +19 -10
- package/clinkx-workflows/dist/transport.js.map +1 -1
- package/clinkx-workflows/dist/types.d.ts +14 -2
- package/clinkx-workflows/dist/validators/council.d.ts +318 -85
- package/clinkx-workflows/dist/validators/council.js +52 -13
- package/clinkx-workflows/dist/validators/council.js.map +1 -1
- package/clinkx-workflows/dist/validators/index.d.ts +1 -0
- package/clinkx-workflows/dist/validators/index.js +3 -0
- package/clinkx-workflows/dist/validators/index.js.map +1 -1
- package/clinkx-workflows/dist/validators/plan-review.d.ts +398 -0
- package/clinkx-workflows/dist/validators/plan-review.js +316 -0
- package/clinkx-workflows/dist/validators/plan-review.js.map +1 -0
- package/clinkx-workflows/dist/workflow-receipt.js +188 -4
- package/clinkx-workflows/dist/workflow-receipt.js.map +1 -1
- package/clinkx-workflows/dist/workflow-tools.d.ts +48 -3
- package/clinkx-workflows/dist/workflow-tools.js +1402 -59
- package/clinkx-workflows/dist/workflow-tools.js.map +1 -1
- package/clinkx-workflows/dist/workspace-signature.d.ts +16 -0
- package/clinkx-workflows/dist/workspace-signature.js +388 -0
- package/clinkx-workflows/dist/workspace-signature.js.map +1 -0
- package/clinkx-workflows/templates/council/answer-specialist.txt +2 -1
- package/clinkx-workflows/templates/council/code-review-chairman.txt +13 -0
- package/clinkx-workflows/templates/council/code-review-cross-reviewer.txt +4 -1
- package/clinkx-workflows/templates/council/code-review-reviewer.txt +19 -3
- package/clinkx-workflows/templates/council/debug-debugger.txt +2 -1
- package/clinkx-workflows/templates/council/discover-analyst.txt +2 -1
- package/clinkx-workflows/templates/council/member.txt +2 -1
- package/clinkx-workflows/templates/council/plan-review-chairman.txt +68 -0
- package/clinkx-workflows/templates/council/plan-review-cross-reviewer.txt +51 -0
- package/clinkx-workflows/templates/council/plan-review-reviewer.txt +60 -0
- package/clinkx-workflows/templates/plan-review/chairman.txt +48 -0
- package/clinkx-workflows/templates/plan-review/cross-review.txt +52 -0
- package/clinkx-workflows/templates/plan-review/ranking.txt +16 -0
- package/clinkx-workflows/templates/plan-review/reviewer.txt +57 -0
- package/clinkx-workflows/workflows/council-answer.yaml +97 -8
- package/clinkx-workflows/workflows/council-code-review.yaml +103 -8
- package/clinkx-workflows/workflows/council-debug.yaml +97 -8
- package/clinkx-workflows/workflows/council-default.yaml +97 -8
- package/clinkx-workflows/workflows/council-discover.yaml +97 -8
- package/clinkx-workflows/workflows/council-plan-review.yaml +250 -0
- package/clinkx-workflows/workflows/plan-review-receipts.yaml +230 -0
- package/conf/adapters/agy.json +49 -0
- package/conf/adapters/claude.json +5 -0
- package/conf/adapters/codex-app-server.json +44 -0
- package/conf/adapters/codex.json +7 -2
- package/conf/adapters/glm.json +5 -0
- package/conf/adapters/grok.json +60 -0
- package/conf/adapters/hapi/agy.json +52 -0
- package/conf/adapters/hapi/claude.json +5 -0
- package/conf/adapters/hapi/codex.json +7 -2
- package/conf/adapters/hapi/glm.json +5 -0
- package/conf/prompts/json-codereviewer.txt +1 -0
- package/conf/prompts/json.txt +1 -0
- package/dist/activity-journal.d.ts +42 -0
- package/dist/activity-journal.js +381 -0
- package/dist/activity-journal.js.map +1 -0
- package/dist/allowed-roots.d.ts +53 -0
- package/dist/allowed-roots.js +216 -0
- package/dist/allowed-roots.js.map +1 -0
- package/dist/codex/activity-session-log.d.ts +14 -0
- package/dist/codex/activity-session-log.js +206 -0
- package/dist/codex/activity-session-log.js.map +1 -0
- package/dist/codex/agent-tools.d.ts +23 -0
- package/dist/codex/agent-tools.js +1270 -0
- package/dist/codex/agent-tools.js.map +1 -0
- package/dist/codex/app-server-client.d.ts +76 -0
- package/dist/codex/app-server-client.js +582 -0
- package/dist/codex/app-server-client.js.map +1 -0
- package/dist/codex/app-server-registry.d.ts +389 -0
- package/dist/codex/app-server-registry.js +1407 -0
- package/dist/codex/app-server-registry.js.map +1 -0
- package/dist/codex/client-types.d.ts +109 -0
- package/dist/codex/client-types.js +2 -0
- package/dist/codex/client-types.js.map +1 -0
- package/dist/codex/protocol.d.ts +5375 -0
- package/dist/codex/protocol.js +614 -0
- package/dist/codex/protocol.js.map +1 -0
- package/dist/codex/session.d.ts +263 -0
- package/dist/codex/session.js +466 -0
- package/dist/codex/session.js.map +1 -0
- package/dist/config.d.ts +185 -19
- package/dist/config.js +82 -16
- package/dist/config.js.map +1 -1
- package/dist/context-builder/anchors.d.ts +12 -0
- package/dist/context-builder/anchors.js +22 -0
- package/dist/context-builder/anchors.js.map +1 -0
- package/dist/context-builder/bundle.d.ts +109 -0
- package/dist/context-builder/bundle.js +1498 -0
- package/dist/context-builder/bundle.js.map +1 -0
- package/dist/context-builder/codemap.d.ts +22 -0
- package/dist/context-builder/codemap.js +675 -0
- package/dist/context-builder/codemap.js.map +1 -0
- package/dist/context-builder/config.d.ts +61 -0
- package/dist/context-builder/config.js +216 -0
- package/dist/context-builder/config.js.map +1 -0
- package/dist/context-builder/context-run-catalog.d.ts +17 -0
- package/dist/context-builder/context-run-catalog.js +99 -0
- package/dist/context-builder/context-run-catalog.js.map +1 -0
- package/dist/context-builder/delegate-packet.d.ts +219 -0
- package/dist/context-builder/delegate-packet.js +1190 -0
- package/dist/context-builder/delegate-packet.js.map +1 -0
- package/dist/context-builder/delegate-reconstruction.d.ts +13 -0
- package/dist/context-builder/delegate-reconstruction.js +220 -0
- package/dist/context-builder/delegate-reconstruction.js.map +1 -0
- package/dist/context-builder/delegate-validation.d.ts +27 -0
- package/dist/context-builder/delegate-validation.js +479 -0
- package/dist/context-builder/delegate-validation.js.map +1 -0
- package/dist/context-builder/delegate.d.ts +26 -0
- package/dist/context-builder/delegate.js +134 -0
- package/dist/context-builder/delegate.js.map +1 -0
- package/dist/context-builder/diagnostics.d.ts +36 -0
- package/dist/context-builder/diagnostics.js +107 -0
- package/dist/context-builder/diagnostics.js.map +1 -0
- package/dist/context-builder/discovery.d.ts +146 -0
- package/dist/context-builder/discovery.js +2129 -0
- package/dist/context-builder/discovery.js.map +1 -0
- package/dist/context-builder/globs.d.ts +14 -0
- package/dist/context-builder/globs.js +175 -0
- package/dist/context-builder/globs.js.map +1 -0
- package/dist/context-builder/handler.d.ts +41 -0
- package/dist/context-builder/handler.js +2590 -0
- package/dist/context-builder/handler.js.map +1 -0
- package/dist/context-builder/lifecycle-contract.d.ts +424 -0
- package/dist/context-builder/lifecycle-contract.js +523 -0
- package/dist/context-builder/lifecycle-contract.js.map +1 -0
- package/dist/context-builder/lifecycle-journal.d.ts +62 -0
- package/dist/context-builder/lifecycle-journal.js +88 -0
- package/dist/context-builder/lifecycle-journal.js.map +1 -0
- package/dist/context-builder/lifecycle-observer.d.ts +102 -0
- package/dist/context-builder/lifecycle-observer.js +258 -0
- package/dist/context-builder/lifecycle-observer.js.map +1 -0
- package/dist/context-builder/lifecycle-reconcile.d.ts +32 -0
- package/dist/context-builder/lifecycle-reconcile.js +191 -0
- package/dist/context-builder/lifecycle-reconcile.js.map +1 -0
- package/dist/context-builder/lifecycle-recovery.d.ts +2 -0
- package/dist/context-builder/lifecycle-recovery.js +14 -0
- package/dist/context-builder/lifecycle-recovery.js.map +1 -0
- package/dist/context-builder/path-signals.d.ts +7 -0
- package/dist/context-builder/path-signals.js +70 -0
- package/dist/context-builder/path-signals.js.map +1 -0
- package/dist/context-builder/paths.d.ts +30 -0
- package/dist/context-builder/paths.js +153 -0
- package/dist/context-builder/paths.js.map +1 -0
- package/dist/context-builder/progress.d.ts +17 -0
- package/dist/context-builder/progress.js +93 -0
- package/dist/context-builder/progress.js.map +1 -0
- package/dist/context-builder/receipt.d.ts +91 -0
- package/dist/context-builder/receipt.js +180 -0
- package/dist/context-builder/receipt.js.map +1 -0
- package/dist/context-builder/refine-handler.d.ts +14 -0
- package/dist/context-builder/refine-handler.js +790 -0
- package/dist/context-builder/refine-handler.js.map +1 -0
- package/dist/context-builder/review-coverage.d.ts +84 -0
- package/dist/context-builder/review-coverage.js +437 -0
- package/dist/context-builder/review-coverage.js.map +1 -0
- package/dist/context-builder/run-id.d.ts +7 -0
- package/dist/context-builder/run-id.js +31 -0
- package/dist/context-builder/run-id.js.map +1 -0
- package/dist/context-builder/schema.d.ts +669 -0
- package/dist/context-builder/schema.js +399 -0
- package/dist/context-builder/schema.js.map +1 -0
- package/dist/context-builder/secrets.d.ts +41 -0
- package/dist/context-builder/secrets.js +1651 -0
- package/dist/context-builder/secrets.js.map +1 -0
- package/dist/context-builder/selection.d.ts +56 -0
- package/dist/context-builder/selection.js +1111 -0
- package/dist/context-builder/selection.js.map +1 -0
- package/dist/context-builder/session-log-identity.d.ts +1 -0
- package/dist/context-builder/session-log-identity.js +66 -0
- package/dist/context-builder/session-log-identity.js.map +1 -0
- package/dist/context-builder/slice-range-math.d.ts +16 -0
- package/dist/context-builder/slice-range-math.js +137 -0
- package/dist/context-builder/slice-range-math.js.map +1 -0
- package/dist/context-builder/status.d.ts +66 -0
- package/dist/context-builder/status.js +160 -0
- package/dist/context-builder/status.js.map +1 -0
- package/dist/context-builder/storage.d.ts +118 -0
- package/dist/context-builder/storage.js +1003 -0
- package/dist/context-builder/storage.js.map +1 -0
- package/dist/context-builder/token-estimate.d.ts +19 -0
- package/dist/context-builder/token-estimate.js +90 -0
- package/dist/context-builder/token-estimate.js.map +1 -0
- package/dist/context-builder/versions.d.ts +29 -0
- package/dist/context-builder/versions.js +30 -0
- package/dist/context-builder/versions.js.map +1 -0
- package/dist/context-builder/workflow-runner.d.ts +69 -0
- package/dist/context-builder/workflow-runner.js +1051 -0
- package/dist/context-builder/workflow-runner.js.map +1 -0
- package/dist/context-builder/write-watchdog.d.ts +14 -0
- package/dist/context-builder/write-watchdog.js +38 -0
- package/dist/context-builder/write-watchdog.js.map +1 -0
- package/dist/dashboard-adapter-catalog.d.ts +9 -0
- package/dist/dashboard-adapter-catalog.js +46 -0
- package/dist/dashboard-adapter-catalog.js.map +1 -0
- package/dist/dashboard-cli.d.ts +79 -0
- package/dist/dashboard-cli.js +585 -0
- package/dist/dashboard-cli.js.map +1 -0
- package/dist/dashboard-companion/client.d.ts +36 -0
- package/dist/dashboard-companion/client.js +210 -0
- package/dist/dashboard-companion/client.js.map +1 -0
- package/dist/dashboard-companion/config.d.ts +8 -0
- package/dist/dashboard-companion/config.js +52 -0
- package/dist/dashboard-companion/config.js.map +1 -0
- package/dist/dashboard-companion/http-proxy.d.ts +10 -0
- package/dist/dashboard-companion/http-proxy.js +234 -0
- package/dist/dashboard-companion/http-proxy.js.map +1 -0
- package/dist/dashboard-companion/protocol.d.ts +80 -0
- package/dist/dashboard-companion/protocol.js +455 -0
- package/dist/dashboard-companion/protocol.js.map +1 -0
- package/dist/dashboard-companion/standalone-assets.d.ts +12 -0
- package/dist/dashboard-companion/standalone-assets.js +72 -0
- package/dist/dashboard-companion/standalone-assets.js.map +1 -0
- package/dist/dashboard-companion/standalone-entry.d.ts +1 -0
- package/dist/dashboard-companion/standalone-entry.js +17 -0
- package/dist/dashboard-companion/standalone-entry.js.map +1 -0
- package/dist/dashboard-gateway/config.d.ts +9 -0
- package/dist/dashboard-gateway/config.js +44 -0
- package/dist/dashboard-gateway/config.js.map +1 -0
- package/dist/dashboard-gateway/index.d.ts +11 -0
- package/dist/dashboard-gateway/index.js +755 -0
- package/dist/dashboard-gateway/index.js.map +1 -0
- package/dist/dashboard-gateway/main.d.ts +1 -0
- package/dist/dashboard-gateway/main.js +25 -0
- package/dist/dashboard-gateway/main.js.map +1 -0
- package/dist/dashboard-pairing-token.d.ts +1 -0
- package/dist/dashboard-pairing-token.js +5 -0
- package/dist/dashboard-pairing-token.js.map +1 -0
- package/dist/elaris/client.d.ts +73 -0
- package/dist/elaris/client.js +825 -0
- package/dist/elaris/client.js.map +1 -0
- package/dist/elaris/config.d.ts +18 -0
- package/dist/elaris/config.js +75 -0
- package/dist/elaris/config.js.map +1 -0
- package/dist/elaris/errors.d.ts +41 -0
- package/dist/elaris/errors.js +205 -0
- package/dist/elaris/errors.js.map +1 -0
- package/dist/elaris/github-repo.d.ts +18 -0
- package/dist/elaris/github-repo.js +89 -0
- package/dist/elaris/github-repo.js.map +1 -0
- package/dist/elaris/index.d.ts +10 -0
- package/dist/elaris/index.js +11 -0
- package/dist/elaris/index.js.map +1 -0
- package/dist/elaris/redact.d.ts +89 -0
- package/dist/elaris/redact.js +423 -0
- package/dist/elaris/redact.js.map +1 -0
- package/dist/elaris/run-id.d.ts +5 -0
- package/dist/elaris/run-id.js +20 -0
- package/dist/elaris/run-id.js.map +1 -0
- package/dist/elaris/status.d.ts +45 -0
- package/dist/elaris/status.js +122 -0
- package/dist/elaris/status.js.map +1 -0
- package/dist/elaris/tool-schemas.d.ts +314 -0
- package/dist/elaris/tool-schemas.js +208 -0
- package/dist/elaris/tool-schemas.js.map +1 -0
- package/dist/elaris/tools.d.ts +73 -0
- package/dist/elaris/tools.js +1065 -0
- package/dist/elaris/tools.js.map +1 -0
- package/dist/elaris/types.d.ts +1541 -0
- package/dist/elaris/types.js +252 -0
- package/dist/elaris/types.js.map +1 -0
- package/dist/errors.d.ts +16 -0
- package/dist/errors.js +19 -0
- package/dist/errors.js.map +1 -1
- package/dist/handler.d.ts +4 -0
- package/dist/handler.js +10 -1
- package/dist/handler.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +13 -3
- package/dist/index.js.map +1 -1
- package/dist/local-clink-backend.d.ts +3 -0
- package/dist/local-clink-backend.js +4 -0
- package/dist/local-clink-backend.js.map +1 -1
- package/dist/package-info.d.ts +2 -0
- package/dist/package-info.js +18 -0
- package/dist/package-info.js.map +1 -0
- package/dist/parsers/codex-jsonl.js +48 -3
- package/dist/parsers/codex-jsonl.js.map +1 -1
- package/dist/parsers/extract.d.ts +10 -1
- package/dist/parsers/extract.js +11 -3
- package/dist/parsers/extract.js.map +1 -1
- package/dist/paths.d.ts +8 -0
- package/dist/paths.js +31 -43
- package/dist/paths.js.map +1 -1
- package/dist/pipeline.d.ts +51 -3
- package/dist/pipeline.js +264 -47
- package/dist/pipeline.js.map +1 -1
- package/dist/run-dir.d.ts +24 -0
- package/dist/run-dir.js +201 -2
- package/dist/run-dir.js.map +1 -1
- package/dist/runner.d.ts +13 -0
- package/dist/runner.js +184 -15
- package/dist/runner.js.map +1 -1
- package/dist/safety.d.ts +4 -4
- package/dist/safety.js +4 -4
- package/dist/schema.d.ts +7 -7
- package/dist/server.d.ts +2 -0
- package/dist/server.js +396 -24
- package/dist/server.js.map +1 -1
- package/dist/session-log-stream.d.ts +18 -0
- package/dist/session-log-stream.js +319 -0
- package/dist/session-log-stream.js.map +1 -0
- package/package.json +47 -5
- package/conf/adapters/gemini.json +0 -44
- package/conf/adapters/hapi/gemini.json +0 -47
- package/dist/artifacts.d.ts +0 -9
- package/dist/artifacts.js +0 -24
- package/dist/artifacts.js.map +0 -1
- package/dist/parsers/gemini-json.d.ts +0 -2
- package/dist/parsers/gemini-json.js +0 -72
- package/dist/parsers/gemini-json.js.map +0 -1
|
@@ -0,0 +1,1178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded dashboard reader for the standalone / refine Context Run journal.
|
|
3
|
+
*
|
|
4
|
+
* This module enumerates and directly resolves stable row identities without
|
|
5
|
+
* exposing raw lifecycle state or manifest content.
|
|
6
|
+
*
|
|
7
|
+
* Two sources feed one projected row type:
|
|
8
|
+
* 1. `.context-runs/<contextRunId>/state.json` — authoritative live/terminal
|
|
9
|
+
* H0 lifecycle snapshots written by the producer journal. Validated back
|
|
10
|
+
* through the frozen schema; malformed rows are dropped.
|
|
11
|
+
* 2. Historical artifact run directories carrying `manifest.json` — a
|
|
12
|
+
* terminal lifecycle row is SYNTHESIZED from the manifest for the exact
|
|
13
|
+
* legacy 7-key and current 8-key artifact shapes; a 9-key (or larger)
|
|
14
|
+
* manifest is surfaced as `compatibility: "legacy_unsupported"` with no
|
|
15
|
+
* fabricated detail.
|
|
16
|
+
*
|
|
17
|
+
* Safety invariants:
|
|
18
|
+
* - Every read goes through {@link readContainedRelativeFile} (no-follow,
|
|
19
|
+
* containment-checked, size-capped). Symlinked entries and traversal names
|
|
20
|
+
* are skipped or rejected.
|
|
21
|
+
* - Workflow-owned records (`wfpre*` / `wfctx*`) are excluded from both
|
|
22
|
+
* sources.
|
|
23
|
+
* - Only safe scalar/enum/numeric fields are projected. Tasks, raw context,
|
|
24
|
+
* full manifests, provider output, raw error strings, and arbitrary JSON are
|
|
25
|
+
* NEVER read into a row. The single, deliberate path carve-out is the
|
|
26
|
+
* top-level project-folder root: `roots[].path` is projected verbatim into
|
|
27
|
+
* `ContextRunRow.projectFolders` (historical_manifest rows only) so the
|
|
28
|
+
* detail page can show the folder the run was created for. Selected-context
|
|
29
|
+
* file paths, task text, and all other manifest content stay private.
|
|
30
|
+
*
|
|
31
|
+
* Carve-out — bounded artifact-body reads (`readArtifact`): the invariant above
|
|
32
|
+
* applies to projected ROWS, which stay scalar-only. `readArtifact` is a
|
|
33
|
+
* separate, opt-in side channel that serves a single artifact file body
|
|
34
|
+
* (e.g. `bundle.md`) for an already-resolved run identity. It is allowlisted to
|
|
35
|
+
* the fixed artifact-key set, size-capped at the manifest ceiling, generation-
|
|
36
|
+
* pinned, and goes through the same no-follow containment read as rows. It does
|
|
37
|
+
* not enrich rows and is reached only via the dedicated HTTP route, never inline
|
|
38
|
+
* in list/detail projection.
|
|
39
|
+
*/
|
|
40
|
+
import { constants as fsConstants, } from "node:fs";
|
|
41
|
+
import { lstat as fsLstat, open as fsOpen, readdir as fsReaddir, realpath as fsRealpath, } from "node:fs/promises";
|
|
42
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
43
|
+
import { CONTEXT_BUILDER_LIFECYCLE_DIRECTORY, CONTEXT_BUILDER_LIFECYCLE_STATE_FILENAME, isContextBuilderOperationId, parseContextBuilderLifecycleStateV1, } from "../context-builder-lifecycle-contract.js";
|
|
44
|
+
import { containedFileFingerprintsEqual, ContainedFileChangedError, ContainedFilePolicyError, ContainedFileTooLargeError, readContainedRelativeFile, resolveOpenedDescriptorPath, } from "../contained-file.js";
|
|
45
|
+
import { MAX_DASHBOARD_ROOTS } from "./contracts.js";
|
|
46
|
+
import { deriveContextRunGeneration } from "./generation.js";
|
|
47
|
+
import { classifyContextRunLiveness, isContextRunLivePin, } from "./context-run-liveness.js";
|
|
48
|
+
const MANIFEST_FILENAME = "manifest.json";
|
|
49
|
+
const CONTEXT_RUN_CATALOG_DIRECTORY = ".catalog";
|
|
50
|
+
const CONTEXT_RUN_CATALOG_LEGACY_CUTOFF_FILENAME = ".legacy-before";
|
|
51
|
+
const CONTEXT_RUN_CATALOG_MARKER_PATTERN = /^(\d{13})-([oa])-(.+)$/;
|
|
52
|
+
const CONTEXT_BUILDER_ARTIFACT_ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
|
|
53
|
+
const MAX_CONTEXT_RUN_PROJECTION_CACHE_ENTRIES = 1_000;
|
|
54
|
+
/** Exact frozen legacy artifact key set (7 keys). */
|
|
55
|
+
const LEGACY_ARTIFACT_KEYS = [
|
|
56
|
+
"manifest_json",
|
|
57
|
+
"bundle_md",
|
|
58
|
+
"tree_json",
|
|
59
|
+
"search_jsonl",
|
|
60
|
+
"slices_jsonl",
|
|
61
|
+
"codemap_jsonl",
|
|
62
|
+
"summary_json",
|
|
63
|
+
];
|
|
64
|
+
/** Exact current artifact key set (8 keys: legacy + selected_context). */
|
|
65
|
+
const CURRENT_ARTIFACT_KEYS = [
|
|
66
|
+
"manifest_json",
|
|
67
|
+
"bundle_md",
|
|
68
|
+
"selected_context_jsonl",
|
|
69
|
+
"tree_json",
|
|
70
|
+
"search_jsonl",
|
|
71
|
+
"slices_jsonl",
|
|
72
|
+
"codemap_jsonl",
|
|
73
|
+
"summary_json",
|
|
74
|
+
];
|
|
75
|
+
/**
|
|
76
|
+
* Artifact key → on-disk filename. Duplicated from the producer
|
|
77
|
+
* (`src/context-builder/storage.ts`) by convention — the dashboard package does
|
|
78
|
+
* not import the producer. A parity test pins this copy to the producer export.
|
|
79
|
+
* `manifest_json` lives at `<runDir>/manifest.json`; every other key lives at
|
|
80
|
+
* `<runDir>/artifacts/<filename>` (see producer `resolveContextBuilderRunPaths`).
|
|
81
|
+
*/
|
|
82
|
+
export const CONTEXT_RUN_ARTIFACT_FILE_NAMES = Object.freeze({
|
|
83
|
+
manifest_json: "manifest.json",
|
|
84
|
+
bundle_md: "bundle.md",
|
|
85
|
+
selected_context_jsonl: "selected-context.jsonl",
|
|
86
|
+
tree_json: "tree.json",
|
|
87
|
+
search_jsonl: "search.jsonl",
|
|
88
|
+
slices_jsonl: "slices.jsonl",
|
|
89
|
+
codemap_jsonl: "codemap.jsonl",
|
|
90
|
+
summary_json: "summary.json",
|
|
91
|
+
});
|
|
92
|
+
export const CONTEXT_RUN_ARTIFACT_KEYS = Object.freeze(Object.keys(CONTEXT_RUN_ARTIFACT_FILE_NAMES));
|
|
93
|
+
export const DEFAULT_CONTEXT_RUN_READER_MAX_ROWS = 500;
|
|
94
|
+
export const DEFAULT_CONTEXT_RUN_READER_MAX_DIR_ENTRIES = 5_000;
|
|
95
|
+
export const DEFAULT_CONTEXT_RUN_READER_MAX_STATE_BYTES = 64 * 1_024;
|
|
96
|
+
export const DEFAULT_CONTEXT_RUN_READER_MAX_MANIFEST_BYTES = 4 * 1_024 * 1_024;
|
|
97
|
+
/**
|
|
98
|
+
* Per-artifact-body cap for {@link readArtifact}. Sized above the producer's
|
|
99
|
+
* `max_total_bytes` ceiling (5_000_000, src/context-builder/schema.ts) so a
|
|
100
|
+
* producer-legal `bundle.md` / `selected-context.jsonl` is served rather than
|
|
101
|
+
* 413'd, while still bounding the body endpoint. Distinct from the manifest
|
|
102
|
+
* cap so each can move independently.
|
|
103
|
+
*/
|
|
104
|
+
export const DEFAULT_CONTEXT_RUN_READER_MAX_ARTIFACT_BYTES = 6 * 1_024 * 1_024;
|
|
105
|
+
function attachContextRunLiveness(row, nowMs) {
|
|
106
|
+
return Object.freeze({
|
|
107
|
+
...row,
|
|
108
|
+
...classifyContextRunLiveness({
|
|
109
|
+
...(row.lifecycle == null
|
|
110
|
+
? {}
|
|
111
|
+
: {
|
|
112
|
+
status: row.lifecycle.status,
|
|
113
|
+
phase: row.lifecycle.phase,
|
|
114
|
+
updatedAt: row.lifecycle.updatedAt,
|
|
115
|
+
}),
|
|
116
|
+
nowMs,
|
|
117
|
+
}),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
function overlayContextRunRows(rows, nowMs) {
|
|
121
|
+
return rows.map((row) => attachContextRunLiveness(row, nowMs));
|
|
122
|
+
}
|
|
123
|
+
class ContextRunGenerationRaceError extends Error {
|
|
124
|
+
}
|
|
125
|
+
export class ContextRunSourceUnavailableError extends Error {
|
|
126
|
+
constructor() {
|
|
127
|
+
super("context runs source unavailable");
|
|
128
|
+
this.name = "ContextRunSourceUnavailableError";
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/** Workflow-owned records are not Context Runs and must never be surfaced. */
|
|
132
|
+
function isWorkflowOwnedId(id) {
|
|
133
|
+
return id.startsWith("wfctx") || id.startsWith("wfpre");
|
|
134
|
+
}
|
|
135
|
+
const defaultContextRunAuthorityFileSystem = {
|
|
136
|
+
open: fsOpen,
|
|
137
|
+
realpath: fsRealpath,
|
|
138
|
+
};
|
|
139
|
+
function resolveAuthority(options) {
|
|
140
|
+
return {
|
|
141
|
+
fileSystem: options.fileSystem ?? defaultContextRunAuthorityFileSystem,
|
|
142
|
+
descriptorPathVerifier: options.descriptorPathVerifier ??
|
|
143
|
+
resolveOpenedDescriptorPath,
|
|
144
|
+
readContainedFile: options.readContainedFile ?? readContainedRelativeFile,
|
|
145
|
+
beforeCommitVerification: options.beforeCommitVerification,
|
|
146
|
+
historicalProjectionCache: new Map(),
|
|
147
|
+
now: options.now ?? (() => new Date()),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
export function createContextRunReader(options = {}) {
|
|
151
|
+
const authority = resolveAuthority(options);
|
|
152
|
+
return {
|
|
153
|
+
list: async (readerOptions) => await readContextRunsWithAuthority(readerOptions, authority),
|
|
154
|
+
readDetail: async (readerOptions, id, contextGeneration) => await readContextRunWithAuthority(readerOptions, id, contextGeneration, authority),
|
|
155
|
+
readArtifact: async (readerOptions, id, artifactKey, contextGeneration) => await readArtifactWithAuthority(readerOptions, id, artifactKey, contextGeneration, authority),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
const defaultContextRunReader = createContextRunReader();
|
|
159
|
+
export async function readContextRuns(options) {
|
|
160
|
+
return await defaultContextRunReader.list(options);
|
|
161
|
+
}
|
|
162
|
+
export async function readContextRunArtifact(options, id, artifactKey, contextGeneration) {
|
|
163
|
+
return await defaultContextRunReader.readArtifact(options, id, artifactKey, contextGeneration);
|
|
164
|
+
}
|
|
165
|
+
async function readContextRunsWithAuthority(options, authority) {
|
|
166
|
+
const resolved = resolveOptions(options);
|
|
167
|
+
if (await admitRunsRoot(resolved.runsDir) === "missing") {
|
|
168
|
+
return { rows: [], truncated: false };
|
|
169
|
+
}
|
|
170
|
+
const catalogBudget = {
|
|
171
|
+
entries: 0,
|
|
172
|
+
maxEntries: resolved.maxDirEntries,
|
|
173
|
+
truncated: false,
|
|
174
|
+
};
|
|
175
|
+
const legacyBudget = {
|
|
176
|
+
entries: 0,
|
|
177
|
+
maxEntries: resolved.maxDirEntries,
|
|
178
|
+
truncated: false,
|
|
179
|
+
};
|
|
180
|
+
// Resolved once and shared by both row readers: it is a per-list constant, and
|
|
181
|
+
// re-deriving it per row would reintroduce exactly the syscall the projection
|
|
182
|
+
// cache exists to avoid.
|
|
183
|
+
const rootIdentity = await resolveListRootIdentity(resolved, authority);
|
|
184
|
+
const catalog = await readCatalogRows(resolved, catalogBudget, authority, rootIdentity);
|
|
185
|
+
if (catalog.rows.length > resolved.maxRows) {
|
|
186
|
+
// Even on the catalog fast path, suppress historical rows linked to a
|
|
187
|
+
// journal row by artifactRunId before slicing — otherwise a busy catalog
|
|
188
|
+
// (operation marker + artifact marker per execution) surfaces duplicates.
|
|
189
|
+
const suppressed = overlayContextRunRows(suppressLinkedHistoricalRows(catalog.rows), authority.now().getTime());
|
|
190
|
+
suppressed.sort((left, right) => compareRowsNewestFirst(left, right, catalog.sortTimes));
|
|
191
|
+
return {
|
|
192
|
+
rows: suppressed.slice(0, resolved.maxRows),
|
|
193
|
+
truncated: true,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
const journalRows = filterLegacyFallbackRows(await readJournalRows(resolved, legacyBudget, authority), catalog);
|
|
197
|
+
const historicalRows = filterLegacyFallbackRows(await readHistoricalRows(resolved, legacyBudget, authority, rootIdentity), catalog);
|
|
198
|
+
// Journal (ctxop_*) and historical (artifact run) IDs never collide by
|
|
199
|
+
// identity domain, so the first pass below keeps one row per (source, id).
|
|
200
|
+
const rowsByIdentity = new Map();
|
|
201
|
+
for (const row of [...catalog.rows, ...journalRows, ...historicalRows]) {
|
|
202
|
+
const identity = contextRunRowIdentity(row);
|
|
203
|
+
if (!rowsByIdentity.has(identity))
|
|
204
|
+
rowsByIdentity.set(identity, row);
|
|
205
|
+
}
|
|
206
|
+
// Second pass: one execution = one row. A journal operation and the artifact
|
|
207
|
+
// set it produced are the same context run, linked by the journal row's
|
|
208
|
+
// lifecycle.artifactRunId. The journal row is strictly more informative
|
|
209
|
+
// (real curator/finalizer status, accurate timestamps), so suppress the
|
|
210
|
+
// duplicate historical_manifest row — whether it came from the catalog
|
|
211
|
+
// markers or the top-level filesystem scan. `readDetail` still resolves the
|
|
212
|
+
// suppressed historical id directly (it is "out of the list, not gone").
|
|
213
|
+
// Note: this is a deliberate list-level suppression, distinct from the
|
|
214
|
+
// existing `truncated` cap; `truncated` is not flipped here.
|
|
215
|
+
const rows = overlayContextRunRows(suppressLinkedHistoricalRows([...rowsByIdentity.values()]), authority.now().getTime());
|
|
216
|
+
rows.sort((left, right) => compareRowsNewestFirst(left, right, catalog.sortTimes));
|
|
217
|
+
const overCap = rows.length > resolved.maxRows;
|
|
218
|
+
return {
|
|
219
|
+
rows: overCap ? rows.slice(0, resolved.maxRows) : rows,
|
|
220
|
+
truncated: catalog.truncated || catalogBudget.truncated ||
|
|
221
|
+
legacyBudget.truncated || overCap,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Drop `historical_manifest` rows that duplicate a journal row by
|
|
226
|
+
* `lifecycle.artifactRunId`. One execution = one row; the journal row wins.
|
|
227
|
+
* Operates over any union of sources (catalog markers, journal state, the
|
|
228
|
+
* filesystem scan) so the suppression is consistent regardless of which
|
|
229
|
+
* sources produced the input rows.
|
|
230
|
+
*/
|
|
231
|
+
function suppressLinkedHistoricalRows(rows) {
|
|
232
|
+
const linkedArtifactIds = new Set();
|
|
233
|
+
for (const row of rows) {
|
|
234
|
+
if (row.source === "journal" && row.lifecycle !== null) {
|
|
235
|
+
const artifactRunId = row.lifecycle.artifactRunId;
|
|
236
|
+
if (artifactRunId !== null)
|
|
237
|
+
linkedArtifactIds.add(artifactRunId);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (linkedArtifactIds.size === 0)
|
|
241
|
+
return [...rows];
|
|
242
|
+
return rows.filter((row) => !(row.source === "historical_manifest" && linkedArtifactIds.has(row.id)));
|
|
243
|
+
}
|
|
244
|
+
function filterLegacyFallbackRows(rows, catalog) {
|
|
245
|
+
if (!catalog.hasValidMarkers || catalog.legacyCutoffMs === undefined) {
|
|
246
|
+
return rows;
|
|
247
|
+
}
|
|
248
|
+
return rows.filter((row) => {
|
|
249
|
+
const createdAt = row.lifecycle?.createdAt;
|
|
250
|
+
if (createdAt === null || createdAt === undefined)
|
|
251
|
+
return true;
|
|
252
|
+
const createdAtMs = Date.parse(createdAt);
|
|
253
|
+
return Number.isFinite(createdAtMs) && createdAtMs < catalog.legacyCutoffMs;
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
/** Resolve one stable row identity directly, independent of list limits/order. */
|
|
257
|
+
export async function readContextRun(options, id, contextGeneration) {
|
|
258
|
+
return await defaultContextRunReader.readDetail(options, id, contextGeneration);
|
|
259
|
+
}
|
|
260
|
+
async function readContextRunWithAuthority(options, id, contextGeneration, authority) {
|
|
261
|
+
const resolved = resolveOptions(options);
|
|
262
|
+
if (await admitRunsRoot(resolved.runsDir) === "missing")
|
|
263
|
+
return { kind: "missing" };
|
|
264
|
+
if (isContextBuilderOperationId(id)) {
|
|
265
|
+
const state = await readJournalState(id, resolved, authority, contextGeneration);
|
|
266
|
+
if (state.kind !== "available") {
|
|
267
|
+
// state.json is sized at maxStateBytes; a too-large journal state is
|
|
268
|
+
// treated as malformed/missing rather than surfaced as a body result.
|
|
269
|
+
return state.kind === "content_too_large" ? { kind: "missing" } : state;
|
|
270
|
+
}
|
|
271
|
+
if (state.value.operation_kind === "workflow_preflight") {
|
|
272
|
+
return { kind: "missing" };
|
|
273
|
+
}
|
|
274
|
+
if (contextGeneration !== undefined &&
|
|
275
|
+
contextGeneration !== state.contextGeneration)
|
|
276
|
+
return { kind: "context_generation_changed" };
|
|
277
|
+
return { kind: "available", row: attachContextRunLiveness({
|
|
278
|
+
id,
|
|
279
|
+
contextGeneration: state.contextGeneration,
|
|
280
|
+
source: "journal",
|
|
281
|
+
compatibility: "current",
|
|
282
|
+
operationKind: state.value.operation_kind,
|
|
283
|
+
lifecycle: projectContextRunLifecycleState(state.value),
|
|
284
|
+
// Journal lifecycle state carries no roots (see ContextRunRow docs).
|
|
285
|
+
projectFolders: [],
|
|
286
|
+
}, authority.now().getTime()) };
|
|
287
|
+
}
|
|
288
|
+
if (!isHistoricalArtifactId(id) || isWorkflowOwnedId(id))
|
|
289
|
+
return { kind: "missing" };
|
|
290
|
+
const manifest = await readHistoricalManifest(id, resolved, authority, contextGeneration);
|
|
291
|
+
if (manifest.kind !== "available") {
|
|
292
|
+
// A manifest over maxManifestBytes cannot be classified; treat as missing
|
|
293
|
+
// for detail projection (the body endpoint surfaces it as content_too_large).
|
|
294
|
+
return manifest.kind === "content_too_large" ? { kind: "missing" } : manifest;
|
|
295
|
+
}
|
|
296
|
+
if (contextGeneration !== undefined &&
|
|
297
|
+
contextGeneration !== manifest.contextGeneration)
|
|
298
|
+
return { kind: "context_generation_changed" };
|
|
299
|
+
const row = synthesizeHistoricalRow(id, manifest.value, manifest.contextGeneration);
|
|
300
|
+
return row === undefined
|
|
301
|
+
? { kind: "missing" }
|
|
302
|
+
: {
|
|
303
|
+
kind: "available",
|
|
304
|
+
row: attachContextRunLiveness(row, authority.now().getTime()),
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Read a single artifact body for an already-resolved run identity. This is the
|
|
309
|
+
* bounded side channel documented in the module docstring: allowlisted key,
|
|
310
|
+
* size-capped, generation-pinned, no-follow containment. The artifact directory
|
|
311
|
+
* is resolved from the run row — for a journal row this is the linked
|
|
312
|
+
* `artifactRunId` (NOT the operation id), so a journal-state swap mid-read can
|
|
313
|
+
* serve a stale artifact; the caller (HTTP route) re-confirms via readDetail to
|
|
314
|
+
* close that TOCTOU.
|
|
315
|
+
*/
|
|
316
|
+
async function readArtifactWithAuthority(options, id, artifactKey, contextGeneration, authority) {
|
|
317
|
+
const resolved = resolveOptions(options);
|
|
318
|
+
if (await admitRunsRoot(resolved.runsDir) === "missing") {
|
|
319
|
+
return { kind: "missing" };
|
|
320
|
+
}
|
|
321
|
+
const detail = await readContextRunWithAuthority(resolved, id, contextGeneration, authority);
|
|
322
|
+
if (detail.kind === "context_generation_changed") {
|
|
323
|
+
return { kind: "context_generation_changed" };
|
|
324
|
+
}
|
|
325
|
+
if (detail.kind === "missing")
|
|
326
|
+
return { kind: "missing" };
|
|
327
|
+
if (detail.kind === "unavailable") {
|
|
328
|
+
return { kind: "unavailable", reason: "context_generation_race" };
|
|
329
|
+
}
|
|
330
|
+
const row = detail.row;
|
|
331
|
+
if (row.lifecycle === null)
|
|
332
|
+
return { kind: "missing" };
|
|
333
|
+
// The artifact set lives under <runsDir>/<artifactRunId>/, keyed by the
|
|
334
|
+
// journal state's artifact_run_id for journal rows, or the row's own id for
|
|
335
|
+
// historical rows. manifest.json sits at the run dir root; the other seven
|
|
336
|
+
// artifacts sit under <runDir>/artifacts/<filename> (producer
|
|
337
|
+
// resolveContextBuilderRunPaths).
|
|
338
|
+
const artifactRunId = row.source === "journal" ? row.lifecycle.artifactRunId : row.id;
|
|
339
|
+
if (artifactRunId === null)
|
|
340
|
+
return { kind: "missing" };
|
|
341
|
+
const filename = CONTEXT_RUN_ARTIFACT_FILE_NAMES[artifactKey];
|
|
342
|
+
const fileRelativePath = artifactKey === "manifest_json"
|
|
343
|
+
? join(artifactRunId, filename)
|
|
344
|
+
: join(artifactRunId, "artifacts", filename);
|
|
345
|
+
const result = await readContextArtifact(resolved, authority, {
|
|
346
|
+
id: artifactRunId,
|
|
347
|
+
// The artifact set is always a top-level historical-style directory,
|
|
348
|
+
// even when reached via a journal row. Source is historical_manifest so
|
|
349
|
+
// the generation is derived from the artifact directory's identity
|
|
350
|
+
// (which is what we are pinning and re-verifying).
|
|
351
|
+
source: "historical_manifest",
|
|
352
|
+
directoryRelativePath: artifactRunId,
|
|
353
|
+
fileRelativePath,
|
|
354
|
+
maxBytes: resolved.maxArtifactBytes,
|
|
355
|
+
pathLabel: filename,
|
|
356
|
+
}, undefined);
|
|
357
|
+
switch (result.kind) {
|
|
358
|
+
case "available":
|
|
359
|
+
return {
|
|
360
|
+
kind: "available",
|
|
361
|
+
content: result.value,
|
|
362
|
+
artifactRunId,
|
|
363
|
+
contextGeneration: result.contextGeneration,
|
|
364
|
+
};
|
|
365
|
+
case "missing":
|
|
366
|
+
return { kind: "missing" };
|
|
367
|
+
case "context_generation_changed":
|
|
368
|
+
return { kind: "context_generation_changed" };
|
|
369
|
+
case "content_too_large":
|
|
370
|
+
return { kind: "content_too_large" };
|
|
371
|
+
case "unavailable":
|
|
372
|
+
return { kind: "unavailable", reason: "context_generation_race" };
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
async function admitRunsRoot(runsDir) {
|
|
376
|
+
try {
|
|
377
|
+
const root = await fsLstat(runsDir);
|
|
378
|
+
if (root.isSymbolicLink() || !root.isDirectory()) {
|
|
379
|
+
throw new ContextRunSourceUnavailableError();
|
|
380
|
+
}
|
|
381
|
+
if (process.platform !== "win32" && (root.mode & 0o077) !== 0) {
|
|
382
|
+
throw new ContextRunSourceUnavailableError();
|
|
383
|
+
}
|
|
384
|
+
const uid = process.getuid?.();
|
|
385
|
+
if (uid !== undefined && root.uid !== uid) {
|
|
386
|
+
throw new ContextRunSourceUnavailableError();
|
|
387
|
+
}
|
|
388
|
+
return "available";
|
|
389
|
+
}
|
|
390
|
+
catch (error) {
|
|
391
|
+
if (isFileSystemError(error, "ENOENT"))
|
|
392
|
+
return "missing";
|
|
393
|
+
if (error instanceof ContextRunSourceUnavailableError)
|
|
394
|
+
throw error;
|
|
395
|
+
throw new ContextRunSourceUnavailableError();
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
function isFileSystemError(error, code) {
|
|
399
|
+
return typeof error === "object" && error !== null &&
|
|
400
|
+
"code" in error && error.code === code;
|
|
401
|
+
}
|
|
402
|
+
function directoryIdentity(stats) {
|
|
403
|
+
return { dev: stats.dev, ino: stats.ino, birthtimeNs: stats.birthtimeNs };
|
|
404
|
+
}
|
|
405
|
+
function directoryIdentitiesEqual(left, right) {
|
|
406
|
+
return left.dev === right.dev && left.ino === right.ino &&
|
|
407
|
+
left.birthtimeNs === right.birthtimeNs;
|
|
408
|
+
}
|
|
409
|
+
async function inspectOpenedDirectory(handle) {
|
|
410
|
+
const stats = await handle.stat({ bigint: true });
|
|
411
|
+
if (!stats.isDirectory() || stats.nlink === 0n) {
|
|
412
|
+
throw new ContextRunGenerationRaceError();
|
|
413
|
+
}
|
|
414
|
+
return directoryIdentity(stats);
|
|
415
|
+
}
|
|
416
|
+
function isProperDescendant(root, candidate) {
|
|
417
|
+
const fromRoot = relative(root, candidate);
|
|
418
|
+
return fromRoot !== "" && fromRoot !== ".." &&
|
|
419
|
+
!fromRoot.startsWith(`..${sep}`) && !isAbsolute(fromRoot);
|
|
420
|
+
}
|
|
421
|
+
function generationFor(source, id, root, directory) {
|
|
422
|
+
return deriveContextRunGeneration(source, id, root, directory);
|
|
423
|
+
}
|
|
424
|
+
async function verifyCurrentDirectoryIdentity(authority, path, flags, expected) {
|
|
425
|
+
let handle;
|
|
426
|
+
try {
|
|
427
|
+
handle = await authority.fileSystem.open(path, flags);
|
|
428
|
+
}
|
|
429
|
+
catch {
|
|
430
|
+
throw new ContextRunGenerationRaceError();
|
|
431
|
+
}
|
|
432
|
+
try {
|
|
433
|
+
if (!directoryIdentitiesEqual(await inspectOpenedDirectory(handle), expected)) {
|
|
434
|
+
throw new ContextRunGenerationRaceError();
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
finally {
|
|
438
|
+
await handle.close();
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
async function readContextArtifact(options, authority, identity, expectedGeneration) {
|
|
442
|
+
const directoryFlags = fsConstants.O_RDONLY |
|
|
443
|
+
(fsConstants.O_DIRECTORY ?? 0) |
|
|
444
|
+
(fsConstants.O_NOFOLLOW ?? 0);
|
|
445
|
+
for (const attempt of [1, 2]) {
|
|
446
|
+
let rootHandle;
|
|
447
|
+
let directoryHandle;
|
|
448
|
+
try {
|
|
449
|
+
const canonicalRoot = await authority.fileSystem.realpath(options.runsDir);
|
|
450
|
+
rootHandle = await authority.fileSystem.open(canonicalRoot, directoryFlags);
|
|
451
|
+
const rootIdentity = await inspectOpenedDirectory(rootHandle);
|
|
452
|
+
const directoryPath = resolve(canonicalRoot, identity.directoryRelativePath);
|
|
453
|
+
if (!isProperDescendant(canonicalRoot, directoryPath))
|
|
454
|
+
return { kind: "missing" };
|
|
455
|
+
directoryHandle = await authority.fileSystem.open(directoryPath, directoryFlags);
|
|
456
|
+
const directoryIdentity = await inspectOpenedDirectory(directoryHandle);
|
|
457
|
+
const descriptorPath = await authority
|
|
458
|
+
.descriptorPathVerifier(directoryHandle.fd, directoryPath)
|
|
459
|
+
.catch(() => undefined);
|
|
460
|
+
if (descriptorPath !== directoryPath) {
|
|
461
|
+
throw new ContextRunGenerationRaceError();
|
|
462
|
+
}
|
|
463
|
+
const contextGeneration = generationFor(identity.source, identity.id, rootIdentity, directoryIdentity);
|
|
464
|
+
if (expectedGeneration !== undefined &&
|
|
465
|
+
expectedGeneration !== contextGeneration)
|
|
466
|
+
return { kind: "context_generation_changed" };
|
|
467
|
+
const read = await authority.readContainedFile(canonicalRoot, identity.fileRelativePath, {
|
|
468
|
+
wantContent: true,
|
|
469
|
+
requireRegular: true,
|
|
470
|
+
requireExactRequestedPath: true,
|
|
471
|
+
descriptorPathVerifier: authority.descriptorPathVerifier,
|
|
472
|
+
maxBytes: identity.maxBytes,
|
|
473
|
+
containerLabel: identity.source === "journal"
|
|
474
|
+
? "context run journal"
|
|
475
|
+
: "context builder runs directory",
|
|
476
|
+
pathLabel: identity.pathLabel ??
|
|
477
|
+
(identity.source === "journal" ? "state.json" : "manifest.json"),
|
|
478
|
+
beforeClose: async () => {
|
|
479
|
+
await authority.beforeCommitVerification?.({
|
|
480
|
+
id: identity.id,
|
|
481
|
+
source: identity.source,
|
|
482
|
+
});
|
|
483
|
+
await verifyCurrentDirectoryIdentity(authority, directoryPath, directoryFlags, directoryIdentity);
|
|
484
|
+
await verifyCurrentDirectoryIdentity(authority, canonicalRoot, directoryFlags, rootIdentity);
|
|
485
|
+
},
|
|
486
|
+
});
|
|
487
|
+
if (read.content === undefined)
|
|
488
|
+
return { kind: "missing" };
|
|
489
|
+
await verifyCurrentDirectoryIdentity(authority, directoryPath, directoryFlags, directoryIdentity);
|
|
490
|
+
await verifyCurrentDirectoryIdentity(authority, canonicalRoot, directoryFlags, rootIdentity);
|
|
491
|
+
return {
|
|
492
|
+
kind: "available",
|
|
493
|
+
value: read.content,
|
|
494
|
+
contextGeneration,
|
|
495
|
+
fingerprint: read.fingerprint,
|
|
496
|
+
rootIdentity,
|
|
497
|
+
directoryIdentity,
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
catch (error) {
|
|
501
|
+
if (error instanceof ContextRunGenerationRaceError ||
|
|
502
|
+
error instanceof ContainedFileChangedError) {
|
|
503
|
+
if (attempt === 2) {
|
|
504
|
+
return { kind: "unavailable", reason: "context_generation_race" };
|
|
505
|
+
}
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
if (error instanceof ContainedFileTooLargeError) {
|
|
509
|
+
return { kind: "content_too_large" };
|
|
510
|
+
}
|
|
511
|
+
if (isFileSystemError(error, "ENOENT") ||
|
|
512
|
+
isFileSystemError(error, "ENOTDIR") ||
|
|
513
|
+
isFileSystemError(error, "ELOOP") ||
|
|
514
|
+
error instanceof ContainedFilePolicyError)
|
|
515
|
+
return { kind: "missing" };
|
|
516
|
+
return { kind: "unavailable", reason: "context_generation_race" };
|
|
517
|
+
}
|
|
518
|
+
finally {
|
|
519
|
+
await directoryHandle?.close().catch(() => undefined);
|
|
520
|
+
await rootHandle?.close().catch(() => undefined);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return { kind: "unavailable", reason: "context_generation_race" };
|
|
524
|
+
}
|
|
525
|
+
async function readCatalogRows(options, budget, authority, rootIdentity) {
|
|
526
|
+
const journalDir = join(options.runsDir, CONTEXT_BUILDER_LIFECYCLE_DIRECTORY);
|
|
527
|
+
const catalogDir = join(journalDir, CONTEXT_RUN_CATALOG_DIRECTORY);
|
|
528
|
+
let entries;
|
|
529
|
+
try {
|
|
530
|
+
const catalog = await fsLstat(catalogDir);
|
|
531
|
+
if (catalog.isSymbolicLink() || !catalog.isDirectory()) {
|
|
532
|
+
return {
|
|
533
|
+
rows: [],
|
|
534
|
+
sortTimes: new Map(),
|
|
535
|
+
truncated: false,
|
|
536
|
+
hasValidMarkers: false,
|
|
537
|
+
legacyCutoffMs: undefined,
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
entries = await fsReaddir(catalogDir, { withFileTypes: true });
|
|
541
|
+
}
|
|
542
|
+
catch (error) {
|
|
543
|
+
if (isFileSystemError(error, "ENOENT")) {
|
|
544
|
+
return {
|
|
545
|
+
rows: [],
|
|
546
|
+
sortTimes: new Map(),
|
|
547
|
+
truncated: false,
|
|
548
|
+
hasValidMarkers: false,
|
|
549
|
+
legacyCutoffMs: undefined,
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
return {
|
|
553
|
+
rows: [],
|
|
554
|
+
sortTimes: new Map(),
|
|
555
|
+
truncated: false,
|
|
556
|
+
hasValidMarkers: false,
|
|
557
|
+
legacyCutoffMs: undefined,
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
const cutoffRead = await readContainedRelativeFile(journalDir, CONTEXT_RUN_CATALOG_LEGACY_CUTOFF_FILENAME, {
|
|
561
|
+
wantContent: true,
|
|
562
|
+
requireRegular: true,
|
|
563
|
+
requireExactRequestedPath: true,
|
|
564
|
+
maxBytes: 32,
|
|
565
|
+
containerLabel: "context run journal",
|
|
566
|
+
pathLabel: "legacy cutoff",
|
|
567
|
+
}).catch(() => undefined);
|
|
568
|
+
const cutoffValue = cutoffRead?.content?.trim();
|
|
569
|
+
const parsedCutoff = cutoffValue === undefined ? Number.NaN : Number(cutoffValue);
|
|
570
|
+
const legacyCutoffMs = Number.isSafeInteger(parsedCutoff) && parsedCutoff >= 0
|
|
571
|
+
? parsedCutoff
|
|
572
|
+
: undefined;
|
|
573
|
+
const rows = [];
|
|
574
|
+
const sortTimes = new Map();
|
|
575
|
+
let truncated = false;
|
|
576
|
+
let hasValidMarkers = false;
|
|
577
|
+
const ordered = entries
|
|
578
|
+
.filter((entry) => entry.isFile())
|
|
579
|
+
.map((entry) => entry.name)
|
|
580
|
+
.sort()
|
|
581
|
+
.reverse();
|
|
582
|
+
for (const name of ordered) {
|
|
583
|
+
if (budget.entries >= budget.maxEntries) {
|
|
584
|
+
budget.truncated = true;
|
|
585
|
+
truncated = true;
|
|
586
|
+
break;
|
|
587
|
+
}
|
|
588
|
+
budget.entries += 1;
|
|
589
|
+
const marker = parseCatalogMarkerName(name);
|
|
590
|
+
if (marker === undefined)
|
|
591
|
+
continue;
|
|
592
|
+
hasValidMarkers = true;
|
|
593
|
+
let row;
|
|
594
|
+
if (marker.kind === "operation") {
|
|
595
|
+
const state = await readJournalState(marker.id, options, authority);
|
|
596
|
+
if (state.kind === "available" &&
|
|
597
|
+
state.value.operation_kind !== "workflow_preflight") {
|
|
598
|
+
row = {
|
|
599
|
+
id: marker.id,
|
|
600
|
+
contextGeneration: state.contextGeneration,
|
|
601
|
+
source: "journal",
|
|
602
|
+
compatibility: "current",
|
|
603
|
+
operationKind: state.value.operation_kind,
|
|
604
|
+
lifecycle: projectContextRunLifecycleState(state.value),
|
|
605
|
+
projectFolders: [],
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
else {
|
|
610
|
+
const cacheKey = historicalProjectionCacheKey(options.runsDir, marker.id);
|
|
611
|
+
// Captured BEFORE the reuse check, which evicts on any mismatch: the
|
|
612
|
+
// `missing` fallback below must keep serving exactly the row it served
|
|
613
|
+
// before this cache existed.
|
|
614
|
+
const cached = authority.historicalProjectionCache.get(cacheKey)?.row;
|
|
615
|
+
const reused = await reuseHistoricalProjection(options, authority, marker.id, rootIdentity);
|
|
616
|
+
if (reused.kind === "hit") {
|
|
617
|
+
row = reused.row;
|
|
618
|
+
}
|
|
619
|
+
else {
|
|
620
|
+
const manifest = await readHistoricalManifest(marker.id, options, authority);
|
|
621
|
+
if (manifest.kind === "available") {
|
|
622
|
+
const projected = synthesizeHistoricalRow(marker.id, manifest.value, manifest.contextGeneration);
|
|
623
|
+
if (projected !== undefined) {
|
|
624
|
+
row = cacheHistoricalProjection(authority, cacheKey, projected, {
|
|
625
|
+
manifestFingerprint: manifest.fingerprint,
|
|
626
|
+
rootIdentity: manifest.rootIdentity,
|
|
627
|
+
directoryIdentity: manifest.directoryIdentity,
|
|
628
|
+
contextGeneration: manifest.contextGeneration,
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
else if (manifest.kind === "missing" && reused.kind === "miss") {
|
|
633
|
+
// Unchanged fallback: the manifest is GONE, so there is nothing left
|
|
634
|
+
// to fingerprint. Serve the last projection unrevalidated rather than
|
|
635
|
+
// dropping a row the user can still open.
|
|
636
|
+
//
|
|
637
|
+
// Gated on `miss`. After `stale_generation` the run directory itself
|
|
638
|
+
// was recreated, so `cached.contextGeneration` is provably wrong and
|
|
639
|
+
// the identity-fenced detail route would 409 on it. Drop the row
|
|
640
|
+
// instead of advertising an unopenable generation.
|
|
641
|
+
row = cached;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
if (row === undefined)
|
|
646
|
+
continue;
|
|
647
|
+
rows.push(row);
|
|
648
|
+
sortTimes.set(contextRunRowIdentity(row), marker.createdAtMs);
|
|
649
|
+
if (rows.length > options.maxRows) {
|
|
650
|
+
truncated = true;
|
|
651
|
+
break;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
return { rows, sortTimes, truncated, hasValidMarkers, legacyCutoffMs };
|
|
655
|
+
}
|
|
656
|
+
function parseCatalogMarkerName(name) {
|
|
657
|
+
const match = CONTEXT_RUN_CATALOG_MARKER_PATTERN.exec(name);
|
|
658
|
+
if (match === null)
|
|
659
|
+
return undefined;
|
|
660
|
+
const createdAtMs = Number(match[1]);
|
|
661
|
+
const kind = match[2] === "o" ? "operation" : "artifact";
|
|
662
|
+
const id = match[3];
|
|
663
|
+
if (!Number.isSafeInteger(createdAtMs) || createdAtMs < 0)
|
|
664
|
+
return undefined;
|
|
665
|
+
if (kind === "operation") {
|
|
666
|
+
if (!isContextBuilderOperationId(id) || isWorkflowOwnedId(id))
|
|
667
|
+
return undefined;
|
|
668
|
+
}
|
|
669
|
+
else if (!isHistoricalArtifactId(id) || isWorkflowOwnedId(id)) {
|
|
670
|
+
return undefined;
|
|
671
|
+
}
|
|
672
|
+
return { createdAtMs, kind, id };
|
|
673
|
+
}
|
|
674
|
+
function contextRunRowIdentity(row) {
|
|
675
|
+
return `${row.source}:${row.id}`;
|
|
676
|
+
}
|
|
677
|
+
function historicalProjectionCacheKey(runsDir, id) {
|
|
678
|
+
return `${runsDir}\0${id}`;
|
|
679
|
+
}
|
|
680
|
+
function fingerprintFromStats(stats) {
|
|
681
|
+
return {
|
|
682
|
+
dev: stats.dev,
|
|
683
|
+
ino: stats.ino,
|
|
684
|
+
mtimeNs: stats.mtimeNs,
|
|
685
|
+
ctimeNs: stats.ctimeNs,
|
|
686
|
+
size: stats.size,
|
|
687
|
+
nlink: stats.nlink,
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Resolve the runs-root identity ONCE per `list()`.
|
|
692
|
+
*
|
|
693
|
+
* `undefined` is a total cache bypass, never a weakening: every reuse check
|
|
694
|
+
* needs this term, so an unresolvable root simply means every lookup misses and
|
|
695
|
+
* every row goes through the full fenced read.
|
|
696
|
+
*/
|
|
697
|
+
async function resolveListRootIdentity(options, authority) {
|
|
698
|
+
try {
|
|
699
|
+
const canonicalRoot = await authority.fileSystem.realpath(options.runsDir);
|
|
700
|
+
const stats = await fsLstat(canonicalRoot, { bigint: true });
|
|
701
|
+
if (!stats.isDirectory() || stats.nlink === 0n)
|
|
702
|
+
return undefined;
|
|
703
|
+
return directoryIdentity(stats);
|
|
704
|
+
}
|
|
705
|
+
catch {
|
|
706
|
+
return undefined;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
const HISTORICAL_REUSE_MISS = Object.freeze({ kind: "miss" });
|
|
710
|
+
const HISTORICAL_REUSE_STALE_GENERATION = Object.freeze({
|
|
711
|
+
kind: "stale_generation",
|
|
712
|
+
});
|
|
713
|
+
async function reuseHistoricalProjection(options, authority, id, rootIdentity) {
|
|
714
|
+
if (rootIdentity === undefined)
|
|
715
|
+
return HISTORICAL_REUSE_MISS;
|
|
716
|
+
const key = historicalProjectionCacheKey(options.runsDir, id);
|
|
717
|
+
const entry = authority.historicalProjectionCache.get(key);
|
|
718
|
+
if (entry === undefined)
|
|
719
|
+
return HISTORICAL_REUSE_MISS;
|
|
720
|
+
try {
|
|
721
|
+
const directoryPath = join(options.runsDir, id);
|
|
722
|
+
const directoryStats = await fsLstat(directoryPath, { bigint: true });
|
|
723
|
+
if (!directoryStats.isDirectory() || directoryStats.nlink === 0n) {
|
|
724
|
+
authority.historicalProjectionCache.delete(key);
|
|
725
|
+
return HISTORICAL_REUSE_MISS;
|
|
726
|
+
}
|
|
727
|
+
const currentGeneration = generationFor("historical_manifest", id, rootIdentity, directoryIdentity(directoryStats));
|
|
728
|
+
if (currentGeneration !== entry.contextGeneration) {
|
|
729
|
+
authority.historicalProjectionCache.delete(key);
|
|
730
|
+
return HISTORICAL_REUSE_STALE_GENERATION;
|
|
731
|
+
}
|
|
732
|
+
const manifestStats = await fsLstat(join(directoryPath, MANIFEST_FILENAME), {
|
|
733
|
+
bigint: true,
|
|
734
|
+
});
|
|
735
|
+
if (!manifestStats.isFile() ||
|
|
736
|
+
!containedFileFingerprintsEqual(entry.manifestFingerprint, fingerprintFromStats(manifestStats))) {
|
|
737
|
+
authority.historicalProjectionCache.delete(key);
|
|
738
|
+
return HISTORICAL_REUSE_MISS;
|
|
739
|
+
}
|
|
740
|
+
// Caller-scoped size limit — see the doc comment: report a miss, do NOT
|
|
741
|
+
// evict. The stats are already in hand, so this costs no extra syscall.
|
|
742
|
+
if (entry.manifestFingerprint.size > BigInt(options.maxManifestBytes)) {
|
|
743
|
+
return HISTORICAL_REUSE_MISS;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
catch {
|
|
747
|
+
authority.historicalProjectionCache.delete(key);
|
|
748
|
+
return HISTORICAL_REUSE_MISS;
|
|
749
|
+
}
|
|
750
|
+
// Re-insert to move the entry to the LRU tail.
|
|
751
|
+
authority.historicalProjectionCache.delete(key);
|
|
752
|
+
authority.historicalProjectionCache.set(key, entry);
|
|
753
|
+
return { kind: "hit", row: entry.row };
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* Store a projection ONLY when the fenced read that produced it carried all
|
|
757
|
+
* three revalidation terms. Malformed, missing, oversized, policy-rejected and
|
|
758
|
+
* generation-race outcomes never reach here — they are not `available`, so they
|
|
759
|
+
* are never cached and never served.
|
|
760
|
+
*/
|
|
761
|
+
function cacheHistoricalProjection(authority, key, row, evidence) {
|
|
762
|
+
const frozen = deepFreezeProjection(row);
|
|
763
|
+
const cache = authority.historicalProjectionCache;
|
|
764
|
+
cache.delete(key);
|
|
765
|
+
if (evidence.manifestFingerprint === undefined ||
|
|
766
|
+
evidence.rootIdentity === undefined ||
|
|
767
|
+
evidence.directoryIdentity === undefined) {
|
|
768
|
+
return frozen;
|
|
769
|
+
}
|
|
770
|
+
cache.set(key, {
|
|
771
|
+
manifestFingerprint: evidence.manifestFingerprint,
|
|
772
|
+
rootIdentity: evidence.rootIdentity,
|
|
773
|
+
directoryIdentity: evidence.directoryIdentity,
|
|
774
|
+
contextGeneration: evidence.contextGeneration,
|
|
775
|
+
row: frozen,
|
|
776
|
+
});
|
|
777
|
+
while (cache.size > MAX_CONTEXT_RUN_PROJECTION_CACHE_ENTRIES) {
|
|
778
|
+
const oldest = cache.keys().next().value;
|
|
779
|
+
if (oldest === undefined)
|
|
780
|
+
break;
|
|
781
|
+
cache.delete(oldest);
|
|
782
|
+
}
|
|
783
|
+
return frozen;
|
|
784
|
+
}
|
|
785
|
+
function deepFreezeProjection(value) {
|
|
786
|
+
if (value !== null && typeof value === "object" && !Object.isFrozen(value)) {
|
|
787
|
+
for (const child of Object.values(value)) {
|
|
788
|
+
deepFreezeProjection(child);
|
|
789
|
+
}
|
|
790
|
+
Object.freeze(value);
|
|
791
|
+
}
|
|
792
|
+
return value;
|
|
793
|
+
}
|
|
794
|
+
function resolveOptions(options) {
|
|
795
|
+
return {
|
|
796
|
+
runsDir: options.runsDir,
|
|
797
|
+
maxRows: boundedPositiveOption(options.maxRows, DEFAULT_CONTEXT_RUN_READER_MAX_ROWS, "maxRows"),
|
|
798
|
+
maxDirEntries: boundedPositiveOption(options.maxDirEntries, DEFAULT_CONTEXT_RUN_READER_MAX_DIR_ENTRIES, "maxDirEntries"),
|
|
799
|
+
maxStateBytes: boundedPositiveOption(options.maxStateBytes, DEFAULT_CONTEXT_RUN_READER_MAX_STATE_BYTES, "maxStateBytes"),
|
|
800
|
+
maxManifestBytes: boundedPositiveOption(options.maxManifestBytes, DEFAULT_CONTEXT_RUN_READER_MAX_MANIFEST_BYTES, "maxManifestBytes"),
|
|
801
|
+
maxArtifactBytes: boundedPositiveOption(options.maxArtifactBytes, DEFAULT_CONTEXT_RUN_READER_MAX_ARTIFACT_BYTES, "maxArtifactBytes"),
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
function boundedPositiveOption(value, maximum, label) {
|
|
805
|
+
const resolved = value ?? maximum;
|
|
806
|
+
if (!Number.isSafeInteger(resolved) || resolved < 1 || resolved > maximum) {
|
|
807
|
+
throw new TypeError(`context run reader ${label} must be an integer from 1 through ${String(maximum)}`);
|
|
808
|
+
}
|
|
809
|
+
return resolved;
|
|
810
|
+
}
|
|
811
|
+
async function readJournalRows(options, budget, authority) {
|
|
812
|
+
const journalDir = join(options.runsDir, CONTEXT_BUILDER_LIFECYCLE_DIRECTORY);
|
|
813
|
+
const entries = await fsReaddir(journalDir, {
|
|
814
|
+
withFileTypes: true,
|
|
815
|
+
}).catch(() => []);
|
|
816
|
+
const rows = [];
|
|
817
|
+
for (const entry of entries) {
|
|
818
|
+
if (budget.entries >= budget.maxEntries) {
|
|
819
|
+
budget.truncated = true;
|
|
820
|
+
break;
|
|
821
|
+
}
|
|
822
|
+
budget.entries += 1;
|
|
823
|
+
// isDirectory() is false for a symlink even when it targets a directory,
|
|
824
|
+
// so a swapped-in symlink is skipped here before any read.
|
|
825
|
+
if (!entry.isDirectory())
|
|
826
|
+
continue;
|
|
827
|
+
const name = entry.name;
|
|
828
|
+
if (!isContextBuilderOperationId(name) || isWorkflowOwnedId(name))
|
|
829
|
+
continue;
|
|
830
|
+
const state = await readJournalState(name, options, authority);
|
|
831
|
+
if (state.kind !== "available")
|
|
832
|
+
continue;
|
|
833
|
+
// Defensive: an operation dir that somehow persisted a workflow-owned kind
|
|
834
|
+
// is excluded — the journal is standalone/refine only.
|
|
835
|
+
if (state.value.operation_kind === "workflow_preflight")
|
|
836
|
+
continue;
|
|
837
|
+
rows.push({
|
|
838
|
+
id: name,
|
|
839
|
+
contextGeneration: state.contextGeneration,
|
|
840
|
+
source: "journal",
|
|
841
|
+
compatibility: "current",
|
|
842
|
+
operationKind: state.value.operation_kind,
|
|
843
|
+
lifecycle: projectContextRunLifecycleState(state.value),
|
|
844
|
+
projectFolders: [],
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
return rows;
|
|
848
|
+
}
|
|
849
|
+
async function readJournalState(name, options, authority, expectedGeneration) {
|
|
850
|
+
const result = await readContextArtifact(options, authority, {
|
|
851
|
+
id: name,
|
|
852
|
+
source: "journal",
|
|
853
|
+
directoryRelativePath: join(CONTEXT_BUILDER_LIFECYCLE_DIRECTORY, name),
|
|
854
|
+
fileRelativePath: join(CONTEXT_BUILDER_LIFECYCLE_DIRECTORY, name, CONTEXT_BUILDER_LIFECYCLE_STATE_FILENAME),
|
|
855
|
+
maxBytes: options.maxStateBytes,
|
|
856
|
+
}, expectedGeneration).catch(() => undefined);
|
|
857
|
+
if (result === undefined) {
|
|
858
|
+
return { kind: "unavailable", reason: "context_generation_race" };
|
|
859
|
+
}
|
|
860
|
+
if (result.kind !== "available")
|
|
861
|
+
return result;
|
|
862
|
+
const parsed = safeJsonParse(result.value);
|
|
863
|
+
if (parsed === undefined)
|
|
864
|
+
return { kind: "missing" };
|
|
865
|
+
try {
|
|
866
|
+
return {
|
|
867
|
+
kind: "available",
|
|
868
|
+
value: parseContextBuilderLifecycleStateV1(parsed),
|
|
869
|
+
contextGeneration: result.contextGeneration,
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
catch {
|
|
873
|
+
return { kind: "missing" };
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
async function readHistoricalRows(options, budget, authority, rootIdentity) {
|
|
877
|
+
const entries = await fsReaddir(options.runsDir, {
|
|
878
|
+
withFileTypes: true,
|
|
879
|
+
}).catch(() => []);
|
|
880
|
+
const rows = [];
|
|
881
|
+
for (const entry of entries) {
|
|
882
|
+
if (budget.entries >= budget.maxEntries) {
|
|
883
|
+
budget.truncated = true;
|
|
884
|
+
break;
|
|
885
|
+
}
|
|
886
|
+
budget.entries += 1;
|
|
887
|
+
if (!entry.isDirectory())
|
|
888
|
+
continue;
|
|
889
|
+
const name = entry.name;
|
|
890
|
+
if (name === CONTEXT_BUILDER_LIFECYCLE_DIRECTORY)
|
|
891
|
+
continue;
|
|
892
|
+
if (isWorkflowOwnedId(name))
|
|
893
|
+
continue;
|
|
894
|
+
// A ctxop_* operation ID is never a top-level artifact run dir.
|
|
895
|
+
if (isContextBuilderOperationId(name))
|
|
896
|
+
continue;
|
|
897
|
+
const reused = await reuseHistoricalProjection(options, authority, name, rootIdentity);
|
|
898
|
+
// No manifest-missing fallback on this path, so `stale_generation` and
|
|
899
|
+
// `miss` are both simply "fall through to the fenced read".
|
|
900
|
+
if (reused.kind === "hit") {
|
|
901
|
+
rows.push(reused.row);
|
|
902
|
+
continue;
|
|
903
|
+
}
|
|
904
|
+
const manifest = await readHistoricalManifest(name, options, authority);
|
|
905
|
+
if (manifest.kind !== "available")
|
|
906
|
+
continue;
|
|
907
|
+
const row = synthesizeHistoricalRow(name, manifest.value, manifest.contextGeneration);
|
|
908
|
+
if (row !== undefined) {
|
|
909
|
+
rows.push(cacheHistoricalProjection(authority, historicalProjectionCacheKey(options.runsDir, name), row, {
|
|
910
|
+
manifestFingerprint: manifest.fingerprint,
|
|
911
|
+
rootIdentity: manifest.rootIdentity,
|
|
912
|
+
directoryIdentity: manifest.directoryIdentity,
|
|
913
|
+
contextGeneration: manifest.contextGeneration,
|
|
914
|
+
}));
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
return rows;
|
|
918
|
+
}
|
|
919
|
+
async function readHistoricalManifest(name, options, authority, expectedGeneration) {
|
|
920
|
+
const result = await readContextArtifact(options, authority, {
|
|
921
|
+
id: name,
|
|
922
|
+
source: "historical_manifest",
|
|
923
|
+
directoryRelativePath: name,
|
|
924
|
+
fileRelativePath: join(name, MANIFEST_FILENAME),
|
|
925
|
+
maxBytes: options.maxManifestBytes,
|
|
926
|
+
}, expectedGeneration).catch(() => undefined);
|
|
927
|
+
if (result === undefined) {
|
|
928
|
+
return { kind: "unavailable", reason: "context_generation_race" };
|
|
929
|
+
}
|
|
930
|
+
if (result.kind !== "available")
|
|
931
|
+
return result;
|
|
932
|
+
const parsed = safeJsonParse(result.value);
|
|
933
|
+
return isRecord(parsed)
|
|
934
|
+
? {
|
|
935
|
+
kind: "available",
|
|
936
|
+
value: parsed,
|
|
937
|
+
contextGeneration: result.contextGeneration,
|
|
938
|
+
fingerprint: result.fingerprint,
|
|
939
|
+
rootIdentity: result.rootIdentity,
|
|
940
|
+
directoryIdentity: result.directoryIdentity,
|
|
941
|
+
}
|
|
942
|
+
: { kind: "missing" };
|
|
943
|
+
}
|
|
944
|
+
function synthesizeHistoricalRow(name, manifest, contextGeneration) {
|
|
945
|
+
const artifacts = manifest["artifacts"];
|
|
946
|
+
if (!isRecord(artifacts))
|
|
947
|
+
return undefined;
|
|
948
|
+
const runId = manifest["run_id"];
|
|
949
|
+
// Artifact identity is both the directory key and manifest identity. Refuse
|
|
950
|
+
// mismatches rather than creating a route that cannot be resolved directly.
|
|
951
|
+
if (typeof runId !== "string" || runId !== name || !isHistoricalArtifactId(runId)) {
|
|
952
|
+
return undefined;
|
|
953
|
+
}
|
|
954
|
+
const compatibility = classifyArtifactKeys(Object.keys(artifacts));
|
|
955
|
+
if (compatibility === undefined)
|
|
956
|
+
return undefined;
|
|
957
|
+
const projectFolders = extractManifestRoots(manifest["roots"]);
|
|
958
|
+
if (compatibility === "legacy_unsupported") {
|
|
959
|
+
return {
|
|
960
|
+
id: runId,
|
|
961
|
+
contextGeneration,
|
|
962
|
+
source: "historical_manifest",
|
|
963
|
+
compatibility: "legacy_unsupported",
|
|
964
|
+
operationKind: "historical",
|
|
965
|
+
lifecycle: null,
|
|
966
|
+
projectFolders,
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
const status = manifestStatus(manifest["status"]);
|
|
970
|
+
if (status === undefined)
|
|
971
|
+
return undefined;
|
|
972
|
+
if (manifest["created_at"] !== undefined && validIso(manifest["created_at"]) === null) {
|
|
973
|
+
return undefined;
|
|
974
|
+
}
|
|
975
|
+
return {
|
|
976
|
+
id: runId,
|
|
977
|
+
contextGeneration,
|
|
978
|
+
source: "historical_manifest",
|
|
979
|
+
compatibility,
|
|
980
|
+
operationKind: "historical",
|
|
981
|
+
lifecycle: synthesizeTerminalLifecycle(manifest, status, runId),
|
|
982
|
+
projectFolders,
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Extract project folder paths from the manifest `roots` array. Each root is a
|
|
987
|
+
* record `{ root_index, input_path, path }`; `path` is the resolved source root.
|
|
988
|
+
* Returns `[]` when roots are absent or malformed.
|
|
989
|
+
*
|
|
990
|
+
* The projection is BOUNDED to {@link MAX_DASHBOARD_ROOTS}, the same ceiling the
|
|
991
|
+
* wire contract enforces on `projectFolders`. Excess roots are DROPPED rather
|
|
992
|
+
* than erroring: an unbounded projection would produce a row that fails
|
|
993
|
+
* `ContextRunListResponseSchema.parse` at the route, and that parse throws — so
|
|
994
|
+
* one over-rooted manifest would fail the ENTIRE list/detail response, not just
|
|
995
|
+
* its own row. Input order is preserved and malformed entries are skipped
|
|
996
|
+
* without consuming budget. Sibling readers cap identically
|
|
997
|
+
* (`activity-reader.ts`, `scanner.ts`).
|
|
998
|
+
*/
|
|
999
|
+
function extractManifestRoots(value) {
|
|
1000
|
+
if (!Array.isArray(value))
|
|
1001
|
+
return [];
|
|
1002
|
+
const folders = [];
|
|
1003
|
+
for (const entry of value) {
|
|
1004
|
+
if (!isRecord(entry))
|
|
1005
|
+
continue;
|
|
1006
|
+
const path = entry["path"];
|
|
1007
|
+
if (typeof path === "string" && path.length > 0) {
|
|
1008
|
+
folders.push(path);
|
|
1009
|
+
if (folders.length >= MAX_DASHBOARD_ROOTS)
|
|
1010
|
+
break;
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
return folders;
|
|
1014
|
+
}
|
|
1015
|
+
function isHistoricalArtifactId(id) {
|
|
1016
|
+
return CONTEXT_BUILDER_ARTIFACT_ID_PATTERN.test(id);
|
|
1017
|
+
}
|
|
1018
|
+
function classifyArtifactKeys(keys) {
|
|
1019
|
+
const unique = new Set(keys);
|
|
1020
|
+
if (unique.size === 7 && LEGACY_ARTIFACT_KEYS.every((k) => unique.has(k))) {
|
|
1021
|
+
return "legacy_synthesized";
|
|
1022
|
+
}
|
|
1023
|
+
if (unique.size === 8 && CURRENT_ARTIFACT_KEYS.every((k) => unique.has(k))) {
|
|
1024
|
+
return "current";
|
|
1025
|
+
}
|
|
1026
|
+
// 9-key (or larger) manifests are a forward-incompatible artifact shape.
|
|
1027
|
+
if (unique.size >= 9)
|
|
1028
|
+
return "legacy_unsupported";
|
|
1029
|
+
return undefined;
|
|
1030
|
+
}
|
|
1031
|
+
function synthesizeTerminalLifecycle(manifest, status, runId) {
|
|
1032
|
+
const createdAt = validIso(manifest["created_at"]);
|
|
1033
|
+
const metrics = isRecord(manifest["metrics"]) ? manifest["metrics"] : {};
|
|
1034
|
+
const coverage = isRecord(manifest["coverage"])
|
|
1035
|
+
? manifest["coverage"]
|
|
1036
|
+
: undefined;
|
|
1037
|
+
return {
|
|
1038
|
+
status,
|
|
1039
|
+
phase: "complete",
|
|
1040
|
+
createdAt,
|
|
1041
|
+
updatedAt: createdAt,
|
|
1042
|
+
startedAt: createdAt,
|
|
1043
|
+
completedAt: createdAt,
|
|
1044
|
+
durationMs: null,
|
|
1045
|
+
// Live curator/finalizer stage identity is not recoverable from a committed
|
|
1046
|
+
// manifest, so historical synthesis reports them as unexecuted rather than
|
|
1047
|
+
// guessing.
|
|
1048
|
+
curator: { status: "not_configured", score: null },
|
|
1049
|
+
finalizer: { status: "not_configured", score: null },
|
|
1050
|
+
cache: { lookup: "not_checked", write: "not_attempted" },
|
|
1051
|
+
fallback: { status: "not_applicable" },
|
|
1052
|
+
counts: {
|
|
1053
|
+
filesSeen: safeCount(metrics["files_seen"]),
|
|
1054
|
+
filesConsidered: safeCount(metrics["files_considered"]),
|
|
1055
|
+
filesSelected: safeCount(metrics["files_selected"]),
|
|
1056
|
+
filesOmitted: safeCount(metrics["files_omitted"]),
|
|
1057
|
+
searchResults: 0,
|
|
1058
|
+
bytesRead: safeCount(metrics["bytes_read"]),
|
|
1059
|
+
bytesWritten: safeCount(metrics["bytes_written"]),
|
|
1060
|
+
},
|
|
1061
|
+
coverage: synthesizeCoverage(coverage),
|
|
1062
|
+
reasonCodes: ["legacy_supported"],
|
|
1063
|
+
artifactRunId: runId,
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
function synthesizeCoverage(coverage) {
|
|
1067
|
+
const status = coverage?.["status"];
|
|
1068
|
+
const policy = coverage?.["policy"];
|
|
1069
|
+
if (status === "satisfied" ||
|
|
1070
|
+
status === "degraded" ||
|
|
1071
|
+
status === "not_evaluated") {
|
|
1072
|
+
return {
|
|
1073
|
+
policy: policy === "review_deep_v1" ? "review_deep_v1" : null,
|
|
1074
|
+
status,
|
|
1075
|
+
required: safeCount(coverage?.["required"]),
|
|
1076
|
+
selected: safeCount(coverage?.["selected"]),
|
|
1077
|
+
missing: safeCount(coverage?.["missing"]),
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
return { policy: null, status: "not_evaluated", required: 0, selected: 0, missing: 0 };
|
|
1081
|
+
}
|
|
1082
|
+
export function projectContextRunLifecycleState(state) {
|
|
1083
|
+
return {
|
|
1084
|
+
status: state.status,
|
|
1085
|
+
phase: state.phase,
|
|
1086
|
+
createdAt: state.created_at,
|
|
1087
|
+
updatedAt: state.updated_at,
|
|
1088
|
+
startedAt: state.started_at ?? null,
|
|
1089
|
+
completedAt: state.completed_at ?? null,
|
|
1090
|
+
durationMs: state.duration_ms ?? null,
|
|
1091
|
+
curator: {
|
|
1092
|
+
status: state.curator.execution_status,
|
|
1093
|
+
score: state.curator.score ?? null,
|
|
1094
|
+
},
|
|
1095
|
+
finalizer: {
|
|
1096
|
+
status: state.finalizer.execution_status,
|
|
1097
|
+
score: state.finalizer.score ?? null,
|
|
1098
|
+
},
|
|
1099
|
+
cache: { lookup: state.cache.lookup, write: state.cache.write },
|
|
1100
|
+
fallback: { status: state.fallback.status },
|
|
1101
|
+
counts: {
|
|
1102
|
+
filesSeen: state.counts.files_seen,
|
|
1103
|
+
filesConsidered: state.counts.files_considered,
|
|
1104
|
+
filesSelected: state.counts.files_selected,
|
|
1105
|
+
filesOmitted: state.counts.files_omitted,
|
|
1106
|
+
searchResults: state.counts.search_results,
|
|
1107
|
+
bytesRead: state.counts.bytes_read,
|
|
1108
|
+
bytesWritten: state.counts.bytes_written,
|
|
1109
|
+
},
|
|
1110
|
+
coverage: {
|
|
1111
|
+
policy: state.coverage.policy,
|
|
1112
|
+
status: state.coverage.status,
|
|
1113
|
+
required: state.coverage.required,
|
|
1114
|
+
selected: state.coverage.selected,
|
|
1115
|
+
missing: state.coverage.missing,
|
|
1116
|
+
},
|
|
1117
|
+
reasonCodes: [...state.reason_codes],
|
|
1118
|
+
artifactRunId: state.artifact_run_id ?? null,
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
/**
|
|
1122
|
+
* Status tier for a context-run row: 0 = live pin (pending/running with a
|
|
1123
|
+
* fresh `updatedAt`), 1 = everything else. Stale persisted `running` rows,
|
|
1124
|
+
* `unknown` liveness, and `legacy_unsupported` sort with history so a
|
|
1125
|
+
* wedged journal cannot occupy the live lane forever.
|
|
1126
|
+
*/
|
|
1127
|
+
function contextRunStatusTier(row) {
|
|
1128
|
+
return isContextRunLivePin(row) ? 0 : 1;
|
|
1129
|
+
}
|
|
1130
|
+
/**
|
|
1131
|
+
* Status-tiered, newest-first: active rows (pending/running) lead, terminal
|
|
1132
|
+
* rows (succeeded/partial/failed/cancelled) follow. Within each tier, newest
|
|
1133
|
+
* first by createdAt (updatedAt fallback), then by id descending.
|
|
1134
|
+
*/
|
|
1135
|
+
function compareRowsNewestFirst(left, right, sortTimes = new Map()) {
|
|
1136
|
+
const tierOrder = contextRunStatusTier(left) - contextRunStatusTier(right);
|
|
1137
|
+
if (tierOrder !== 0)
|
|
1138
|
+
return tierOrder;
|
|
1139
|
+
const leftTime = sortTimes.get(contextRunRowIdentity(left)) ?? rowSortTime(left);
|
|
1140
|
+
const rightTime = sortTimes.get(contextRunRowIdentity(right)) ?? rowSortTime(right);
|
|
1141
|
+
if (leftTime !== rightTime)
|
|
1142
|
+
return rightTime - leftTime;
|
|
1143
|
+
return left.id < right.id ? 1 : left.id > right.id ? -1 : 0;
|
|
1144
|
+
}
|
|
1145
|
+
function rowSortTime(row) {
|
|
1146
|
+
const iso = row.lifecycle?.createdAt ?? row.lifecycle?.updatedAt ?? null;
|
|
1147
|
+
if (iso === null)
|
|
1148
|
+
return -1;
|
|
1149
|
+
const parsed = Date.parse(iso);
|
|
1150
|
+
return Number.isNaN(parsed) ? -1 : parsed;
|
|
1151
|
+
}
|
|
1152
|
+
function manifestStatus(value) {
|
|
1153
|
+
return value === "succeeded" || value === "partial" ? value : undefined;
|
|
1154
|
+
}
|
|
1155
|
+
function validIso(value) {
|
|
1156
|
+
if (typeof value !== "string")
|
|
1157
|
+
return null;
|
|
1158
|
+
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(value))
|
|
1159
|
+
return null;
|
|
1160
|
+
return Number.isNaN(Date.parse(value)) ? null : value;
|
|
1161
|
+
}
|
|
1162
|
+
function safeCount(value) {
|
|
1163
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0
|
|
1164
|
+
? Math.min(250_000_000, Math.trunc(value))
|
|
1165
|
+
: 0;
|
|
1166
|
+
}
|
|
1167
|
+
function safeJsonParse(text) {
|
|
1168
|
+
try {
|
|
1169
|
+
return JSON.parse(text);
|
|
1170
|
+
}
|
|
1171
|
+
catch {
|
|
1172
|
+
return undefined;
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
function isRecord(value) {
|
|
1176
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1177
|
+
}
|
|
1178
|
+
//# sourceMappingURL=context-run-reader.js.map
|