@litfamily/litopencode 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/.gitattributes +8 -0
- package/ATTRIBUTION.md +56 -0
- package/CHANGELOG.md +267 -0
- package/CODE_OF_CONDUCT.md +30 -0
- package/CONTRIBUTING.md +82 -0
- package/LICENSE +21 -0
- package/README-Ko-KR.md +252 -0
- package/README.md +252 -0
- package/SECURITY.md +36 -0
- package/SUPPORT.md +37 -0
- package/bin/litopencode +3 -0
- package/bin/litopencode.cjs +10 -0
- package/dist/activation-managed-prompts.d.ts +2 -0
- package/dist/activation-managed-prompts.js +51 -0
- package/dist/activation-primary-prompts.d.ts +2 -0
- package/dist/activation-primary-prompts.js +176 -0
- package/dist/activation-probe.d.ts +3 -0
- package/dist/activation-probe.js +19 -0
- package/dist/activation-prompt-utils.d.ts +6 -0
- package/dist/activation-prompt-utils.js +36 -0
- package/dist/activation-routing.d.ts +5 -0
- package/dist/activation-routing.js +340 -0
- package/dist/activation-workflow-prompts.d.ts +26 -0
- package/dist/activation-workflow-prompts.js +295 -0
- package/dist/activation.d.ts +24 -0
- package/dist/activation.js +152 -0
- package/dist/agents/defaults.d.ts +15 -0
- package/dist/agents/defaults.js +112 -0
- package/dist/agents/registry.d.ts +65 -0
- package/dist/agents/registry.js +319 -0
- package/dist/agents/specialists.d.ts +49 -0
- package/dist/agents/specialists.js +307 -0
- package/dist/agents/types.d.ts +55 -0
- package/dist/agents/types.js +4 -0
- package/dist/agents.d.ts +4 -0
- package/dist/agents.js +3 -0
- package/dist/benchmark.d.ts +94 -0
- package/dist/benchmark.js +172 -0
- package/dist/bounded-authority-hooks.d.ts +19 -0
- package/dist/bounded-authority-hooks.js +157 -0
- package/dist/bounded-authority.d.ts +184 -0
- package/dist/bounded-authority.js +825 -0
- package/dist/cache-metrics.d.ts +54 -0
- package/dist/cache-metrics.js +81 -0
- package/dist/cli/args.d.ts +5 -0
- package/dist/cli/args.js +415 -0
- package/dist/cli/auto-update.d.ts +117 -0
- package/dist/cli/auto-update.js +604 -0
- package/dist/cli/command-alias-ownership.d.ts +2 -0
- package/dist/cli/command-alias-ownership.js +14 -0
- package/dist/cli/command-aliases.d.ts +12 -0
- package/dist/cli/command-aliases.js +112 -0
- package/dist/cli/doctor.d.ts +2 -0
- package/dist/cli/doctor.js +175 -0
- package/dist/cli/host-capabilities.d.ts +12 -0
- package/dist/cli/host-capabilities.js +25 -0
- package/dist/cli/host-limits.d.ts +21 -0
- package/dist/cli/host-limits.js +83 -0
- package/dist/cli/install-config.d.ts +13 -0
- package/dist/cli/install-config.js +201 -0
- package/dist/cli/install-report.d.ts +2 -0
- package/dist/cli/install-report.js +180 -0
- package/dist/cli/install-tui.d.ts +12 -0
- package/dist/cli/install-tui.js +174 -0
- package/dist/cli/install.d.ts +2 -0
- package/dist/cli/install.js +264 -0
- package/dist/cli/json.d.ts +4 -0
- package/dist/cli/json.js +31 -0
- package/dist/cli/loop.d.ts +3 -0
- package/dist/cli/loop.js +135 -0
- package/dist/cli/lsp-capability.d.ts +17 -0
- package/dist/cli/lsp-capability.js +92 -0
- package/dist/cli/managed-skill-assets.d.ts +23 -0
- package/dist/cli/managed-skill-assets.js +117 -0
- package/dist/cli/model-catalog.d.ts +21 -0
- package/dist/cli/model-catalog.js +107 -0
- package/dist/cli/model-policy.d.ts +3 -0
- package/dist/cli/model-policy.js +143 -0
- package/dist/cli/model-routing.d.ts +5 -0
- package/dist/cli/model-routing.js +208 -0
- package/dist/cli/native-canonical-backup.d.ts +9 -0
- package/dist/cli/native-canonical-backup.js +96 -0
- package/dist/cli/native-canonical-cache.d.ts +9 -0
- package/dist/cli/native-canonical-cache.js +37 -0
- package/dist/cli/native-skill-install.d.ts +2 -0
- package/dist/cli/native-skill-install.js +128 -0
- package/dist/cli/native-skill-integrity.d.ts +25 -0
- package/dist/cli/native-skill-integrity.js +178 -0
- package/dist/cli/native-skill-tree.d.ts +12 -0
- package/dist/cli/native-skill-tree.js +104 -0
- package/dist/cli/native-skills.d.ts +22 -0
- package/dist/cli/native-skills.js +105 -0
- package/dist/cli/plugin-mutation.d.ts +4 -0
- package/dist/cli/plugin-mutation.js +89 -0
- package/dist/cli/scientific-visualization-dependencies.d.ts +18 -0
- package/dist/cli/scientific-visualization-dependencies.js +91 -0
- package/dist/cli/skill-loop.d.ts +3 -0
- package/dist/cli/skill-loop.js +649 -0
- package/dist/cli/types.d.ts +188 -0
- package/dist/cli/types.js +1 -0
- package/dist/cli/update-check.d.ts +2 -0
- package/dist/cli/update-check.js +15 -0
- package/dist/cli/update-notifier.d.ts +70 -0
- package/dist/cli/update-notifier.js +717 -0
- package/dist/cli/vendor-path-migration.d.ts +9 -0
- package/dist/cli/vendor-path-migration.js +96 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +250 -0
- package/dist/commands.d.ts +331 -0
- package/dist/commands.js +600 -0
- package/dist/config-parser.d.ts +5 -0
- package/dist/config-parser.js +274 -0
- package/dist/config.d.ts +72 -0
- package/dist/config.js +157 -0
- package/dist/deliverable-hedge-guard.d.ts +26 -0
- package/dist/deliverable-hedge-guard.js +302 -0
- package/dist/durable-plan.d.ts +23 -0
- package/dist/durable-plan.js +349 -0
- package/dist/features.d.ts +828 -0
- package/dist/features.js +1109 -0
- package/dist/hooks.d.ts +17 -0
- package/dist/hooks.js +83 -0
- package/dist/ignition.d.ts +14 -0
- package/dist/ignition.js +33 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +30 -0
- package/dist/inert-data.d.ts +2 -0
- package/dist/inert-data.js +13 -0
- package/dist/knowledge.d.ts +98 -0
- package/dist/knowledge.js +961 -0
- package/dist/ledger.d.ts +181 -0
- package/dist/ledger.js +1179 -0
- package/dist/lit-fetch-classify.d.ts +8 -0
- package/dist/lit-fetch-classify.js +107 -0
- package/dist/lit-fetch-content.d.ts +2 -0
- package/dist/lit-fetch-content.js +43 -0
- package/dist/lit-fetch-http.d.ts +10 -0
- package/dist/lit-fetch-http.js +82 -0
- package/dist/lit-fetch-result.d.ts +2 -0
- package/dist/lit-fetch-result.js +65 -0
- package/dist/lit-fetch-ssrf.d.ts +9 -0
- package/dist/lit-fetch-ssrf.js +77 -0
- package/dist/lit-fetch-url.d.ts +3 -0
- package/dist/lit-fetch-url.js +28 -0
- package/dist/lit-fetch.d.ts +69 -0
- package/dist/lit-fetch.js +84 -0
- package/dist/lit-mark.d.ts +17 -0
- package/dist/lit-mark.js +129 -0
- package/dist/logger.d.ts +9 -0
- package/dist/logger.js +21 -0
- package/dist/model-route-policy.d.ts +25 -0
- package/dist/model-route-policy.js +129 -0
- package/dist/reader-facing-output.d.ts +3 -0
- package/dist/reader-facing-output.js +28 -0
- package/dist/rules/discovery.d.ts +36 -0
- package/dist/rules/discovery.js +270 -0
- package/dist/rules/engine.d.ts +43 -0
- package/dist/rules/engine.js +236 -0
- package/dist/rules/frontmatter.d.ts +16 -0
- package/dist/rules/frontmatter.js +179 -0
- package/dist/rules/glob.d.ts +12 -0
- package/dist/rules/glob.js +233 -0
- package/dist/rules/hooks.d.ts +25 -0
- package/dist/rules/hooks.js +75 -0
- package/dist/rules/output-style.d.ts +1 -0
- package/dist/rules/output-style.js +20 -0
- package/dist/scientific-visualization-banner.d.ts +1 -0
- package/dist/scientific-visualization-banner.js +2 -0
- package/dist/search-workflow-ideas.d.ts +67 -0
- package/dist/search-workflow-ideas.js +128 -0
- package/dist/secret-shapes.d.ts +9 -0
- package/dist/secret-shapes.js +63 -0
- package/dist/server.d.ts +6 -0
- package/dist/server.js +129 -0
- package/dist/session-lineage.d.ts +8 -0
- package/dist/session-lineage.js +12 -0
- package/dist/skill-loop/apply.d.ts +19 -0
- package/dist/skill-loop/apply.js +483 -0
- package/dist/skill-loop/config.d.ts +50 -0
- package/dist/skill-loop/config.js +215 -0
- package/dist/skill-loop/curator.d.ts +18 -0
- package/dist/skill-loop/curator.js +232 -0
- package/dist/skill-loop/ledger.d.ts +39 -0
- package/dist/skill-loop/ledger.js +344 -0
- package/dist/skill-loop/proposals.d.ts +48 -0
- package/dist/skill-loop/proposals.js +290 -0
- package/dist/skill-loop/storage.d.ts +72 -0
- package/dist/skill-loop/storage.js +817 -0
- package/dist/skill-loop/time.d.ts +2 -0
- package/dist/skill-loop/time.js +23 -0
- package/dist/skill-loop/transaction.d.ts +62 -0
- package/dist/skill-loop/transaction.js +836 -0
- package/dist/skill-loop/usage.d.ts +31 -0
- package/dist/skill-loop/usage.js +146 -0
- package/dist/skill-observer.d.ts +22 -0
- package/dist/skill-observer.js +1154 -0
- package/dist/skill-renames.d.ts +5 -0
- package/dist/skill-renames.js +8 -0
- package/dist/skills.d.ts +307 -0
- package/dist/skills.js +450 -0
- package/dist/stable-identity.d.ts +14 -0
- package/dist/stable-identity.js +15 -0
- package/dist/state.d.ts +25 -0
- package/dist/state.js +38 -0
- package/dist/strict-json.d.ts +2 -0
- package/dist/strict-json.js +94 -0
- package/dist/tool-guards.d.ts +34 -0
- package/dist/tool-guards.js +210 -0
- package/dist/tool-kit.d.ts +32 -0
- package/dist/tool-kit.js +75 -0
- package/dist/tools.d.ts +43 -0
- package/dist/tools.js +381 -0
- package/dist/uiux-visual-catalog.d.ts +50 -0
- package/dist/uiux-visual-catalog.js +103 -0
- package/dist/user-facing-markdown.d.ts +5 -0
- package/dist/user-facing-markdown.js +93 -0
- package/dist/workflow-families.d.ts +16 -0
- package/dist/workflow-families.js +95 -0
- package/docs/assets/cover.webp +0 -0
- package/docs/assets/litopencode-continuity-1600.webp +0 -0
- package/docs/assets/litopencode-ignition-1600.webp +0 -0
- package/docs/assets/readme/badge-license.svg +1 -0
- package/docs/assets/readme/badge-version.svg +1 -0
- package/docs/assets/readme/litopencode-clay-icon.png +0 -0
- package/docs/assets/readme/litopencode-wordmark.svg +5 -0
- package/docs/lit-mark.md +46 -0
- package/docs/migration.md +162 -0
- package/docs/privacy.md +82 -0
- package/docs/reference-Ko-KR.md +309 -0
- package/docs/reference.md +444 -0
- package/output-styles/asd-ste100-ko.md +41 -0
- package/output-styles/asd-ste100.md +40 -0
- package/output-styles/eli5-ko.md +13 -0
- package/output-styles/eli5.md +11 -0
- package/package.json +57 -0
- package/qa/fixtures/litfamily-harness-speed-v1.json +38 -0
- package/qa/harness-speed-contract.mjs +209 -0
- package/qa/harness-speed-v2-contract.mjs +213 -0
- package/qa/harness-speed-verdict.mjs +105 -0
- package/skills/agent-roster/SKILL.md +262 -0
- package/skills/autoconference/LICENSE +21 -0
- package/skills/autoconference/PROVENANCE.md +20 -0
- package/skills/autoconference/SKILL.md +125 -0
- package/skills/autoconference/assets/conference_template.md +76 -0
- package/skills/autoconference/assets/report_template.md +53 -0
- package/skills/autoconference/assets/synthesis_template.md +39 -0
- package/skills/autoconference/modes/analyze.md +255 -0
- package/skills/autoconference/modes/core.md +112 -0
- package/skills/autoconference/modes/debate.md +373 -0
- package/skills/autoconference/modes/plan.md +310 -0
- package/skills/autoconference/modes/resume.md +48 -0
- package/skills/autoconference/modes/ship.md +57 -0
- package/skills/autoconference/modes/survey.md +109 -0
- package/skills/autoconference/references/agent-prompts.md +134 -0
- package/skills/autoconference/references/conference-protocol.md +102 -0
- package/skills/autoconference/references/convergence-guide.md +167 -0
- package/skills/autoconference/references/core-principles.md +83 -0
- package/skills/autoconference/references/crash-recovery.md +104 -0
- package/skills/autoconference/references/results-logging.md +198 -0
- package/skills/autoconference/references/upstream-family-contract.md +130 -0
- package/skills/autoconference/references/visualization-guide.md +105 -0
- package/skills/autoconference/scripts/check_conference.sh +371 -0
- package/skills/autoconference/scripts/init_conference.py +625 -0
- package/skills/autoconference/scripts/style_presets.py +104 -0
- package/skills/autoresearch/LICENSE +21 -0
- package/skills/autoresearch/PROVENANCE.md +20 -0
- package/skills/autoresearch/SKILL.md +131 -0
- package/skills/autoresearch/assets/report_template.md +50 -0
- package/skills/autoresearch/assets/research_template.md +38 -0
- package/skills/autoresearch/assets/results_template.tsv +1 -0
- package/skills/autoresearch/modes/core.md +216 -0
- package/skills/autoresearch/modes/debug.md +235 -0
- package/skills/autoresearch/modes/fix.md +173 -0
- package/skills/autoresearch/modes/learn.md +91 -0
- package/skills/autoresearch/modes/plan.md +291 -0
- package/skills/autoresearch/modes/predict.md +266 -0
- package/skills/autoresearch/modes/reason.md +170 -0
- package/skills/autoresearch/modes/scenario.md +164 -0
- package/skills/autoresearch/modes/security.md +282 -0
- package/skills/autoresearch/modes/ship.md +49 -0
- package/skills/autoresearch/references/core-principles.md +80 -0
- package/skills/autoresearch/references/evaluator-contract.md +126 -0
- package/skills/autoresearch/references/investigation-techniques.md +205 -0
- package/skills/autoresearch/references/owasp-checklist.md +161 -0
- package/skills/autoresearch/references/persona-templates.md +232 -0
- package/skills/autoresearch/references/results-logging.md +96 -0
- package/skills/autoresearch/references/scenario-dimensions.md +178 -0
- package/skills/autoresearch/references/stride-model.md +196 -0
- package/skills/autoresearch/references/stuck-detection.md +107 -0
- package/skills/autoresearch/references/type-checklists.md +163 -0
- package/skills/autoresearch/references/upstream-family-contract.md +125 -0
- package/skills/autoresearch/references/visualization-guide.md +95 -0
- package/skills/autoresearch/scripts/check_progress.sh +272 -0
- package/skills/autoresearch/scripts/init_research.py +330 -0
- package/skills/autoresearch/scripts/run_with_deadline.py +164 -0
- package/skills/autoresearch/scripts/style_presets.py +104 -0
- package/skills/browser-drive/SKILL.md +133 -0
- package/skills/browser-drive/references/snapshot-act-loop.md +37 -0
- package/skills/browser-drive/scripts/capability-probe.mjs +262 -0
- package/skills/comment-checker/SKILL.md +171 -0
- package/skills/debugging/SKILL.md +202 -0
- package/skills/debugging/references/README.md +15 -0
- package/skills/debugging/references/escalation.md +43 -0
- package/skills/debugging/references/runtimes/README.md +17 -0
- package/skills/debugging/references/runtimes/bundled-js-binary.md +44 -0
- package/skills/debugging/references/runtimes/go.md +37 -0
- package/skills/debugging/references/runtimes/native-binary.md +44 -0
- package/skills/debugging/references/runtimes/node.md +43 -0
- package/skills/debugging/references/runtimes/python.md +41 -0
- package/skills/debugging/references/runtimes/rust.md +37 -0
- package/skills/debugging/references/tools.md +72 -0
- package/skills/deep-interview/SKILL.md +201 -0
- package/skills/doctor-installer/SKILL.md +252 -0
- package/skills/durable-litgoal/SKILL.md +248 -0
- package/skills/frontend-ui-ux/SKILL.md +70 -0
- package/skills/frontend-ui-ux/data/LICENSE +21 -0
- package/skills/frontend-ui-ux/data/PROVENANCE.json +1088 -0
- package/skills/frontend-ui-ux/data/THIRD-PARTY-NOTICE.txt +14 -0
- package/skills/frontend-ui-ux/data/design-intelligence.json +1 -0
- package/skills/frontend-ui-ux/references/_canonical-corpus/ATTRIBUTION.md +217 -0
- package/skills/frontend-ui-ux/references/_canonical-corpus/LICENSE +21 -0
- package/skills/frontend-ui-ux/references/_canonical-corpus/LICENSE-Apache-2.0.txt +201 -0
- package/skills/frontend-ui-ux/references/_canonical-corpus/manifest.json +867 -0
- package/skills/frontend-ui-ux/references/adaptive-layout.md +85 -0
- package/skills/frontend-ui-ux/references/brand-and-imagery.md +79 -0
- package/skills/frontend-ui-ux/references/complete-contract.md +282 -0
- package/skills/frontend-ui-ux/references/composition.md +87 -0
- package/skills/frontend-ui-ux/references/creative-directions.md +83 -0
- package/skills/frontend-ui-ux/references/design/README.md +248 -0
- package/skills/frontend-ui-ux/references/design/_INDEX.md +191 -0
- package/skills/frontend-ui-ux/references/design/airbnb.md +393 -0
- package/skills/frontend-ui-ux/references/design/airtable.md +92 -0
- package/skills/frontend-ui-ux/references/design/apple.md +250 -0
- package/skills/frontend-ui-ux/references/design/aside.md +209 -0
- package/skills/frontend-ui-ux/references/design/binance.md +348 -0
- package/skills/frontend-ui-ux/references/design/bmw.md +183 -0
- package/skills/frontend-ui-ux/references/design/brutalist-skill.md +92 -0
- package/skills/frontend-ui-ux/references/design/bugatti.md +271 -0
- package/skills/frontend-ui-ux/references/design/cal.md +262 -0
- package/skills/frontend-ui-ux/references/design/claude.md +315 -0
- package/skills/frontend-ui-ux/references/design/clay.md +307 -0
- package/skills/frontend-ui-ux/references/design/clickhouse.md +284 -0
- package/skills/frontend-ui-ux/references/design/clone-from-url.md +65 -0
- package/skills/frontend-ui-ux/references/design/cohere.md +269 -0
- package/skills/frontend-ui-ux/references/design/coinbase.md +132 -0
- package/skills/frontend-ui-ux/references/design/composio.md +310 -0
- package/skills/frontend-ui-ux/references/design/cursor.md +312 -0
- package/skills/frontend-ui-ux/references/design/design-system-architecture.md +244 -0
- package/skills/frontend-ui-ux/references/design/elevenlabs.md +268 -0
- package/skills/frontend-ui-ux/references/design/expo.md +284 -0
- package/skills/frontend-ui-ux/references/design/ferrari.md +317 -0
- package/skills/frontend-ui-ux/references/design/figma.md +223 -0
- package/skills/frontend-ui-ux/references/design/framer.md +249 -0
- package/skills/frontend-ui-ux/references/design/gpt-tasteskill.md +74 -0
- package/skills/frontend-ui-ux/references/design/hashicorp.md +281 -0
- package/skills/frontend-ui-ux/references/design/ibm.md +335 -0
- package/skills/frontend-ui-ux/references/design/image-to-code-skill.md +1228 -0
- package/skills/frontend-ui-ux/references/design/imagegen-brandkit.md +798 -0
- package/skills/frontend-ui-ux/references/design/imagegen-frontend-mobile.md +1465 -0
- package/skills/frontend-ui-ux/references/design/imagegen-frontend-web.md +987 -0
- package/skills/frontend-ui-ux/references/design/intercom.md +149 -0
- package/skills/frontend-ui-ux/references/design/kraken.md +128 -0
- package/skills/frontend-ui-ux/references/design/lamborghini.md +291 -0
- package/skills/frontend-ui-ux/references/design/layout-skill.md +107 -0
- package/skills/frontend-ui-ux/references/design/lazyweb.md +77 -0
- package/skills/frontend-ui-ux/references/design/linear.app.md +370 -0
- package/skills/frontend-ui-ux/references/design/lovable.md +301 -0
- package/skills/frontend-ui-ux/references/design/mastercard.md +368 -0
- package/skills/frontend-ui-ux/references/design/meta.md +369 -0
- package/skills/frontend-ui-ux/references/design/minimalist-skill.md +85 -0
- package/skills/frontend-ui-ux/references/design/minimax.md +260 -0
- package/skills/frontend-ui-ux/references/design/mintlify.md +329 -0
- package/skills/frontend-ui-ux/references/design/miro.md +111 -0
- package/skills/frontend-ui-ux/references/design/mistral.ai.md +264 -0
- package/skills/frontend-ui-ux/references/design/mongodb.md +269 -0
- package/skills/frontend-ui-ux/references/design/nike.md +366 -0
- package/skills/frontend-ui-ux/references/design/notion.md +312 -0
- package/skills/frontend-ui-ux/references/design/nvidia.md +296 -0
- package/skills/frontend-ui-ux/references/design/ollama.md +270 -0
- package/skills/frontend-ui-ux/references/design/opencode.ai.md +284 -0
- package/skills/frontend-ui-ux/references/design/output-skill.md +49 -0
- package/skills/frontend-ui-ux/references/design/pinterest.md +233 -0
- package/skills/frontend-ui-ux/references/design/playstation.md +367 -0
- package/skills/frontend-ui-ux/references/design/posthog.md +259 -0
- package/skills/frontend-ui-ux/references/design/raycast.md +271 -0
- package/skills/frontend-ui-ux/references/design/react-dev-tooling-skill.md +230 -0
- package/skills/frontend-ui-ux/references/design/redesign-skill.md +178 -0
- package/skills/frontend-ui-ux/references/design/renault.md +314 -0
- package/skills/frontend-ui-ux/references/design/replicate.md +264 -0
- package/skills/frontend-ui-ux/references/design/resend.md +306 -0
- package/skills/frontend-ui-ux/references/design/revolut.md +188 -0
- package/skills/frontend-ui-ux/references/design/runwayml.md +247 -0
- package/skills/frontend-ui-ux/references/design/sanity.md +360 -0
- package/skills/frontend-ui-ux/references/design/sentry.md +265 -0
- package/skills/frontend-ui-ux/references/design/shopify.md +353 -0
- package/skills/frontend-ui-ux/references/design/soft-skill.md +98 -0
- package/skills/frontend-ui-ux/references/design/spacex.md +197 -0
- package/skills/frontend-ui-ux/references/design/spotify.md +249 -0
- package/skills/frontend-ui-ux/references/design/starbucks.md +583 -0
- package/skills/frontend-ui-ux/references/design/stitch-design-example.md +121 -0
- package/skills/frontend-ui-ux/references/design/stitch-skill.md +184 -0
- package/skills/frontend-ui-ux/references/design/stripe.md +325 -0
- package/skills/frontend-ui-ux/references/design/supabase.md +258 -0
- package/skills/frontend-ui-ux/references/design/superhuman.md +255 -0
- package/skills/frontend-ui-ux/references/design/taste-skill.md +1206 -0
- package/skills/frontend-ui-ux/references/design/tesla.md +289 -0
- package/skills/frontend-ui-ux/references/design/theverge.md +342 -0
- package/skills/frontend-ui-ux/references/design/together.ai.md +266 -0
- package/skills/frontend-ui-ux/references/design/uber.md +298 -0
- package/skills/frontend-ui-ux/references/design/vercel.md +313 -0
- package/skills/frontend-ui-ux/references/design/vodafone.md +426 -0
- package/skills/frontend-ui-ux/references/design/voltagent.md +326 -0
- package/skills/frontend-ui-ux/references/design/warp.md +256 -0
- package/skills/frontend-ui-ux/references/design/webflow.md +95 -0
- package/skills/frontend-ui-ux/references/design/wired.md +281 -0
- package/skills/frontend-ui-ux/references/design/wise.md +176 -0
- package/skills/frontend-ui-ux/references/design/x.ai.md +260 -0
- package/skills/frontend-ui-ux/references/design/zapier.md +331 -0
- package/skills/frontend-ui-ux/references/designpowers/EVIDENCE.md +97 -0
- package/skills/frontend-ui-ux/references/designpowers/README.md +48 -0
- package/skills/frontend-ui-ux/references/designpowers/UPSTREAM.md +80 -0
- package/skills/frontend-ui-ux/references/designpowers/lane-a-direction.md +64 -0
- package/skills/frontend-ui-ux/references/designpowers/lane-b-execution.md +65 -0
- package/skills/frontend-ui-ux/references/designpowers/lane-c-review.md +65 -0
- package/skills/frontend-ui-ux/references/designpowers/lane-d-memory.md +83 -0
- package/skills/frontend-ui-ux/references/designpowers/orchestration.md +80 -0
- package/skills/frontend-ui-ux/references/designpowers/routing.md +79 -0
- package/skills/frontend-ui-ux/references/designpowers/vendor/LICENSE +21 -0
- package/skills/frontend-ui-ux/references/designpowers/vendor/agents/accessibility-reviewer.md +83 -0
- package/skills/frontend-ui-ux/references/designpowers/vendor/agents/content-writer.md +132 -0
- package/skills/frontend-ui-ux/references/designpowers/vendor/agents/design-builder.md +109 -0
- package/skills/frontend-ui-ux/references/designpowers/vendor/agents/design-critic.md +89 -0
- package/skills/frontend-ui-ux/references/designpowers/vendor/agents/design-lead.md +113 -0
- package/skills/frontend-ui-ux/references/designpowers/vendor/agents/design-scout.md +78 -0
- package/skills/frontend-ui-ux/references/designpowers/vendor/agents/design-strategist.md +121 -0
- package/skills/frontend-ui-ux/references/designpowers/vendor/agents/heuristic-evaluator.md +268 -0
- package/skills/frontend-ui-ux/references/designpowers/vendor/agents/inspiration-scout.md +107 -0
- package/skills/frontend-ui-ux/references/designpowers/vendor/agents/motion-designer.md +120 -0
- package/skills/frontend-ui-ux/references/designpowers/vendor/skills/accessible-content/reference.md +101 -0
- package/skills/frontend-ui-ux/references/designpowers/vendor/skills/adaptive-interfaces/reference.md +109 -0
- package/skills/frontend-ui-ux/references/designpowers/vendor/skills/cognitive-accessibility/reference.md +107 -0
- package/skills/frontend-ui-ux/references/designpowers/vendor/skills/design-debate/reference.md +199 -0
- package/skills/frontend-ui-ux/references/designpowers/vendor/skills/design-debt-tracker/reference.md +174 -0
- package/skills/frontend-ui-ux/references/designpowers/vendor/skills/design-handoff/reference.md +125 -0
- package/skills/frontend-ui-ux/references/designpowers/vendor/skills/design-md/reference.md +106 -0
- package/skills/frontend-ui-ux/references/designpowers/vendor/skills/design-retrospective/reference.md +266 -0
- package/skills/frontend-ui-ux/references/designpowers/vendor/skills/design-review/reference.md +123 -0
- package/skills/frontend-ui-ux/references/designpowers/vendor/skills/design-system-alignment/reference.md +120 -0
- package/skills/frontend-ui-ux/references/designpowers/vendor/skills/designpowers-critique/reference.md +164 -0
- package/skills/frontend-ui-ux/references/designpowers/vendor/skills/heuristic-evaluation/reference.md +85 -0
- package/skills/frontend-ui-ux/references/designpowers/vendor/skills/inclusive-personas/reference.md +98 -0
- package/skills/frontend-ui-ux/references/designpowers/vendor/skills/inspiration-scouting/reference.md +165 -0
- package/skills/frontend-ui-ux/references/designpowers/vendor/skills/interaction-design/reference.md +122 -0
- package/skills/frontend-ui-ux/references/designpowers/vendor/skills/motion-choreography/reference.md +81 -0
- package/skills/frontend-ui-ux/references/designpowers/vendor/skills/research-planning/reference.md +96 -0
- package/skills/frontend-ui-ux/references/designpowers/vendor/skills/responsive-patterns/reference.md +77 -0
- package/skills/frontend-ui-ux/references/designpowers/vendor/skills/synthetic-user-testing/reference.md +192 -0
- package/skills/frontend-ui-ux/references/designpowers/vendor/skills/taste-feedback/reference.md +165 -0
- package/skills/frontend-ui-ux/references/designpowers/vendor/skills/taste-report/reference.md +78 -0
- package/skills/frontend-ui-ux/references/designpowers/vendor/skills/token-architecture/reference.md +75 -0
- package/skills/frontend-ui-ux/references/designpowers/vendor/skills/ui-composition/reference.md +117 -0
- package/skills/frontend-ui-ux/references/designpowers/vendor/skills/usability-testing/reference.md +78 -0
- package/skills/frontend-ui-ux/references/designpowers/vendor/skills/verification-before-shipping/reference.md +125 -0
- package/skills/frontend-ui-ux/references/designpowers/vendor/skills/voice-and-tone/reference.md +79 -0
- package/skills/frontend-ui-ux/references/designpowers/vendor/skills/writing-design-plans/reference.md +119 -0
- package/skills/frontend-ui-ux/references/evidence-review.md +103 -0
- package/skills/frontend-ui-ux/references/implementation-platforms.md +94 -0
- package/skills/frontend-ui-ux/references/inclusive-interface.md +85 -0
- package/skills/frontend-ui-ux/references/interaction-motion.md +93 -0
- package/skills/frontend-ui-ux/references/operating-lanes.md +80 -0
- package/skills/frontend-ui-ux/references/perfection/README.md +160 -0
- package/skills/frontend-ui-ux/references/perfection/react-perf-tooling.md +127 -0
- package/skills/frontend-ui-ux/references/performance-delivery.md +77 -0
- package/skills/frontend-ui-ux/references/product-direction.md +84 -0
- package/skills/frontend-ui-ux/references/redesign-playbook.md +88 -0
- package/skills/frontend-ui-ux/references/system-foundations.md +86 -0
- package/skills/frontend-ui-ux/references/taste-direction.md +55 -0
- package/skills/frontend-ui-ux/references/ui-ux-db/README.md +659 -0
- package/skills/frontend-ui-ux/references/ui-ux-db/data/charts.csv +26 -0
- package/skills/frontend-ui-ux/references/ui-ux-db/data/colors.csv +162 -0
- package/skills/frontend-ui-ux/references/ui-ux-db/data/icons.csv +106 -0
- package/skills/frontend-ui-ux/references/ui-ux-db/data/landing.csv +35 -0
- package/skills/frontend-ui-ux/references/ui-ux-db/data/products.csv +162 -0
- package/skills/frontend-ui-ux/references/ui-ux-db/data/react-performance.csv +45 -0
- package/skills/frontend-ui-ux/references/ui-ux-db/data/stacks/astro.csv +54 -0
- package/skills/frontend-ui-ux/references/ui-ux-db/data/stacks/flutter.csv +53 -0
- package/skills/frontend-ui-ux/references/ui-ux-db/data/stacks/html-tailwind.csv +56 -0
- package/skills/frontend-ui-ux/references/ui-ux-db/data/stacks/jetpack-compose.csv +53 -0
- package/skills/frontend-ui-ux/references/ui-ux-db/data/stacks/nextjs.csv +53 -0
- package/skills/frontend-ui-ux/references/ui-ux-db/data/stacks/nuxt-ui.csv +51 -0
- package/skills/frontend-ui-ux/references/ui-ux-db/data/stacks/nuxtjs.csv +59 -0
- package/skills/frontend-ui-ux/references/ui-ux-db/data/stacks/react-native.csv +52 -0
- package/skills/frontend-ui-ux/references/ui-ux-db/data/stacks/react.csv +54 -0
- package/skills/frontend-ui-ux/references/ui-ux-db/data/stacks/shadcn.csv +61 -0
- package/skills/frontend-ui-ux/references/ui-ux-db/data/stacks/svelte.csv +54 -0
- package/skills/frontend-ui-ux/references/ui-ux-db/data/stacks/swiftui.csv +51 -0
- package/skills/frontend-ui-ux/references/ui-ux-db/data/stacks/vue.csv +50 -0
- package/skills/frontend-ui-ux/references/ui-ux-db/data/styles.csv +85 -0
- package/skills/frontend-ui-ux/references/ui-ux-db/data/typography.csv +74 -0
- package/skills/frontend-ui-ux/references/ui-ux-db/data/ui-reasoning.csv +162 -0
- package/skills/frontend-ui-ux/references/ui-ux-db/data/ux-guidelines.csv +100 -0
- package/skills/frontend-ui-ux/references/ui-ux-db/data/web-interface.csv +31 -0
- package/skills/frontend-ui-ux/references/ui-ux-db/scripts/core.py +262 -0
- package/skills/frontend-ui-ux/references/ui-ux-db/scripts/design_system.py +1148 -0
- package/skills/frontend-ui-ux/references/ui-ux-db/scripts/search.py +114 -0
- package/skills/frontend-ui-ux/references/visual-language.md +84 -0
- package/skills/frontend-ui-ux/references/visual-reconstruction.md +87 -0
- package/skills/frontend-ui-ux/schemas/design-contract-v1alpha1.json +570 -0
- package/skills/frontend-ui-ux/schemas/design-contract-v1beta1.json +32 -0
- package/skills/frontend-ui-ux/schemas/design-contract-v1beta2.json +34 -0
- package/skills/frontend-ui-ux/scripts/bounded-json.mjs +45 -0
- package/skills/frontend-ui-ux/scripts/canonical-json.mjs +26 -0
- package/skills/frontend-ui-ux/scripts/contract-error.mjs +15 -0
- package/skills/frontend-ui-ux/scripts/csv.mjs +64 -0
- package/skills/frontend-ui-ux/scripts/dataset.mjs +65 -0
- package/skills/frontend-ui-ux/scripts/design-contract.mjs +609 -0
- package/skills/frontend-ui-ux/scripts/import-design-intelligence.mjs +193 -0
- package/skills/frontend-ui-ux/scripts/retrieval.mjs +92 -0
- package/skills/frontend-ui-ux/scripts/stable-file-read.mjs +237 -0
- package/skills/frontend-ui-ux/scripts/stdin-json.mjs +16 -0
- package/skills/frontend-ui-ux/scripts/strict-json.mjs +102 -0
- package/skills/frontend-ui-ux/scripts/uiux.mjs +86 -0
- package/skills/frontend-ui-ux/scripts/verify-canonical-corpus.mjs +266 -0
- package/skills/lit-burnoff/SKILL.md +227 -0
- package/skills/lit-burnoff-file/SKILL.md +175 -0
- package/skills/lit-code/SKILL.md +266 -0
- package/skills/lit-code/references/README.md +18 -0
- package/skills/lit-code/references/go/README.md +12 -0
- package/skills/lit-code/references/go/concurrency.md +42 -0
- package/skills/lit-code/references/go/error-handling.md +47 -0
- package/skills/lit-code/references/go/testing.md +55 -0
- package/skills/lit-code/references/go/tooling.md +35 -0
- package/skills/lit-code/references/go/type-patterns.md +50 -0
- package/skills/lit-code/references/python/README.md +12 -0
- package/skills/lit-code/references/python/async.md +50 -0
- package/skills/lit-code/references/python/error-handling.md +44 -0
- package/skills/lit-code/references/python/testing.md +50 -0
- package/skills/lit-code/references/python/tooling.md +38 -0
- package/skills/lit-code/references/python/type-patterns.md +46 -0
- package/skills/lit-code/references/rust/README.md +12 -0
- package/skills/lit-code/references/rust/concurrency.md +45 -0
- package/skills/lit-code/references/rust/error-handling.md +43 -0
- package/skills/lit-code/references/rust/tooling.md +36 -0
- package/skills/lit-code/references/rust/type-patterns.md +46 -0
- package/skills/lit-code/references/rust/unsafe.md +47 -0
- package/skills/lit-code/references/typescript/README.md +11 -0
- package/skills/lit-code/references/typescript/error-handling.md +56 -0
- package/skills/lit-code/references/typescript/testing.md +42 -0
- package/skills/lit-code/references/typescript/tsconfig-strict.md +40 -0
- package/skills/lit-code/references/typescript/type-patterns.md +60 -0
- package/skills/lit-commit/SKILL.md +222 -0
- package/skills/lit-comprehend/SKILL.md +278 -0
- package/skills/lit-comprehend/assets/explainer-scaffold.html +104 -0
- package/skills/lit-comprehend/references/artifact-template.md +114 -0
- package/skills/lit-comprehend/references/micro-worlds.md +115 -0
- package/skills/lit-comprehend/scripts/verify-explainer.ts +339 -0
- package/skills/lit-crucible/SKILL.md +212 -0
- package/skills/lit-fetch/SKILL.md +242 -0
- package/skills/lit-handoff/SKILL.md +199 -0
- package/skills/lit-init/SKILL.md +214 -0
- package/skills/lit-korean/SKILL.md +231 -0
- package/skills/lit-plan/SKILL.md +351 -0
- package/skills/lit-plan/scripts/scaffold-plan.mjs +275 -0
- package/skills/lit-recap/SKILL.md +233 -0
- package/skills/lit-scientific-visualization/SKILL.md +175 -0
- package/skills/litresearch/SKILL.md +436 -0
- package/skills/litwork/SKILL.md +227 -0
- package/skills/lsp/SKILL.md +186 -0
- package/skills/lsp-setup/SKILL.md +187 -0
- package/skills/lsp-setup/references/README.md +40 -0
- package/skills/lsp-setup/references/bash.md +31 -0
- package/skills/lsp-setup/references/c-cpp.md +43 -0
- package/skills/lsp-setup/references/csharp.md +29 -0
- package/skills/lsp-setup/references/dart.md +25 -0
- package/skills/lsp-setup/references/elixir.md +30 -0
- package/skills/lsp-setup/references/go.md +33 -0
- package/skills/lsp-setup/references/haskell.md +27 -0
- package/skills/lsp-setup/references/java.md +28 -0
- package/skills/lsp-setup/references/julia.md +28 -0
- package/skills/lsp-setup/references/kotlin.md +25 -0
- package/skills/lsp-setup/references/lua.md +26 -0
- package/skills/lsp-setup/references/php.md +30 -0
- package/skills/lsp-setup/references/python.md +33 -0
- package/skills/lsp-setup/references/ruby.md +34 -0
- package/skills/lsp-setup/references/rust.md +34 -0
- package/skills/lsp-setup/references/swift.md +26 -0
- package/skills/lsp-setup/references/terraform.md +26 -0
- package/skills/lsp-setup/references/typescript.md +35 -0
- package/skills/lsp-setup/references/yaml.md +27 -0
- package/skills/lsp-setup/references/zig.md +28 -0
- package/skills/managed-skill-manifest.json +1386 -0
- package/skills/native-goal-verdict/SKILL.md +216 -0
- package/skills/refactor/SKILL.md +221 -0
- package/skills/reference-benchmark-claims/SKILL.md +204 -0
- package/skills/release-guardrails/SKILL.md +245 -0
- package/skills/review-work/SKILL.md +301 -0
- package/skills/rules/SKILL.md +196 -0
- package/skills/search-workflow-ideas/SKILL.md +223 -0
- package/skills/skill-observer/SKILL.md +148 -0
- package/skills/skill-observer/references/review-contract.md +95 -0
- package/skills/skill-rename-aliases.json +10 -0
- package/skills/start-work/SKILL.md +334 -0
- package/skills/structural-search/SKILL.md +234 -0
- package/skills/tool-guards/SKILL.md +223 -0
- package/skills/visual-qa/SKILL.md +61 -0
- package/skills/visual-qa/references/capture-playbook.md +105 -0
- package/skills/visual-qa/references/complete-contract.md +411 -0
- package/skills/visual-qa/schemas/evidence-manifest-v1alpha1.json +201 -0
- package/skills/visual-qa/schemas/evidence-manifest-v1beta1.json +141 -0
- package/skills/visual-qa/schemas/review-receipt-v1alpha1.json +113 -0
- package/skills/visual-qa/scripts/artifact.mjs +123 -0
- package/skills/visual-qa/scripts/bounded-json.mjs +47 -0
- package/skills/visual-qa/scripts/canonical-json.mjs +28 -0
- package/skills/visual-qa/scripts/capabilities.mjs +66 -0
- package/skills/visual-qa/scripts/contract-text.mjs +15 -0
- package/skills/visual-qa/scripts/design-contract.mjs +611 -0
- package/skills/visual-qa/scripts/evidence-evaluate.mjs +521 -0
- package/skills/visual-qa/scripts/evidence.mjs +271 -0
- package/skills/visual-qa/scripts/png-decode.mjs +146 -0
- package/skills/visual-qa/scripts/png.mjs +154 -0
- package/skills/visual-qa/scripts/review.mjs +188 -0
- package/skills/visual-qa/scripts/stdin-json.mjs +16 -0
- package/skills/visual-qa/scripts/strict-json.mjs +102 -0
- package/skills/visual-qa/scripts/tui.mjs +149 -0
- package/skills/visual-qa/scripts/visual-qa.mjs +90 -0
- package/skills/wikify/LICENSE +21 -0
- package/skills/wikify/PROVENANCE.md +18 -0
- package/skills/wikify/SKILL.md +176 -0
- package/skills/wikify/assets/home-template.md +44 -0
- package/skills/wikify/assets/maintenance-report-template.md +46 -0
- package/skills/wikify/assets/paper-source-note-template.md +137 -0
- package/skills/wikify/assets/source-note-template.md +45 -0
- package/skills/wikify/assets/wiki-rules-template.md +117 -0
- package/skills/wikify/modes/ingest.md +57 -0
- package/skills/wikify/modes/init.md +44 -0
- package/skills/wikify/modes/lint.md +60 -0
- package/skills/wikify/modes/query.md +38 -0
- package/skills/wikify/modes/save.md +45 -0
- package/skills/wikify/references-upstream-contract.md +758 -0
- package/skills/workflow-loop/SKILL.md +387 -0
- package/tools/check-pack-payload.mjs +410 -0
- package/tools/check-payload-substance.mjs +738 -0
- package/tools/check-version-lockstep.mjs +238 -0
- package/tools/gen-canonical-frontend-manifest.mjs +66 -0
- package/tools/gen-managed-skill-manifest.mjs +135 -0
- package/tools/harness-speed-local.mjs +261 -0
- package/tools/payload-reference-exemptions.json +49 -0
- package/tools/payload-substance-allowlist.json +46 -0
- package/tools/payload-substance-parity.json +571 -0
- package/tools/qa-real-surface-fixtures.mjs +428 -0
- package/tools/qa-real-surface-harness.mjs +173 -0
- package/tools/run-behavior-replacement-probes.mjs +296 -0
- package/tools/run-build.mjs +101 -0
- package/tools/run-harness-speed-local.mjs +52 -0
- package/tools/run-harness-speed-v2.mjs +36 -0
- package/tools/run-installed-resource-tamper-probe.mjs +62 -0
- package/tools/run-negative-gate-matrix.mjs +516 -0
- package/tools/run-rules-glob-differential.mjs +232 -0
- package/tools/run-typecheck.mjs +25 -0
- package/tools/run-uiux-visual-qa-scenarios.mjs +234 -0
- package/tools/run-wikify-surface-probe.mjs +170 -0
- package/tools/scan-legacy-tokens.mjs +431 -0
- package/tools/version-manifests.json +60 -0
- package/tsconfig.build.json +12 -0
- package/tsconfig.json +13 -0
- package/vendor/NOTICE.md +21 -0
- package/vendor/handoff/SKILL.md +199 -0
- package/vendor/handoff/evals/evals.json +154 -0
- package/vendor/handoff/examples/HANDOFF-example-generic-auth-refactor.md +97 -0
- package/vendor/handoff/templates/HANDOFF.md +121 -0
- package/vendor/licenses/022_handoff-MIT.txt +21 -0
- package/vendor/licenses/045_scientific-visualization-MIT.txt +21 -0
- package/vendor/provenance/022_handoff.md +19 -0
- package/vendor/provenance/045_scientific-visualization.md +36 -0
- package/vendor/scientific-visualization/SKILL.md +283 -0
- package/vendor/scientific-visualization/assets/color_palettes.py +197 -0
- package/vendor/scientific-visualization/assets/nature.mplstyle +75 -0
- package/vendor/scientific-visualization/assets/presentation.mplstyle +74 -0
- package/vendor/scientific-visualization/assets/publication.mplstyle +78 -0
- package/vendor/scientific-visualization/evals/evals.json +158 -0
- package/vendor/scientific-visualization/references/color_palettes.md +380 -0
- package/vendor/scientific-visualization/references/journal_requirements.md +359 -0
- package/vendor/scientific-visualization/references/matplotlib_examples.md +608 -0
- package/vendor/scientific-visualization/references/mdanalysis_martini_visualization.md +85 -0
- package/vendor/scientific-visualization/references/publication_guidelines.md +217 -0
- package/vendor/scientific-visualization/references/seaborn_for_publications.md +293 -0
- package/vendor/scientific-visualization/scripts/figure_export.py +238 -0
- package/vendor/scientific-visualization/scripts/style_presets.py +467 -0
- package/vendor/scientific-visualization/tests/test_figure_export.py +51 -0
- package/vendor/scientific-visualization/tests/test_style_presets.py +114 -0
package/dist/ledger.js
ADDED
|
@@ -0,0 +1,1179 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { createRuntimePaths } from "./state.js";
|
|
5
|
+
export class LedgerParseError extends Error {
|
|
6
|
+
constructor(message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "LedgerParseError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export class LedgerIoError extends Error {
|
|
12
|
+
constructor(message) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = "LedgerIoError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
const ledgerTempPrefix = "ledger.jsonl.tmp-";
|
|
18
|
+
const goalsTempPrefix = "goals.json.tmp-";
|
|
19
|
+
function hasErrorCode(error, code) {
|
|
20
|
+
return error instanceof Error && "code" in error && error.code === code;
|
|
21
|
+
}
|
|
22
|
+
function isRecord(value) {
|
|
23
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
24
|
+
}
|
|
25
|
+
function isJsonValue(value) {
|
|
26
|
+
if (value === null)
|
|
27
|
+
return true;
|
|
28
|
+
const kind = typeof value;
|
|
29
|
+
if (kind === "string" || kind === "boolean")
|
|
30
|
+
return true;
|
|
31
|
+
if (kind === "number")
|
|
32
|
+
return Number.isFinite(value);
|
|
33
|
+
if (Array.isArray(value))
|
|
34
|
+
return value.every(isJsonValue);
|
|
35
|
+
if (!isRecord(value))
|
|
36
|
+
return false;
|
|
37
|
+
return Object.values(value).every(isJsonValue);
|
|
38
|
+
}
|
|
39
|
+
function parseLedgerEvent(value) {
|
|
40
|
+
if (!isRecord(value)) {
|
|
41
|
+
throw new LedgerParseError("Ledger event must be a JSON object.");
|
|
42
|
+
}
|
|
43
|
+
const copy = {};
|
|
44
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
45
|
+
if (!isJsonValue(entry)) {
|
|
46
|
+
throw new LedgerParseError(`Ledger event field ${key} is not JSON-serializable.`);
|
|
47
|
+
}
|
|
48
|
+
copy[key] = entry;
|
|
49
|
+
}
|
|
50
|
+
const eventType = copy["type"];
|
|
51
|
+
if (typeof eventType !== "string" || eventType.trim() === "") {
|
|
52
|
+
throw new LedgerParseError("Ledger event type must be a non-empty string.");
|
|
53
|
+
}
|
|
54
|
+
const timestamp = copy["timestamp"];
|
|
55
|
+
if (timestamp !== undefined && typeof timestamp !== "string") {
|
|
56
|
+
throw new LedgerParseError("Ledger event timestamp must be a string when present.");
|
|
57
|
+
}
|
|
58
|
+
return Object.freeze({ ...copy, type: eventType });
|
|
59
|
+
}
|
|
60
|
+
async function readUtf8IfPresent(filePath) {
|
|
61
|
+
try {
|
|
62
|
+
return await fs.readFile(filePath, "utf8");
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
if (hasErrorCode(error, "ENOENT"))
|
|
66
|
+
return "";
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async function removeIfPresent(filePath) {
|
|
71
|
+
try {
|
|
72
|
+
await fs.rm(filePath, { force: true });
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
if (hasErrorCode(error, "ENOENT"))
|
|
76
|
+
return;
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async function ensureLedgerFile(paths) {
|
|
81
|
+
await fs.mkdir(paths.litLoopDir, { recursive: true });
|
|
82
|
+
try {
|
|
83
|
+
await fs.writeFile(paths.ledgerFile, "", { flag: "wx" });
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
if (hasErrorCode(error, "EEXIST"))
|
|
87
|
+
return;
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function parseLedgerLine(line, lineNumber) {
|
|
92
|
+
let parsed;
|
|
93
|
+
try {
|
|
94
|
+
parsed = JSON.parse(line);
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
const reason = error instanceof Error ? error.message : "invalid JSON";
|
|
98
|
+
throw new LedgerParseError(`Malformed ledger JSON at line ${lineNumber}: ${reason}`);
|
|
99
|
+
}
|
|
100
|
+
return parseLedgerEvent(parsed);
|
|
101
|
+
}
|
|
102
|
+
export async function initializeLitGoal(projectRoot) {
|
|
103
|
+
const paths = createRuntimePaths(projectRoot);
|
|
104
|
+
await fs.mkdir(paths.litLoopDir, { recursive: true });
|
|
105
|
+
await ensureLedgerFile(paths);
|
|
106
|
+
return { paths };
|
|
107
|
+
}
|
|
108
|
+
export async function recoverLedgerTemps(projectRoot) {
|
|
109
|
+
const paths = createRuntimePaths(projectRoot);
|
|
110
|
+
let entries;
|
|
111
|
+
try {
|
|
112
|
+
entries = await fs.readdir(paths.litLoopDir);
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
if (hasErrorCode(error, "ENOENT"))
|
|
116
|
+
return { paths, removedTempFiles: [] };
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
const removedTempFiles = [];
|
|
120
|
+
for (const entry of entries) {
|
|
121
|
+
if (!entry.startsWith(ledgerTempPrefix) && !entry.startsWith(goalsTempPrefix))
|
|
122
|
+
continue;
|
|
123
|
+
await removeIfPresent(path.join(paths.litLoopDir, entry));
|
|
124
|
+
removedTempFiles.push(entry);
|
|
125
|
+
}
|
|
126
|
+
return { paths, removedTempFiles: Object.freeze(removedTempFiles) };
|
|
127
|
+
}
|
|
128
|
+
export async function appendLedgerEvent(projectRoot, event) {
|
|
129
|
+
const parsed = parseLedgerEvent(event);
|
|
130
|
+
const initialized = await initializeLitGoal(projectRoot);
|
|
131
|
+
await fs.appendFile(initialized.paths.ledgerFile, `${JSON.stringify(parsed)}\n`, "utf8");
|
|
132
|
+
return { paths: initialized.paths, event: parsed };
|
|
133
|
+
}
|
|
134
|
+
export async function readLedgerEvents(projectRoot) {
|
|
135
|
+
const paths = createRuntimePaths(projectRoot);
|
|
136
|
+
const raw = await readUtf8IfPresent(paths.ledgerFile);
|
|
137
|
+
const lines = raw.split("\n").filter((line) => line.trim() !== "");
|
|
138
|
+
return Object.freeze(lines.map((line, index) => parseLedgerLine(line, index + 1)));
|
|
139
|
+
}
|
|
140
|
+
export async function readLedgerEventsLenient(projectRoot) {
|
|
141
|
+
const paths = createRuntimePaths(projectRoot);
|
|
142
|
+
const raw = await readUtf8IfPresent(paths.ledgerFile);
|
|
143
|
+
const lines = raw.split("\n").filter((line) => line.trim() !== "");
|
|
144
|
+
const events = [];
|
|
145
|
+
const skipped = [];
|
|
146
|
+
for (let i = 0; i < lines.length; i++) {
|
|
147
|
+
try {
|
|
148
|
+
events.push(parseLedgerLine(lines[i], i + 1));
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
skipped.push({
|
|
152
|
+
line: i + 1,
|
|
153
|
+
error: error instanceof Error ? error.message : String(error)
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
events: Object.freeze(events),
|
|
159
|
+
skipped: Object.freeze(skipped),
|
|
160
|
+
filePath: paths.ledgerFile
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
export function createLitGoalOperations(projectRoot) {
|
|
164
|
+
return Object.freeze({
|
|
165
|
+
init: () => initializeLitGoal(projectRoot),
|
|
166
|
+
append: (event) => appendLedgerEvent(projectRoot, event),
|
|
167
|
+
read: () => readLedgerEvents(projectRoot),
|
|
168
|
+
readLenient: () => readLedgerEventsLenient(projectRoot),
|
|
169
|
+
recover: () => recoverLedgerTemps(projectRoot)
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
// --- Evidence ledger -------------------------------------------------------
|
|
173
|
+
//
|
|
174
|
+
// The JSONL stream above is the append-only audit trail: it records that something happened.
|
|
175
|
+
// It cannot answer "is this goal done", because it has no criteria, no evidence kinds, and no
|
|
176
|
+
// pass/fail. The evidence ledger below adds that layer on top of the same directory: goals.json
|
|
177
|
+
// holds the machine state, brief.md holds the human-readable rendering, and every mutation still
|
|
178
|
+
// appends one audit line so the trail and the state can never silently disagree.
|
|
179
|
+
export const criterionStatuses = ["pending", "in_progress", "blocked", "pass", "fail"];
|
|
180
|
+
// review_blocked and needs_user_decision exist so a goal stopped in review is distinguishable from
|
|
181
|
+
// an active one by status alone, without reading the blocker list.
|
|
182
|
+
export const goalStatuses = ["active", "blocked", "review_blocked", "needs_user_decision", "complete"];
|
|
183
|
+
export const evidenceKinds = ["red", "green", "scenario", "cleanup", "note"];
|
|
184
|
+
// Steering can redirect or extend a goal. It can never weaken the completion gate.
|
|
185
|
+
export const steeringKinds = ["redirect", "add_criterion", "narrow_scope", "reprioritize", "annotate"];
|
|
186
|
+
export const evidenceLedgerVersion = 1;
|
|
187
|
+
export class EvidenceLedgerError extends Error {
|
|
188
|
+
code;
|
|
189
|
+
constructor(code, message) {
|
|
190
|
+
super(message);
|
|
191
|
+
this.name = "EvidenceLedgerError";
|
|
192
|
+
this.code = code;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// A steering directive may change what the goal is. It may not change what counts as done.
|
|
196
|
+
const steeringWeakeningPatterns = [
|
|
197
|
+
/\bskip\s+(?:the\s+)?(?:test|tests|qa|review|gate|criteria|criterion|evidence)\b/iu,
|
|
198
|
+
/\b(?:no|without)\s+(?:need\s+for\s+)?(?:test|tests|qa|review|evidence|proof)\b/iu,
|
|
199
|
+
/\bdon'?t\s+(?:bother\s+)?(?:run|write|do)?\s*(?:the\s+)?(?:test|tests|qa|review)\b/iu,
|
|
200
|
+
/\bauto[-\s]?complete\b/iu,
|
|
201
|
+
/\bforce[-\s]complete\b/iu,
|
|
202
|
+
/\bmark\s+(?:it\s+)?(?:as\s+)?(?:done|complete|passing|green)\b/iu,
|
|
203
|
+
/\bignore\s+(?:the\s+)?(?:gate|failure|failures|blocker|blockers)\b/iu,
|
|
204
|
+
/\bbypass\s+(?:the\s+)?(?:gate|review|qa)\b/iu
|
|
205
|
+
];
|
|
206
|
+
let goalsTempCounter = 0;
|
|
207
|
+
let evidenceTransactionTempCounter = 0;
|
|
208
|
+
const evidenceLockDirectoryName = ".evidence-ledger.lock";
|
|
209
|
+
const evidenceTransitionLockDirectoryName = ".evidence-ledger-transition.lock";
|
|
210
|
+
const evidenceLockOwnerFileName = "owner.json";
|
|
211
|
+
const evidenceTransitionRecoveryFileName = "recovery.json";
|
|
212
|
+
const evidenceTransactionFileName = "evidence-transaction.json";
|
|
213
|
+
const evidenceLockTimeoutMs = 2_000;
|
|
214
|
+
const evidenceStaleLockMs = 30_000;
|
|
215
|
+
const evidenceLockOwnerMaxBytes = 4_096;
|
|
216
|
+
function nowIso(now) {
|
|
217
|
+
return now ?? new Date().toISOString();
|
|
218
|
+
}
|
|
219
|
+
function nextId(prefix, existing) {
|
|
220
|
+
let highest = 0;
|
|
221
|
+
for (const id of existing) {
|
|
222
|
+
const parsed = Number(id.startsWith(prefix) ? id.slice(prefix.length) : Number.NaN);
|
|
223
|
+
if (Number.isInteger(parsed) && parsed > highest)
|
|
224
|
+
highest = parsed;
|
|
225
|
+
}
|
|
226
|
+
return `${prefix}${highest + 1}`;
|
|
227
|
+
}
|
|
228
|
+
function requireOneOf(value, allowed, label) {
|
|
229
|
+
const match = allowed.find((candidate) => candidate === value);
|
|
230
|
+
if (match === undefined) {
|
|
231
|
+
throw new EvidenceLedgerError("invalid_value", `invalid ${label} '${value}' (valid: ${allowed.join(", ")})`);
|
|
232
|
+
}
|
|
233
|
+
return match;
|
|
234
|
+
}
|
|
235
|
+
function emptyEvidenceLedgerState(at) {
|
|
236
|
+
return { version: evidenceLedgerVersion, createdAt: at, updatedAt: at, activeGoalId: "", goals: [] };
|
|
237
|
+
}
|
|
238
|
+
function asString(value, fallback = "") {
|
|
239
|
+
return typeof value === "string" ? value : fallback;
|
|
240
|
+
}
|
|
241
|
+
function parseEvidenceEntry(value) {
|
|
242
|
+
const record = isRecord(value) ? value : {};
|
|
243
|
+
return {
|
|
244
|
+
kind: requireOneOf(asString(record["kind"], "note"), evidenceKinds, "evidence kind"),
|
|
245
|
+
ref: asString(record["ref"]),
|
|
246
|
+
detail: asString(record["detail"]),
|
|
247
|
+
at: asString(record["at"])
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function parseCriterion(value) {
|
|
251
|
+
const record = isRecord(value) ? value : {};
|
|
252
|
+
const evidence = Array.isArray(record["evidence"])
|
|
253
|
+
? record["evidence"].map(parseEvidenceEntry)
|
|
254
|
+
: typeof record["evidence"] === "string" && record["evidence"].trim() !== ""
|
|
255
|
+
? [{ kind: "note", ref: record["evidence"], detail: "", at: "" }]
|
|
256
|
+
: [];
|
|
257
|
+
const rawStatus = asString(record["status"], "pending");
|
|
258
|
+
return {
|
|
259
|
+
id: asString(record["id"]),
|
|
260
|
+
scenario: asString(record["scenario"]),
|
|
261
|
+
qaChannel: asString(record["qaChannel"]),
|
|
262
|
+
testRef: asString(record["testRef"]),
|
|
263
|
+
status: requireOneOf(rawStatus === "passed" ? "pass" : rawStatus === "failed" ? "fail" : rawStatus, criterionStatuses, "criterion status"),
|
|
264
|
+
evidence
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
function parseGoal(value) {
|
|
268
|
+
const record = isRecord(value) ? value : {};
|
|
269
|
+
const criteria = Array.isArray(record["criteria"]) ? record["criteria"] : [];
|
|
270
|
+
const checkpoints = Array.isArray(record["checkpoints"]) ? record["checkpoints"] : [];
|
|
271
|
+
const steering = Array.isArray(record["steering"]) ? record["steering"] : [];
|
|
272
|
+
const blockers = Array.isArray(record["reviewBlockers"]) ? record["reviewBlockers"] : [];
|
|
273
|
+
const rawStatus = asString(record["status"], "active");
|
|
274
|
+
return {
|
|
275
|
+
id: asString(record["id"]),
|
|
276
|
+
sessionId: asString(record["sessionId"]),
|
|
277
|
+
objective: asString(record["objective"]),
|
|
278
|
+
title: asString(record["title"]),
|
|
279
|
+
status: requireOneOf(rawStatus === "completed" ? "complete" : rawStatus, goalStatuses, "goal status"),
|
|
280
|
+
createdAt: asString(record["createdAt"]),
|
|
281
|
+
updatedAt: asString(record["updatedAt"]),
|
|
282
|
+
criteria: criteria.map(parseCriterion),
|
|
283
|
+
checkpoints: checkpoints.map((entry) => {
|
|
284
|
+
const cp = isRecord(entry) ? entry : {};
|
|
285
|
+
return {
|
|
286
|
+
id: asString(cp["id"]),
|
|
287
|
+
at: asString(cp["at"]),
|
|
288
|
+
summary: asString(cp["summary"]),
|
|
289
|
+
activeCriterion: asString(cp["activeCriterion"])
|
|
290
|
+
};
|
|
291
|
+
}),
|
|
292
|
+
steering: steering.map((entry) => {
|
|
293
|
+
const st = isRecord(entry) ? entry : {};
|
|
294
|
+
return {
|
|
295
|
+
id: asString(st["id"]),
|
|
296
|
+
at: asString(st["at"]),
|
|
297
|
+
directive: asString(st["directive"]),
|
|
298
|
+
kind: requireOneOf(asString(st["kind"], "redirect"), steeringKinds, "steering kind"),
|
|
299
|
+
...(asString(st["criterionId"]) === "" ? {} : { criterionId: asString(st["criterionId"]) })
|
|
300
|
+
};
|
|
301
|
+
}),
|
|
302
|
+
reviewBlockers: blockers.map((entry) => {
|
|
303
|
+
const bl = isRecord(entry) ? entry : {};
|
|
304
|
+
return {
|
|
305
|
+
id: asString(bl["id"]),
|
|
306
|
+
detail: asString(bl["detail"]),
|
|
307
|
+
resolved: bl["resolved"] === true,
|
|
308
|
+
needsUserDecision: bl["needsUserDecision"] === true
|
|
309
|
+
};
|
|
310
|
+
})
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
function parseEvidenceLedgerState(value) {
|
|
314
|
+
if (!isRecord(value))
|
|
315
|
+
throw new LedgerParseError("goals.json must contain a JSON object.");
|
|
316
|
+
const goals = Array.isArray(value["goals"]) ? value["goals"] : [];
|
|
317
|
+
return {
|
|
318
|
+
version: typeof value["version"] === "number" ? value["version"] : evidenceLedgerVersion,
|
|
319
|
+
createdAt: asString(value["createdAt"]),
|
|
320
|
+
updatedAt: asString(value["updatedAt"]),
|
|
321
|
+
activeGoalId: asString(value["activeGoalId"]),
|
|
322
|
+
goals: goals.map(parseGoal)
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
export async function readEvidenceLedger(projectRoot) {
|
|
326
|
+
const paths = createRuntimePaths(projectRoot);
|
|
327
|
+
const raw = await readUtf8IfPresent(paths.goalsFile);
|
|
328
|
+
if (raw.trim() === "")
|
|
329
|
+
return emptyEvidenceLedgerState("");
|
|
330
|
+
let parsed;
|
|
331
|
+
try {
|
|
332
|
+
parsed = JSON.parse(raw);
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
const reason = error instanceof Error ? error.message : "invalid JSON";
|
|
336
|
+
throw new LedgerParseError(`Malformed goals.json: ${reason}`);
|
|
337
|
+
}
|
|
338
|
+
return parseEvidenceLedgerState(parsed);
|
|
339
|
+
}
|
|
340
|
+
export function renderEvidenceBrief(state) {
|
|
341
|
+
const lines = ["# LitOpenCode brief", ""];
|
|
342
|
+
lines.push(`Updated: ${state.updatedAt || "(never)"}`);
|
|
343
|
+
lines.push(`Active goal: ${state.activeGoalId || "(none)"}`);
|
|
344
|
+
lines.push("");
|
|
345
|
+
if (state.goals.length === 0) {
|
|
346
|
+
lines.push("No goals recorded yet. Run `litopencode create-goals` to open one.");
|
|
347
|
+
lines.push("");
|
|
348
|
+
return lines.join("\n");
|
|
349
|
+
}
|
|
350
|
+
for (const goal of state.goals) {
|
|
351
|
+
lines.push(`## ${goal.id} — ${goal.title || goal.objective}`);
|
|
352
|
+
lines.push("");
|
|
353
|
+
lines.push(`- status: **${goal.status}**`);
|
|
354
|
+
lines.push(`- session: ${goal.sessionId}`);
|
|
355
|
+
lines.push(`- objective: ${goal.objective}`);
|
|
356
|
+
lines.push("");
|
|
357
|
+
lines.push("### Success criteria");
|
|
358
|
+
lines.push("");
|
|
359
|
+
if (goal.criteria.length === 0)
|
|
360
|
+
lines.push("- (none recorded)");
|
|
361
|
+
for (const criterion of goal.criteria) {
|
|
362
|
+
lines.push(`- [${criterion.status === "pass" ? "x" : " "}] ${criterion.id} \`${criterion.status}\` — ${criterion.scenario}`);
|
|
363
|
+
for (const entry of criterion.evidence) {
|
|
364
|
+
lines.push(` - ${entry.kind}: ${entry.ref}${entry.detail === "" ? "" : ` — ${entry.detail}`}`);
|
|
365
|
+
}
|
|
366
|
+
if (criterion.evidence.length === 0)
|
|
367
|
+
lines.push(" - (no evidence captured)");
|
|
368
|
+
}
|
|
369
|
+
lines.push("");
|
|
370
|
+
if (goal.checkpoints.length > 0) {
|
|
371
|
+
lines.push("### Checkpoints");
|
|
372
|
+
lines.push("");
|
|
373
|
+
for (const cp of goal.checkpoints) {
|
|
374
|
+
lines.push(`- ${cp.id} ${cp.at}${cp.activeCriterion === "" ? "" : ` [${cp.activeCriterion}]`} — ${cp.summary}`);
|
|
375
|
+
}
|
|
376
|
+
lines.push("");
|
|
377
|
+
}
|
|
378
|
+
if (goal.steering.length > 0) {
|
|
379
|
+
lines.push("### Steering");
|
|
380
|
+
lines.push("");
|
|
381
|
+
for (const st of goal.steering)
|
|
382
|
+
lines.push(`- ${st.id} (${st.kind}) — ${st.directive}`);
|
|
383
|
+
lines.push("");
|
|
384
|
+
}
|
|
385
|
+
if (goal.reviewBlockers.length > 0) {
|
|
386
|
+
lines.push("### Review blockers");
|
|
387
|
+
lines.push("");
|
|
388
|
+
for (const bl of goal.reviewBlockers) {
|
|
389
|
+
const flags = [bl.resolved ? "resolved" : "open", ...(bl.needsUserDecision ? ["needs user decision"] : [])];
|
|
390
|
+
lines.push(`- ${bl.id} (${flags.join(", ")}) — ${bl.detail}`);
|
|
391
|
+
}
|
|
392
|
+
lines.push("");
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return lines.join("\n");
|
|
396
|
+
}
|
|
397
|
+
async function writeEvidenceLedger(projectRoot, state) {
|
|
398
|
+
const paths = createRuntimePaths(projectRoot);
|
|
399
|
+
await fs.mkdir(paths.evidenceDir, { recursive: true });
|
|
400
|
+
goalsTempCounter += 1;
|
|
401
|
+
const tempFile = path.join(paths.litLoopDir, `${goalsTempPrefix}${process.pid}-${goalsTempCounter}`);
|
|
402
|
+
try {
|
|
403
|
+
await fs.writeFile(tempFile, `${JSON.stringify(state, null, 2)}\n`, "utf8");
|
|
404
|
+
await fs.rename(tempFile, paths.goalsFile);
|
|
405
|
+
}
|
|
406
|
+
catch (error) {
|
|
407
|
+
await removeIfPresent(tempFile);
|
|
408
|
+
throw new LedgerIoError(`Failed to write goals.json: ${error instanceof Error ? error.message : String(error)}`);
|
|
409
|
+
}
|
|
410
|
+
await fs.writeFile(paths.briefFile, renderEvidenceBrief(state), "utf8");
|
|
411
|
+
return state;
|
|
412
|
+
}
|
|
413
|
+
function evidenceMutationPaths(projectRoot) {
|
|
414
|
+
const { litLoopDir } = createRuntimePaths(projectRoot);
|
|
415
|
+
const lockDir = path.join(litLoopDir, evidenceLockDirectoryName);
|
|
416
|
+
const transitionLockDir = path.join(litLoopDir, evidenceTransitionLockDirectoryName);
|
|
417
|
+
return {
|
|
418
|
+
lockDir,
|
|
419
|
+
ownerFile: path.join(lockDir, evidenceLockOwnerFileName),
|
|
420
|
+
transactionFile: path.join(litLoopDir, evidenceTransactionFileName),
|
|
421
|
+
transitionLockDir,
|
|
422
|
+
transitionOwnerFile: path.join(transitionLockDir, evidenceLockOwnerFileName),
|
|
423
|
+
transitionRecoveryFile: path.join(transitionLockDir, evidenceTransitionRecoveryFileName)
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
function processIsAlive(pid) {
|
|
427
|
+
try {
|
|
428
|
+
process.kill(pid, 0);
|
|
429
|
+
return true;
|
|
430
|
+
}
|
|
431
|
+
catch (error) {
|
|
432
|
+
return !hasErrorCode(error, "ESRCH");
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
async function readEvidenceLockOwner(ownerFile) {
|
|
436
|
+
const stat = await fs.lstat(ownerFile).catch((error) => {
|
|
437
|
+
if (hasErrorCode(error, "ENOENT"))
|
|
438
|
+
return undefined;
|
|
439
|
+
throw error;
|
|
440
|
+
});
|
|
441
|
+
if (stat === undefined)
|
|
442
|
+
return undefined;
|
|
443
|
+
if (stat.isSymbolicLink() || !stat.isFile() || stat.size > evidenceLockOwnerMaxBytes) {
|
|
444
|
+
throw new LedgerIoError("Evidence ledger lock owner metadata is unsafe.");
|
|
445
|
+
}
|
|
446
|
+
const raw = await fs.readFile(ownerFile, "utf8").catch((error) => {
|
|
447
|
+
if (hasErrorCode(error, "ENOENT"))
|
|
448
|
+
return "";
|
|
449
|
+
throw error;
|
|
450
|
+
});
|
|
451
|
+
if (raw === "")
|
|
452
|
+
return undefined;
|
|
453
|
+
if (Buffer.byteLength(raw, "utf8") > evidenceLockOwnerMaxBytes) {
|
|
454
|
+
throw new LedgerIoError("Evidence ledger lock owner metadata is unsafe.");
|
|
455
|
+
}
|
|
456
|
+
try {
|
|
457
|
+
const parsed = JSON.parse(raw);
|
|
458
|
+
if (!isRecord(parsed) || typeof parsed["token"] !== "string" || !Number.isSafeInteger(parsed["pid"]))
|
|
459
|
+
return undefined;
|
|
460
|
+
return { token: parsed["token"], pid: parsed["pid"] };
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
return undefined;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
async function removeOwnedEvidenceLock(lockDir, ownerFile, token) {
|
|
467
|
+
const owner = await readEvidenceLockOwner(ownerFile);
|
|
468
|
+
if (owner?.token !== token)
|
|
469
|
+
return;
|
|
470
|
+
const retired = `${lockDir}.released-${randomUUID()}`;
|
|
471
|
+
try {
|
|
472
|
+
await fs.rename(lockDir, retired);
|
|
473
|
+
}
|
|
474
|
+
catch (error) {
|
|
475
|
+
if (hasErrorCode(error, "ENOENT"))
|
|
476
|
+
return;
|
|
477
|
+
throw error;
|
|
478
|
+
}
|
|
479
|
+
await fs.rm(retired, { recursive: true, force: true });
|
|
480
|
+
}
|
|
481
|
+
async function tryCreateEvidenceLock(lockDir, ownerFile, token) {
|
|
482
|
+
const candidateLockDir = path.join(path.dirname(lockDir), `.${path.basename(lockDir)}.candidate-${randomUUID()}`);
|
|
483
|
+
const candidateOwnerFile = path.join(candidateLockDir, path.basename(ownerFile));
|
|
484
|
+
try {
|
|
485
|
+
await fs.mkdir(candidateLockDir, { mode: 0o700 });
|
|
486
|
+
await fs.writeFile(candidateOwnerFile, `${JSON.stringify({ token, pid: process.pid })}\n`, {
|
|
487
|
+
encoding: "utf8",
|
|
488
|
+
flag: "wx",
|
|
489
|
+
mode: 0o600
|
|
490
|
+
});
|
|
491
|
+
try {
|
|
492
|
+
await fs.rename(candidateLockDir, lockDir);
|
|
493
|
+
return true;
|
|
494
|
+
}
|
|
495
|
+
catch (error) {
|
|
496
|
+
if (hasErrorCode(error, "EEXIST") || hasErrorCode(error, "ENOTEMPTY"))
|
|
497
|
+
return false;
|
|
498
|
+
throw error;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
finally {
|
|
502
|
+
await fs.rm(candidateLockDir, { recursive: true, force: true });
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
async function readTransitionOwnerSnapshot(ownerFile) {
|
|
506
|
+
const stat = await fs.lstat(ownerFile).catch((error) => {
|
|
507
|
+
if (hasErrorCode(error, "ENOENT"))
|
|
508
|
+
return undefined;
|
|
509
|
+
throw error;
|
|
510
|
+
});
|
|
511
|
+
if (stat === undefined)
|
|
512
|
+
return { status: "ownerless" };
|
|
513
|
+
const owner = await readEvidenceLockOwner(ownerFile);
|
|
514
|
+
return owner === undefined ? { status: "invalid" } : { status: "valid", ...owner };
|
|
515
|
+
}
|
|
516
|
+
function sameTransitionOwner(left, right) {
|
|
517
|
+
if (left.status !== right.status)
|
|
518
|
+
return false;
|
|
519
|
+
if (left.status !== "valid" || right.status !== "valid")
|
|
520
|
+
return true;
|
|
521
|
+
return left.token === right.token && left.pid === right.pid;
|
|
522
|
+
}
|
|
523
|
+
function transitionOwnerGeneration(owner) {
|
|
524
|
+
return owner.status === "valid" ? `${owner.token}:${owner.pid}` : owner.status;
|
|
525
|
+
}
|
|
526
|
+
function sameEvidenceLockOwner(left, right) {
|
|
527
|
+
return left !== undefined && right !== undefined && left.token === right.token && left.pid === right.pid;
|
|
528
|
+
}
|
|
529
|
+
async function restoreMovedEvidenceRecoveryClaim(movedFile, claimFile) {
|
|
530
|
+
try {
|
|
531
|
+
await fs.link(movedFile, claimFile);
|
|
532
|
+
}
|
|
533
|
+
catch (error) {
|
|
534
|
+
if (hasErrorCode(error, "ENOENT"))
|
|
535
|
+
return;
|
|
536
|
+
if (hasErrorCode(error, "EEXIST")) {
|
|
537
|
+
if ((await readEvidenceLockOwner(claimFile)) === undefined) {
|
|
538
|
+
throw new LedgerIoError("Evidence ledger recovery claim metadata is unsafe.");
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
else {
|
|
542
|
+
throw error;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
await fs.rm(movedFile, { force: true });
|
|
546
|
+
}
|
|
547
|
+
async function tryRetireDeadEvidenceRecoveryClaim(claimFile) {
|
|
548
|
+
const observed = await readEvidenceLockOwner(claimFile);
|
|
549
|
+
if (observed === undefined || processIsAlive(observed.pid))
|
|
550
|
+
return false;
|
|
551
|
+
const current = await readEvidenceLockOwner(claimFile);
|
|
552
|
+
if (!sameEvidenceLockOwner(observed, current) || current === undefined || processIsAlive(current.pid))
|
|
553
|
+
return false;
|
|
554
|
+
const movedFile = `${claimFile}.abandoned-${randomUUID()}`;
|
|
555
|
+
try {
|
|
556
|
+
await fs.rename(claimFile, movedFile);
|
|
557
|
+
}
|
|
558
|
+
catch (error) {
|
|
559
|
+
if (hasErrorCode(error, "ENOENT"))
|
|
560
|
+
return true;
|
|
561
|
+
throw error;
|
|
562
|
+
}
|
|
563
|
+
const moved = await readEvidenceLockOwner(movedFile);
|
|
564
|
+
if (sameEvidenceLockOwner(observed, moved) && moved !== undefined && !processIsAlive(moved.pid)) {
|
|
565
|
+
await fs.rm(movedFile, { force: true });
|
|
566
|
+
return true;
|
|
567
|
+
}
|
|
568
|
+
await restoreMovedEvidenceRecoveryClaim(movedFile, claimFile);
|
|
569
|
+
return false;
|
|
570
|
+
}
|
|
571
|
+
async function removeOwnedEvidenceRecoveryClaim(claimFile, token) {
|
|
572
|
+
const owner = await readEvidenceLockOwner(claimFile);
|
|
573
|
+
if (owner?.token !== token)
|
|
574
|
+
return;
|
|
575
|
+
const movedFile = `${claimFile}.released-${randomUUID()}`;
|
|
576
|
+
try {
|
|
577
|
+
await fs.rename(claimFile, movedFile);
|
|
578
|
+
}
|
|
579
|
+
catch (error) {
|
|
580
|
+
if (hasErrorCode(error, "ENOENT"))
|
|
581
|
+
return;
|
|
582
|
+
throw error;
|
|
583
|
+
}
|
|
584
|
+
const moved = await readEvidenceLockOwner(movedFile);
|
|
585
|
+
if (moved?.token === token) {
|
|
586
|
+
await fs.rm(movedFile, { force: true });
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
await restoreMovedEvidenceRecoveryClaim(movedFile, claimFile);
|
|
590
|
+
}
|
|
591
|
+
async function tryRecoverEvidenceTransitionLock(projectRoot) {
|
|
592
|
+
const { transitionLockDir, transitionOwnerFile, transitionRecoveryFile } = evidenceMutationPaths(projectRoot);
|
|
593
|
+
const claimToken = randomUUID();
|
|
594
|
+
try {
|
|
595
|
+
await fs.writeFile(transitionRecoveryFile, `${JSON.stringify({ token: claimToken, pid: process.pid })}\n`, {
|
|
596
|
+
encoding: "utf8",
|
|
597
|
+
flag: "wx",
|
|
598
|
+
mode: 0o600
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
catch (error) {
|
|
602
|
+
if (hasErrorCode(error, "ENOENT"))
|
|
603
|
+
return true;
|
|
604
|
+
if (hasErrorCode(error, "EEXIST")) {
|
|
605
|
+
return tryRetireDeadEvidenceRecoveryClaim(transitionRecoveryFile);
|
|
606
|
+
}
|
|
607
|
+
throw error;
|
|
608
|
+
}
|
|
609
|
+
let renamed = false;
|
|
610
|
+
try {
|
|
611
|
+
const observed = await readTransitionOwnerSnapshot(transitionOwnerFile);
|
|
612
|
+
if (observed.status === "invalid")
|
|
613
|
+
return false;
|
|
614
|
+
if (observed.status === "valid" && processIsAlive(observed.pid))
|
|
615
|
+
return false;
|
|
616
|
+
const current = await readTransitionOwnerSnapshot(transitionOwnerFile);
|
|
617
|
+
if (!sameTransitionOwner(observed, current))
|
|
618
|
+
return false;
|
|
619
|
+
if (current.status === "valid" && processIsAlive(current.pid))
|
|
620
|
+
return false;
|
|
621
|
+
if ((await readEvidenceLockOwner(transitionRecoveryFile))?.token !== claimToken)
|
|
622
|
+
return false;
|
|
623
|
+
const abandoned = `${transitionLockDir}.abandoned-${randomUUID()}`;
|
|
624
|
+
try {
|
|
625
|
+
await fs.rename(transitionLockDir, abandoned);
|
|
626
|
+
renamed = true;
|
|
627
|
+
await fs.rm(abandoned, { recursive: true, force: true });
|
|
628
|
+
return true;
|
|
629
|
+
}
|
|
630
|
+
catch (error) {
|
|
631
|
+
if (hasErrorCode(error, "ENOENT"))
|
|
632
|
+
return true;
|
|
633
|
+
throw error;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
finally {
|
|
637
|
+
if (!renamed)
|
|
638
|
+
await removeOwnedEvidenceRecoveryClaim(transitionRecoveryFile, claimToken);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
async function removeOwnedEvidenceTransitionLock(lockDir, ownerFile, token) {
|
|
642
|
+
const owner = await readEvidenceLockOwner(ownerFile);
|
|
643
|
+
if (owner?.token !== token)
|
|
644
|
+
return;
|
|
645
|
+
const retired = `${lockDir}.released-${randomUUID()}`;
|
|
646
|
+
try {
|
|
647
|
+
await fs.rename(lockDir, retired);
|
|
648
|
+
}
|
|
649
|
+
catch (error) {
|
|
650
|
+
if (hasErrorCode(error, "ENOENT"))
|
|
651
|
+
return;
|
|
652
|
+
throw error;
|
|
653
|
+
}
|
|
654
|
+
await fs.rm(retired, { recursive: true, force: true });
|
|
655
|
+
}
|
|
656
|
+
async function withEvidenceTransitionLock(projectRoot, operation) {
|
|
657
|
+
const paths = createRuntimePaths(projectRoot);
|
|
658
|
+
const { transitionLockDir, transitionOwnerFile } = evidenceMutationPaths(projectRoot);
|
|
659
|
+
await fs.mkdir(paths.litLoopDir, { recursive: true });
|
|
660
|
+
let lastProgressAt = Date.now();
|
|
661
|
+
let observedGeneration = "";
|
|
662
|
+
const token = randomUUID();
|
|
663
|
+
while (true) {
|
|
664
|
+
if (await tryCreateEvidenceLock(transitionLockDir, transitionOwnerFile, token))
|
|
665
|
+
break;
|
|
666
|
+
const stat = await fs.lstat(transitionLockDir).catch((error) => {
|
|
667
|
+
if (hasErrorCode(error, "ENOENT"))
|
|
668
|
+
return undefined;
|
|
669
|
+
throw error;
|
|
670
|
+
});
|
|
671
|
+
if (stat === undefined)
|
|
672
|
+
continue;
|
|
673
|
+
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
674
|
+
throw new LedgerIoError("Evidence ledger transition lock path is unsafe.");
|
|
675
|
+
const owner = await readTransitionOwnerSnapshot(transitionOwnerFile);
|
|
676
|
+
const generation = `${stat.dev}:${stat.ino}:${transitionOwnerGeneration(owner)}`;
|
|
677
|
+
if (generation !== observedGeneration) {
|
|
678
|
+
observedGeneration = generation;
|
|
679
|
+
lastProgressAt = Date.now();
|
|
680
|
+
}
|
|
681
|
+
if (owner.status === "ownerless" || (owner.status === "valid" && !processIsAlive(owner.pid))) {
|
|
682
|
+
if (await tryRecoverEvidenceTransitionLock(projectRoot))
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
// Never age-evict this mutex: its owner may be paused immediately before replacing a stale lock.
|
|
686
|
+
if (Date.now() - lastProgressAt >= evidenceLockTimeoutMs)
|
|
687
|
+
throw new LedgerIoError("Evidence ledger transition lock timed out.");
|
|
688
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
689
|
+
}
|
|
690
|
+
try {
|
|
691
|
+
return await operation(token);
|
|
692
|
+
}
|
|
693
|
+
finally {
|
|
694
|
+
await removeOwnedEvidenceTransitionLock(transitionLockDir, transitionOwnerFile, token);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
async function inspectEvidenceLock(lockDir, ownerFile) {
|
|
698
|
+
const stat = await fs.lstat(lockDir).catch((error) => {
|
|
699
|
+
if (hasErrorCode(error, "ENOENT"))
|
|
700
|
+
return undefined;
|
|
701
|
+
throw error;
|
|
702
|
+
});
|
|
703
|
+
if (stat === undefined)
|
|
704
|
+
return { needsTransition: true, generation: "absent" };
|
|
705
|
+
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
706
|
+
throw new LedgerIoError("Evidence ledger lock path is unsafe.");
|
|
707
|
+
const owner = await readEvidenceLockOwner(ownerFile);
|
|
708
|
+
const ownerGeneration = owner === undefined ? "ownerless" : `${owner.token}:${owner.pid}`;
|
|
709
|
+
const generation = `${stat.dev}:${stat.ino}:${ownerGeneration}`;
|
|
710
|
+
if (Date.now() - stat.mtimeMs <= evidenceStaleLockMs)
|
|
711
|
+
return { needsTransition: false, generation };
|
|
712
|
+
return { needsTransition: owner === undefined || !processIsAlive(owner.pid), generation };
|
|
713
|
+
}
|
|
714
|
+
async function acquireEvidenceLedgerLock(projectRoot, token) {
|
|
715
|
+
const { lockDir, ownerFile, transitionOwnerFile } = evidenceMutationPaths(projectRoot);
|
|
716
|
+
let lastProgressAt = Date.now();
|
|
717
|
+
let observedGeneration = "";
|
|
718
|
+
while (true) {
|
|
719
|
+
const inspection = await inspectEvidenceLock(lockDir, ownerFile);
|
|
720
|
+
if (inspection.generation !== observedGeneration) {
|
|
721
|
+
observedGeneration = inspection.generation;
|
|
722
|
+
lastProgressAt = Date.now();
|
|
723
|
+
}
|
|
724
|
+
if (!inspection.needsTransition) {
|
|
725
|
+
if (Date.now() - lastProgressAt >= evidenceLockTimeoutMs)
|
|
726
|
+
throw new LedgerIoError("Evidence ledger lock timed out.");
|
|
727
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
const acquired = await withEvidenceTransitionLock(projectRoot, async (transitionToken) => {
|
|
731
|
+
if (await tryCreateEvidenceLock(lockDir, ownerFile, token))
|
|
732
|
+
return true;
|
|
733
|
+
const stat = await fs.lstat(lockDir).catch((error) => {
|
|
734
|
+
if (hasErrorCode(error, "ENOENT"))
|
|
735
|
+
return undefined;
|
|
736
|
+
throw error;
|
|
737
|
+
});
|
|
738
|
+
if (stat === undefined)
|
|
739
|
+
return false;
|
|
740
|
+
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
741
|
+
throw new LedgerIoError("Evidence ledger lock path is unsafe.");
|
|
742
|
+
if (Date.now() - stat.mtimeMs <= evidenceStaleLockMs)
|
|
743
|
+
return false;
|
|
744
|
+
const owner = await readEvidenceLockOwner(ownerFile);
|
|
745
|
+
if (owner !== undefined && processIsAlive(owner.pid))
|
|
746
|
+
return false;
|
|
747
|
+
if ((await readEvidenceLockOwner(transitionOwnerFile))?.token !== transitionToken)
|
|
748
|
+
return false;
|
|
749
|
+
const abandoned = `${lockDir}.abandoned-${randomUUID()}`;
|
|
750
|
+
try {
|
|
751
|
+
await fs.rename(lockDir, abandoned);
|
|
752
|
+
await fs.rm(abandoned, { recursive: true, force: true });
|
|
753
|
+
}
|
|
754
|
+
catch (error) {
|
|
755
|
+
if (!hasErrorCode(error, "ENOENT"))
|
|
756
|
+
throw error;
|
|
757
|
+
return false;
|
|
758
|
+
}
|
|
759
|
+
return tryCreateEvidenceLock(lockDir, ownerFile, token);
|
|
760
|
+
});
|
|
761
|
+
lastProgressAt = Date.now();
|
|
762
|
+
if (acquired)
|
|
763
|
+
return;
|
|
764
|
+
if (Date.now() - lastProgressAt >= evidenceLockTimeoutMs)
|
|
765
|
+
throw new LedgerIoError("Evidence ledger lock timed out.");
|
|
766
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
async function withEvidenceLedgerLock(projectRoot, operation) {
|
|
770
|
+
const { lockDir, ownerFile } = evidenceMutationPaths(projectRoot);
|
|
771
|
+
const token = randomUUID();
|
|
772
|
+
await acquireEvidenceLedgerLock(projectRoot, token);
|
|
773
|
+
try {
|
|
774
|
+
return await operation();
|
|
775
|
+
}
|
|
776
|
+
finally {
|
|
777
|
+
await withEvidenceTransitionLock(projectRoot, async () => removeOwnedEvidenceLock(lockDir, ownerFile, token));
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
async function ledgerContainsMutation(projectRoot, mutationId) {
|
|
781
|
+
const raw = await readUtf8IfPresent(createRuntimePaths(projectRoot).ledgerFile);
|
|
782
|
+
return raw.split("\n").some((line) => {
|
|
783
|
+
if (line.trim() === "")
|
|
784
|
+
return false;
|
|
785
|
+
try {
|
|
786
|
+
const parsed = JSON.parse(line);
|
|
787
|
+
return isRecord(parsed) && parsed["mutationId"] === mutationId;
|
|
788
|
+
}
|
|
789
|
+
catch {
|
|
790
|
+
return false;
|
|
791
|
+
}
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
async function writeEvidenceTransaction(projectRoot, transaction) {
|
|
795
|
+
const { transactionFile } = evidenceMutationPaths(projectRoot);
|
|
796
|
+
evidenceTransactionTempCounter += 1;
|
|
797
|
+
const tempFile = `${transactionFile}.tmp-${process.pid}-${evidenceTransactionTempCounter}`;
|
|
798
|
+
try {
|
|
799
|
+
await fs.writeFile(tempFile, `${JSON.stringify(transaction, null, 2)}\n`, "utf8");
|
|
800
|
+
await fs.rename(tempFile, transactionFile);
|
|
801
|
+
}
|
|
802
|
+
catch (error) {
|
|
803
|
+
await removeIfPresent(tempFile);
|
|
804
|
+
throw new LedgerIoError(`Failed to stage evidence transaction: ${error instanceof Error ? error.message : String(error)}`);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
async function commitEvidenceTransaction(projectRoot, transaction) {
|
|
808
|
+
const { transactionFile } = evidenceMutationPaths(projectRoot);
|
|
809
|
+
await writeEvidenceLedger(projectRoot, transaction.state);
|
|
810
|
+
if (!(await ledgerContainsMutation(projectRoot, transaction.mutationId))) {
|
|
811
|
+
await appendLedgerEvent(projectRoot, { ...transaction.event, mutationId: transaction.mutationId });
|
|
812
|
+
}
|
|
813
|
+
await removeIfPresent(transactionFile);
|
|
814
|
+
}
|
|
815
|
+
async function recoverEvidenceTransaction(projectRoot) {
|
|
816
|
+
const { transactionFile } = evidenceMutationPaths(projectRoot);
|
|
817
|
+
const raw = await readUtf8IfPresent(transactionFile);
|
|
818
|
+
if (raw === "")
|
|
819
|
+
return;
|
|
820
|
+
let parsed;
|
|
821
|
+
try {
|
|
822
|
+
parsed = JSON.parse(raw);
|
|
823
|
+
}
|
|
824
|
+
catch (error) {
|
|
825
|
+
throw new LedgerParseError(`Malformed evidence transaction: ${error instanceof Error ? error.message : String(error)}`);
|
|
826
|
+
}
|
|
827
|
+
if (!isRecord(parsed) || parsed["version"] !== 1 || typeof parsed["mutationId"] !== "string") {
|
|
828
|
+
throw new LedgerParseError("Malformed evidence transaction envelope.");
|
|
829
|
+
}
|
|
830
|
+
const transaction = {
|
|
831
|
+
version: 1,
|
|
832
|
+
mutationId: parsed["mutationId"],
|
|
833
|
+
state: parseEvidenceLedgerState(parsed["state"]),
|
|
834
|
+
event: parseLedgerEvent(parsed["event"])
|
|
835
|
+
};
|
|
836
|
+
await commitEvidenceTransaction(projectRoot, transaction);
|
|
837
|
+
}
|
|
838
|
+
async function refusePendingEvidenceTransaction(projectRoot) {
|
|
839
|
+
const { transactionFile } = evidenceMutationPaths(projectRoot);
|
|
840
|
+
const stat = await fs.lstat(transactionFile).catch((error) => {
|
|
841
|
+
if (hasErrorCode(error, "ENOENT"))
|
|
842
|
+
return undefined;
|
|
843
|
+
throw error;
|
|
844
|
+
});
|
|
845
|
+
if (stat === undefined)
|
|
846
|
+
return;
|
|
847
|
+
const detail = stat.isSymbolicLink() || !stat.isFile() ? "unsafe pending evidence transaction" : "pending evidence transaction";
|
|
848
|
+
throw new EvidenceLedgerError("recovery_required", `${detail}; status is unavailable until the next mutation recovers it under the ledger lock`);
|
|
849
|
+
}
|
|
850
|
+
async function mutateEvidenceLedger(projectRoot, operation) {
|
|
851
|
+
return withEvidenceLedgerLock(projectRoot, async () => {
|
|
852
|
+
await recoverEvidenceTransaction(projectRoot);
|
|
853
|
+
const mutation = await operation(await readEvidenceLedger(projectRoot));
|
|
854
|
+
const transaction = {
|
|
855
|
+
version: 1,
|
|
856
|
+
mutationId: randomUUID(),
|
|
857
|
+
state: mutation.state,
|
|
858
|
+
event: mutation.event
|
|
859
|
+
};
|
|
860
|
+
await writeEvidenceTransaction(projectRoot, transaction);
|
|
861
|
+
await commitEvidenceTransaction(projectRoot, transaction);
|
|
862
|
+
return mutation.result;
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
function activeGoalOf(state) {
|
|
866
|
+
const goal = state.goals.find((candidate) => candidate.id === state.activeGoalId);
|
|
867
|
+
if (goal === undefined) {
|
|
868
|
+
throw new EvidenceLedgerError("no_active_goal", "no active goal; run create-goals first");
|
|
869
|
+
}
|
|
870
|
+
return goal;
|
|
871
|
+
}
|
|
872
|
+
function mutableActiveGoalOf(state) {
|
|
873
|
+
const goal = activeGoalOf(state);
|
|
874
|
+
if (goal.status === "complete") {
|
|
875
|
+
throw new EvidenceLedgerError("goal_already_complete", `${goal.id} is complete and cannot be mutated or reopened`);
|
|
876
|
+
}
|
|
877
|
+
return goal;
|
|
878
|
+
}
|
|
879
|
+
function replaceGoal(state, goal, at) {
|
|
880
|
+
return {
|
|
881
|
+
...state,
|
|
882
|
+
updatedAt: at,
|
|
883
|
+
goals: state.goals.map((candidate) => (candidate.id === goal.id ? { ...goal, updatedAt: at } : candidate))
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
export function evaluateGoalGate(goal) {
|
|
887
|
+
const reasons = [];
|
|
888
|
+
if (goal.criteria.length === 0)
|
|
889
|
+
reasons.push("no success criteria defined");
|
|
890
|
+
for (const criterion of goal.criteria) {
|
|
891
|
+
const kinds = new Set(criterion.evidence.map((entry) => entry.kind));
|
|
892
|
+
if (criterion.status !== "pass")
|
|
893
|
+
reasons.push(`${criterion.id} status is '${criterion.status}' (need 'pass')`);
|
|
894
|
+
if (!kinds.has("green"))
|
|
895
|
+
reasons.push(`${criterion.id} missing RED to GREEN proof (green evidence)`);
|
|
896
|
+
if (!kinds.has("scenario"))
|
|
897
|
+
reasons.push(`${criterion.id} missing real-surface scenario evidence`);
|
|
898
|
+
}
|
|
899
|
+
for (const blocker of goal.reviewBlockers) {
|
|
900
|
+
if (!blocker.resolved)
|
|
901
|
+
reasons.push(`unresolved review blocker ${blocker.id}: ${blocker.detail}`);
|
|
902
|
+
}
|
|
903
|
+
return { passed: reasons.length === 0, goalId: goal.id, reasons: Object.freeze(reasons) };
|
|
904
|
+
}
|
|
905
|
+
export function countRecordedEvidence(goal) {
|
|
906
|
+
return goal.criteria.reduce((total, criterion) => total + criterion.evidence.length, 0);
|
|
907
|
+
}
|
|
908
|
+
export async function createGoals(projectRoot, input) {
|
|
909
|
+
const at = nowIso(input.now);
|
|
910
|
+
return mutateEvidenceLedger(projectRoot, (state) => {
|
|
911
|
+
const existing = state.goals.find((goal) => goal.sessionId === input.sessionId);
|
|
912
|
+
// A completed session is immutable even with --force. A fresh session opens beside it.
|
|
913
|
+
if (existing?.status === "complete") {
|
|
914
|
+
throw new EvidenceLedgerError("goal_already_complete", `${existing.id} is complete and cannot be mutated or reopened`);
|
|
915
|
+
}
|
|
916
|
+
if (existing !== undefined) {
|
|
917
|
+
const recorded = countRecordedEvidence(existing);
|
|
918
|
+
if (recorded > 0 && input.force !== true) {
|
|
919
|
+
throw new EvidenceLedgerError("evidence_overwrite_refused", `session '${input.sessionId}' already has ${recorded} recorded evidence entr${recorded === 1 ? "y" : "ies"} on ${existing.id}; ` +
|
|
920
|
+
"pass --force to overwrite it deliberately, or use a fresh --session-id to open new state");
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
const criteria = (input.criteria ?? []).map((scenario, index) => ({
|
|
924
|
+
id: `C${index + 1}`,
|
|
925
|
+
scenario,
|
|
926
|
+
qaChannel: "",
|
|
927
|
+
testRef: "",
|
|
928
|
+
status: "pending",
|
|
929
|
+
evidence: []
|
|
930
|
+
}));
|
|
931
|
+
const goal = {
|
|
932
|
+
id: existing?.id ?? nextId("G", state.goals.map((candidate) => candidate.id)),
|
|
933
|
+
sessionId: input.sessionId,
|
|
934
|
+
objective: input.objective,
|
|
935
|
+
title: input.title ?? "",
|
|
936
|
+
status: "active",
|
|
937
|
+
createdAt: existing?.createdAt ?? at,
|
|
938
|
+
updatedAt: at,
|
|
939
|
+
criteria,
|
|
940
|
+
checkpoints: [],
|
|
941
|
+
steering: [],
|
|
942
|
+
reviewBlockers: []
|
|
943
|
+
};
|
|
944
|
+
const goals = existing === undefined
|
|
945
|
+
? [...state.goals, goal]
|
|
946
|
+
: state.goals.map((candidate) => (candidate.id === goal.id ? goal : candidate));
|
|
947
|
+
const next = {
|
|
948
|
+
version: evidenceLedgerVersion,
|
|
949
|
+
createdAt: state.createdAt === "" ? at : state.createdAt,
|
|
950
|
+
updatedAt: at,
|
|
951
|
+
activeGoalId: goal.id,
|
|
952
|
+
goals
|
|
953
|
+
};
|
|
954
|
+
return {
|
|
955
|
+
state: next,
|
|
956
|
+
event: {
|
|
957
|
+
type: "goal.created",
|
|
958
|
+
goalId: goal.id,
|
|
959
|
+
sessionId: goal.sessionId,
|
|
960
|
+
criteria: goal.criteria.length,
|
|
961
|
+
overwrote: existing === undefined ? false : true,
|
|
962
|
+
forced: input.force === true,
|
|
963
|
+
timestamp: at
|
|
964
|
+
},
|
|
965
|
+
result: goal
|
|
966
|
+
};
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
export async function recordEvidence(projectRoot, input) {
|
|
970
|
+
const at = nowIso(input.now);
|
|
971
|
+
const kind = requireOneOf(input.kind, evidenceKinds, "evidence kind");
|
|
972
|
+
const status = input.status === undefined ? undefined : requireOneOf(input.status, criterionStatuses, "criterion status");
|
|
973
|
+
return mutateEvidenceLedger(projectRoot, (state) => {
|
|
974
|
+
const goal = mutableActiveGoalOf(state);
|
|
975
|
+
const criterion = goal.criteria.find((candidate) => candidate.id === input.criterionId);
|
|
976
|
+
if (criterion === undefined) {
|
|
977
|
+
throw new EvidenceLedgerError("criterion_not_found", `criterion '${input.criterionId}' not found on ${goal.id}`);
|
|
978
|
+
}
|
|
979
|
+
// Evidence is append-only per criterion. A retry adds a new entry; it never rewrites an old one,
|
|
980
|
+
// so a second attempt cannot erase the proof that the first attempt failed.
|
|
981
|
+
const updated = {
|
|
982
|
+
...criterion,
|
|
983
|
+
status: status ?? criterion.status,
|
|
984
|
+
evidence: [...criterion.evidence, { kind, ref: input.ref, detail: input.detail ?? "", at }]
|
|
985
|
+
};
|
|
986
|
+
const nextGoal = {
|
|
987
|
+
...goal,
|
|
988
|
+
criteria: goal.criteria.map((candidate) => (candidate.id === updated.id ? updated : candidate))
|
|
989
|
+
};
|
|
990
|
+
return {
|
|
991
|
+
state: replaceGoal(state, nextGoal, at),
|
|
992
|
+
event: {
|
|
993
|
+
type: "evidence.recorded",
|
|
994
|
+
goalId: goal.id,
|
|
995
|
+
criterionId: updated.id,
|
|
996
|
+
evidenceKind: kind,
|
|
997
|
+
ref: input.ref,
|
|
998
|
+
criterionStatus: updated.status,
|
|
999
|
+
timestamp: at
|
|
1000
|
+
},
|
|
1001
|
+
result: updated
|
|
1002
|
+
};
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
export async function recordCheckpoint(projectRoot, input) {
|
|
1006
|
+
const at = nowIso(input.now);
|
|
1007
|
+
return mutateEvidenceLedger(projectRoot, (state) => {
|
|
1008
|
+
const goal = mutableActiveGoalOf(state);
|
|
1009
|
+
const checkpoint = {
|
|
1010
|
+
id: nextId("CP", goal.checkpoints.map((entry) => entry.id)),
|
|
1011
|
+
at,
|
|
1012
|
+
summary: input.summary,
|
|
1013
|
+
activeCriterion: input.activeCriterion ?? ""
|
|
1014
|
+
};
|
|
1015
|
+
const nextGoal = { ...goal, checkpoints: [...goal.checkpoints, checkpoint] };
|
|
1016
|
+
return {
|
|
1017
|
+
state: replaceGoal(state, nextGoal, at),
|
|
1018
|
+
event: {
|
|
1019
|
+
type: "checkpoint.recorded",
|
|
1020
|
+
goalId: goal.id,
|
|
1021
|
+
checkpointId: checkpoint.id,
|
|
1022
|
+
activeCriterion: checkpoint.activeCriterion,
|
|
1023
|
+
timestamp: at
|
|
1024
|
+
},
|
|
1025
|
+
result: checkpoint
|
|
1026
|
+
};
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
export function steeringWeakeningReason(directive) {
|
|
1030
|
+
const match = steeringWeakeningPatterns.find((pattern) => pattern.test(directive));
|
|
1031
|
+
return match === undefined ? undefined : `directive matches a completion-weakening pattern (${match.source})`;
|
|
1032
|
+
}
|
|
1033
|
+
export async function recordSteering(projectRoot, input) {
|
|
1034
|
+
const at = nowIso(input.now);
|
|
1035
|
+
const kind = requireOneOf(input.kind ?? "redirect", steeringKinds, "steering kind");
|
|
1036
|
+
const reason = steeringWeakeningReason(input.directive);
|
|
1037
|
+
if (reason !== undefined) {
|
|
1038
|
+
throw new EvidenceLedgerError("steering_weakens_gate", `steering refused: ${reason}. Steering can redirect or extend the goal, never weaken the completion gate.`);
|
|
1039
|
+
}
|
|
1040
|
+
return mutateEvidenceLedger(projectRoot, (state) => {
|
|
1041
|
+
const goal = mutableActiveGoalOf(state);
|
|
1042
|
+
const directive = input.directive.trim();
|
|
1043
|
+
const criterionId = kind === "add_criterion"
|
|
1044
|
+
? nextId("C", goal.criteria.map((criterion) => criterion.id))
|
|
1045
|
+
: undefined;
|
|
1046
|
+
const steering = {
|
|
1047
|
+
id: nextId("S", goal.steering.map((entry) => entry.id)),
|
|
1048
|
+
at,
|
|
1049
|
+
directive,
|
|
1050
|
+
kind,
|
|
1051
|
+
...(criterionId === undefined ? {} : { criterionId })
|
|
1052
|
+
};
|
|
1053
|
+
const criteria = criterionId === undefined
|
|
1054
|
+
? goal.criteria
|
|
1055
|
+
: [
|
|
1056
|
+
...goal.criteria,
|
|
1057
|
+
{
|
|
1058
|
+
id: criterionId,
|
|
1059
|
+
scenario: directive,
|
|
1060
|
+
qaChannel: "",
|
|
1061
|
+
testRef: "",
|
|
1062
|
+
status: "pending",
|
|
1063
|
+
evidence: []
|
|
1064
|
+
}
|
|
1065
|
+
];
|
|
1066
|
+
const nextGoal = { ...goal, criteria, steering: [...goal.steering, steering] };
|
|
1067
|
+
return {
|
|
1068
|
+
state: replaceGoal(state, nextGoal, at),
|
|
1069
|
+
event: {
|
|
1070
|
+
type: "steering.recorded",
|
|
1071
|
+
goalId: goal.id,
|
|
1072
|
+
steeringId: steering.id,
|
|
1073
|
+
steeringKind: kind,
|
|
1074
|
+
...(criterionId === undefined ? {} : { addedCriterionId: criterionId }),
|
|
1075
|
+
timestamp: at
|
|
1076
|
+
},
|
|
1077
|
+
result: steering
|
|
1078
|
+
};
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
export async function recordReviewBlocker(projectRoot, input) {
|
|
1082
|
+
const at = nowIso(input.now);
|
|
1083
|
+
return mutateEvidenceLedger(projectRoot, (state) => {
|
|
1084
|
+
const goal = mutableActiveGoalOf(state);
|
|
1085
|
+
const blocker = {
|
|
1086
|
+
id: nextId("B", goal.reviewBlockers.map((entry) => entry.id)),
|
|
1087
|
+
detail: input.detail.trim(),
|
|
1088
|
+
resolved: false,
|
|
1089
|
+
needsUserDecision: input.needsUserDecision === true
|
|
1090
|
+
};
|
|
1091
|
+
const nextGoal = {
|
|
1092
|
+
...goal,
|
|
1093
|
+
status: blocker.needsUserDecision ? "needs_user_decision" : "review_blocked",
|
|
1094
|
+
reviewBlockers: [...goal.reviewBlockers, blocker]
|
|
1095
|
+
};
|
|
1096
|
+
return {
|
|
1097
|
+
state: replaceGoal(state, nextGoal, at),
|
|
1098
|
+
event: {
|
|
1099
|
+
type: "review.blocked",
|
|
1100
|
+
goalId: goal.id,
|
|
1101
|
+
blockerId: blocker.id,
|
|
1102
|
+
goalStatus: nextGoal.status,
|
|
1103
|
+
needsUserDecision: blocker.needsUserDecision,
|
|
1104
|
+
timestamp: at
|
|
1105
|
+
},
|
|
1106
|
+
result: blocker
|
|
1107
|
+
};
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
// Recording a blocker without a way to clear it would make review_blocked a terminal state and the
|
|
1111
|
+
// completion gate unreachable forever, so resolution belongs to the same verb that opens a blocker.
|
|
1112
|
+
export async function resolveReviewBlocker(projectRoot, input) {
|
|
1113
|
+
const at = nowIso(input.now);
|
|
1114
|
+
return mutateEvidenceLedger(projectRoot, (state) => {
|
|
1115
|
+
const goal = mutableActiveGoalOf(state);
|
|
1116
|
+
const blocker = goal.reviewBlockers.find((candidate) => candidate.id === input.blockerId);
|
|
1117
|
+
if (blocker === undefined) {
|
|
1118
|
+
throw new EvidenceLedgerError("criterion_not_found", `review blocker '${input.blockerId}' not found on ${goal.id}`);
|
|
1119
|
+
}
|
|
1120
|
+
const resolved = { ...blocker, resolved: true };
|
|
1121
|
+
const reviewBlockers = goal.reviewBlockers.map((candidate) => (candidate.id === resolved.id ? resolved : candidate));
|
|
1122
|
+
const stillBlocked = reviewBlockers.some((candidate) => !candidate.resolved);
|
|
1123
|
+
const nextGoal = {
|
|
1124
|
+
...goal,
|
|
1125
|
+
status: stillBlocked ? goal.status : "active",
|
|
1126
|
+
reviewBlockers
|
|
1127
|
+
};
|
|
1128
|
+
return {
|
|
1129
|
+
state: replaceGoal(state, nextGoal, at),
|
|
1130
|
+
event: {
|
|
1131
|
+
type: "review.blocker.resolved",
|
|
1132
|
+
goalId: goal.id,
|
|
1133
|
+
blockerId: resolved.id,
|
|
1134
|
+
goalStatus: nextGoal.status,
|
|
1135
|
+
timestamp: at
|
|
1136
|
+
},
|
|
1137
|
+
result: resolved
|
|
1138
|
+
};
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
export async function completeGoals(projectRoot, options = {}) {
|
|
1142
|
+
const at = nowIso(options.now);
|
|
1143
|
+
return mutateEvidenceLedger(projectRoot, (state) => {
|
|
1144
|
+
const goal = mutableActiveGoalOf(state);
|
|
1145
|
+
const verdict = evaluateGoalGate(goal);
|
|
1146
|
+
if (!verdict.passed) {
|
|
1147
|
+
const result = { completed: false, goalId: goal.id, reasons: verdict.reasons };
|
|
1148
|
+
const event = {
|
|
1149
|
+
type: "goal.completion.refused",
|
|
1150
|
+
goalId: goal.id,
|
|
1151
|
+
reasons: verdict.reasons.length,
|
|
1152
|
+
timestamp: at
|
|
1153
|
+
};
|
|
1154
|
+
return {
|
|
1155
|
+
state,
|
|
1156
|
+
event,
|
|
1157
|
+
result
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
const result = { completed: true, goalId: goal.id, reasons: [] };
|
|
1161
|
+
const event = { type: "goal.completed", goalId: goal.id, timestamp: at };
|
|
1162
|
+
return {
|
|
1163
|
+
state: replaceGoal(state, { ...goal, status: "complete" }, at),
|
|
1164
|
+
event,
|
|
1165
|
+
result
|
|
1166
|
+
};
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
export async function readEvidenceLedgerStatus(projectRoot) {
|
|
1170
|
+
await refusePendingEvidenceTransaction(projectRoot);
|
|
1171
|
+
const state = await readEvidenceLedger(projectRoot);
|
|
1172
|
+
await refusePendingEvidenceTransaction(projectRoot);
|
|
1173
|
+
const goal = state.goals.find((candidate) => candidate.id === state.activeGoalId);
|
|
1174
|
+
return {
|
|
1175
|
+
activeGoalId: state.activeGoalId,
|
|
1176
|
+
goals: state.goals,
|
|
1177
|
+
...(goal === undefined ? {} : { gate: evaluateGoalGate(goal) })
|
|
1178
|
+
};
|
|
1179
|
+
}
|