@intentius/chant 0.124.13 → 0.124.15
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/bin/chant +11 -0
- package/dist/agents/checks.js +326 -0
- package/dist/agents/discover.js +885 -0
- package/dist/agents/importer.js +17 -0
- package/dist/agents/index.js +12 -0
- package/dist/agents/types.js +19 -0
- package/dist/apply.js +151 -0
- package/dist/attrref.js +70 -0
- package/dist/audit/catalog.js +350 -0
- package/dist/audit/core.js +252 -0
- package/dist/audit/discover.js +633 -0
- package/dist/audit/fetch.js +666 -0
- package/dist/audit/lineage.js +91 -0
- package/dist/audit/nginx.js +366 -0
- package/dist/audit/prior-art.js +132 -0
- package/dist/audit/proof.js +321 -0
- package/dist/audit/report-html.js +194 -0
- package/dist/audit/report-model.js +198 -0
- package/dist/audit/report.js +102 -0
- package/dist/audit/rules-doc.js +144 -0
- package/dist/audit/secrets.js +405 -0
- package/dist/audit/terraform-state.js +140 -0
- package/dist/audit/wrangler.js +262 -0
- package/dist/behaviour-delta.js +401 -0
- package/dist/behaviour-engine.js +357 -0
- package/dist/behaviour-graph.js +83 -0
- package/dist/behaviour-http.js +188 -0
- package/dist/behaviour-kinds.js +172 -0
- package/dist/behaviour-overlay.js +50 -0
- package/dist/behaviour-predict.js +188 -0
- package/dist/behaviour-request.js +194 -0
- package/dist/behaviour.js +1325 -0
- package/dist/build-params.js +108 -0
- package/dist/build.d.ts +6 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +669 -0
- package/dist/builder.js +44 -0
- package/dist/cdk/advise.js +30 -0
- package/dist/cdk/assembly.js +122 -0
- package/dist/cdk/graph.js +444 -0
- package/dist/cdk/tier-map.js +64 -0
- package/dist/cdk/types.js +24 -0
- package/dist/change-set.d.ts +24 -2
- package/dist/change-set.d.ts.map +1 -1
- package/dist/change-set.js +361 -0
- package/dist/child-project.js +21 -0
- package/dist/ci-jobs.d.ts +227 -0
- package/dist/ci-jobs.d.ts.map +1 -0
- package/dist/ci-jobs.js +448 -0
- package/dist/claimed-fields.js +135 -0
- package/dist/cli/build-options.js +70 -0
- package/dist/cli/build-params-cli.js +139 -0
- package/dist/cli/command-group.js +123 -0
- package/dist/cli/commands/audit-agents.js +326 -0
- package/dist/cli/commands/audit.js +571 -0
- package/dist/cli/commands/build.js +920 -0
- package/dist/cli/commands/carve-apply.js +153 -0
- package/dist/cli/commands/carve-bridge.js +201 -0
- package/dist/cli/commands/carve-emit.js +332 -0
- package/dist/cli/commands/carve-status.js +119 -0
- package/dist/cli/commands/carve.js +204 -0
- package/dist/cli/commands/check-lexicon-docs.js +79 -0
- package/dist/cli/commands/check-lexicon-examples.js +177 -0
- package/dist/cli/commands/check-lexicon-intrinsics.js +291 -0
- package/dist/cli/commands/check-lexicon-mcp.js +82 -0
- package/dist/cli/commands/check-lexicon-plugin.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-plugin.js +104 -0
- package/dist/cli/commands/check-lexicon.js +766 -0
- package/dist/cli/commands/describe.js +85 -0
- package/dist/cli/commands/diff.js +175 -0
- package/dist/cli/commands/doctor.js +350 -0
- package/dist/cli/commands/import-agents.js +155 -0
- package/dist/cli/commands/import.js +670 -0
- package/dist/cli/commands/init-lexicon/templates/codegen.js +194 -0
- package/dist/cli/commands/init-lexicon/templates/docs.js +100 -0
- package/dist/cli/commands/init-lexicon/templates/examples.js +29 -0
- package/dist/cli/commands/init-lexicon/templates/lint.js +34 -0
- package/dist/cli/commands/init-lexicon/templates/lsp.js +37 -0
- package/dist/cli/commands/init-lexicon/templates/plugin.js +108 -0
- package/dist/cli/commands/init-lexicon/templates/project.js +240 -0
- package/dist/cli/commands/init-lexicon/templates/spec.js +56 -0
- package/dist/cli/commands/init-lexicon/templates/tests.js +66 -0
- package/dist/cli/commands/init-lexicon.js +172 -0
- package/dist/cli/commands/init.js +381 -0
- package/dist/cli/commands/lexicon-rolling-upgrade.js +145 -0
- package/dist/cli/commands/lexicon-surface-diff.js +145 -0
- package/dist/cli/commands/licenses.js +125 -0
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/commands/lint.js +948 -0
- package/dist/cli/commands/list.js +63 -0
- package/dist/cli/commands/migrate.d.ts.map +1 -1
- package/dist/cli/commands/migrate.js +423 -0
- package/dist/cli/commands/onboard.js +403 -0
- package/dist/cli/commands/pinned-upgrade.js +191 -0
- package/dist/cli/commands/update.js +194 -0
- package/dist/cli/commands/vendor.js +217 -0
- package/dist/cli/conflict-check.js +104 -0
- package/dist/cli/debug.js +8 -0
- package/dist/cli/format.js +104 -0
- package/dist/cli/handlers/build.js +267 -0
- package/dist/cli/handlers/carve-apply.js +30 -0
- package/dist/cli/handlers/carve-bridge.js +28 -0
- package/dist/cli/handlers/carve-emit.js +50 -0
- package/dist/cli/handlers/carve-status.js +26 -0
- package/dist/cli/handlers/carve.js +40 -0
- package/dist/cli/handlers/change-set.js +46 -0
- package/dist/cli/handlers/components.js +730 -0
- package/dist/cli/handlers/dev.js +113 -0
- package/dist/cli/handlers/emulator.js +233 -0
- package/dist/cli/handlers/explain.js +54 -0
- package/dist/cli/handlers/fan-out.js +422 -0
- package/dist/cli/handlers/gate-resume.js +275 -0
- package/dist/cli/handlers/generate.js +84 -0
- package/dist/cli/handlers/graph.js +969 -0
- package/dist/cli/handlers/init.js +102 -0
- package/dist/cli/handlers/lifecycle.js +1727 -0
- package/dist/cli/handlers/lint.js +62 -0
- package/dist/cli/handlers/migrate.js +48 -0
- package/dist/cli/handlers/misc.js +391 -0
- package/dist/cli/handlers/operator.js +1162 -0
- package/dist/cli/handlers/override-approve.js +102 -0
- package/dist/cli/handlers/pr.js +503 -0
- package/dist/cli/handlers/promote.js +342 -0
- package/dist/cli/handlers/run-generate.js +206 -0
- package/dist/cli/handlers/run-wave.js +66 -0
- package/dist/cli/handlers/run.js +1191 -0
- package/dist/cli/handlers/scenario.js +325 -0
- package/dist/cli/handlers/search.js +924 -0
- package/dist/cli/handlers/serve.js +32 -0
- package/dist/cli/handlers/vendor.js +154 -0
- package/dist/cli/index.js +3 -0
- package/dist/cli/is-entry-point.js +23 -0
- package/dist/cli/lsp/capabilities.js +28 -0
- package/dist/cli/lsp/diagnostics.js +34 -0
- package/dist/cli/lsp/server.js +329 -0
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +2540 -0
- package/dist/cli/mcp/lifecycle-tools.js +80 -0
- package/dist/cli/mcp/op-tools.js +306 -0
- package/dist/cli/mcp/resource-handlers.js +258 -0
- package/dist/cli/mcp/resources/context.js +85 -0
- package/dist/cli/mcp/server.js +353 -0
- package/dist/cli/mcp/tools/build.js +111 -0
- package/dist/cli/mcp/tools/composites.js +95 -0
- package/dist/cli/mcp/tools/explain.js +166 -0
- package/dist/cli/mcp/tools/import.js +49 -0
- package/dist/cli/mcp/tools/lint.js +41 -0
- package/dist/cli/mcp/tools/scaffold.js +111 -0
- package/dist/cli/mcp/tools/search.js +112 -0
- package/dist/cli/mcp/types.js +1 -0
- package/dist/cli/mcp/workspace-plugins.js +91 -0
- package/dist/cli/mcp/workspace-tools.d.ts +1 -1
- package/dist/cli/mcp/workspace-tools.js +564 -0
- package/dist/cli/mcp-config.js +70 -0
- package/dist/cli/plugins.js +313 -0
- package/dist/cli/registry.js +23 -0
- package/dist/cli/reporters/stylish.js +329 -0
- package/dist/cli/version.js +15 -0
- package/dist/cli/watch.js +79 -0
- package/dist/codegen/bound-property-types.js +82 -0
- package/dist/codegen/coverage.js +119 -0
- package/dist/codegen/docs-file-markers.js +60 -0
- package/dist/codegen/docs-pages.js +115 -0
- package/dist/codegen/docs-rule-scanning.js +164 -0
- package/dist/codegen/docs-rules.js +76 -0
- package/dist/codegen/docs-sections.js +117 -0
- package/dist/codegen/docs-sidebar.js +65 -0
- package/dist/codegen/docs-types.js +4 -0
- package/dist/codegen/docs.js +356 -0
- package/dist/codegen/fetch.js +340 -0
- package/dist/codegen/generate-post-synth-barrel.js +63 -0
- package/dist/codegen/generate-registry.js +71 -0
- package/dist/codegen/generate-runtime-index.js +56 -0
- package/dist/codegen/generate-typescript.js +119 -0
- package/dist/codegen/generate.js +115 -0
- package/dist/codegen/json-patch.js +141 -0
- package/dist/codegen/json-schema-to-ts.js +344 -0
- package/dist/codegen/json-schema.js +246 -0
- package/dist/codegen/lexicon-regen.js +412 -0
- package/dist/codegen/naming.js +217 -0
- package/dist/codegen/okf-lexicon.js +245 -0
- package/dist/codegen/package.js +150 -0
- package/dist/codegen/pinned-upgrade.d.ts.map +1 -1
- package/dist/codegen/pinned-upgrade.js +494 -0
- package/dist/codegen/registry.js +60 -0
- package/dist/codegen/rehype-base-url.mjs +68 -0
- package/dist/codegen/rolling-upgrade.js +217 -0
- package/dist/codegen/surface-snapshot.js +537 -0
- package/dist/codegen/testdata/postgres-tags.json +242 -0
- package/dist/codegen/topo-sort.js +36 -0
- package/dist/codegen/typecheck.js +115 -0
- package/dist/codegen/validate.js +200 -0
- package/dist/codegen/version-bump.js +35 -0
- package/dist/components/auto-release.js +143 -0
- package/dist/components/builders.js +47 -0
- package/dist/components/capability-plugin-loader.js +175 -0
- package/dist/components/capability-plugin-schema.js +46 -0
- package/dist/components/capability-plugin.js +99 -0
- package/dist/components/capability.js +60 -0
- package/dist/components/changed-files.js +72 -0
- package/dist/components/cli-support.js +427 -0
- package/dist/components/component.js +120 -0
- package/dist/components/component.schema.json +271 -0
- package/dist/components/config-defaults.js +130 -0
- package/dist/components/deploy-units.js +60 -0
- package/dist/components/discover.d.ts.map +1 -1
- package/dist/components/discover.js +189 -0
- package/dist/components/driver-output.js +86 -0
- package/dist/components/driver.js +728 -0
- package/dist/components/fan-out-output.js +165 -0
- package/dist/components/fan-out-record.js +94 -0
- package/dist/components/fan-out-run.js +373 -0
- package/dist/components/fan-out-support.js +60 -0
- package/dist/components/fan-out.js +273 -0
- package/dist/components/gate-plan.js +41 -0
- package/dist/components/gated-wave-pipeline.js +46 -0
- package/dist/components/index.js +24 -0
- package/dist/components/layers.js +28 -0
- package/dist/components/manifest-persistence.js +108 -0
- package/dist/components/pilots/alb-ecs.pilot.js +59 -0
- package/dist/components/pilots/dynamodb.pilot.js +51 -0
- package/dist/components/pilots/jar-emr.pilot.js +65 -0
- package/dist/components/pilots/lambda.pilot.js +52 -0
- package/dist/components/pilots/neo4j-fanout.pilot.js +68 -0
- package/dist/components/pilots/project.js +12 -0
- package/dist/components/pr-pipeline.d.ts.map +1 -1
- package/dist/components/pr-pipeline.js +135 -0
- package/dist/components/pr-run.js +363 -0
- package/dist/components/presets/ecs-fargate.js +72 -0
- package/dist/components/presets/index.js +28 -0
- package/dist/components/presets/lambda.js +34 -0
- package/dist/components/presets/single-host-compose.js +41 -0
- package/dist/components/promote.js +549 -0
- package/dist/components/registry.js +34 -0
- package/dist/components/run-progress.js +33 -0
- package/dist/components/sandbox/driver.d.ts.map +1 -1
- package/dist/components/sandbox/driver.js +93 -0
- package/dist/components/sandbox/run.js +115 -0
- package/dist/components/starter-plugin.js +104 -0
- package/dist/components/verbs/__tests__/mock-cloud-executor.js +68 -0
- package/dist/components/verbs/__tests__/mock-process-runner.js +45 -0
- package/dist/components/verbs/__tests__/mock-sbom-generator.js +56 -0
- package/dist/components/verbs/bom-writer.js +287 -0
- package/dist/components/verbs/build-archive.js +188 -0
- package/dist/components/verbs/build.js +142 -0
- package/dist/components/verbs/cloud-executor.js +110 -0
- package/dist/components/verbs/component-bom.js +125 -0
- package/dist/components/verbs/ensure-secret.js +50 -0
- package/dist/components/verbs/index.js +61 -0
- package/dist/components/verbs/license-policy.js +75 -0
- package/dist/components/verbs/lockfile-sbom-generator.js +191 -0
- package/dist/components/verbs/process-runner.js +78 -0
- package/dist/components/verbs/r2-sync.js +93 -0
- package/dist/components/verbs/reproducibility.js +62 -0
- package/dist/components/verbs/run-agent.js +577 -0
- package/dist/components/verbs/sbom-generator.js +119 -0
- package/dist/components/verbs/sbom.js +97 -0
- package/dist/components/verbs/shell.js +32 -0
- package/dist/components/verbs/sign.js +221 -0
- package/dist/components/verbs/stub.js +28 -0
- package/dist/components/verbs/tool-sbom-generator.js +154 -0
- package/dist/components/verbs/verify.js +112 -0
- package/dist/components/verbs/vex.js +131 -0
- package/dist/components/verbs/vuln-gate.js +152 -0
- package/dist/components/verbs/vuln-scan.js +288 -0
- package/dist/components/verbs/wait-verify.js +106 -0
- package/dist/components/verbs/wrangler.js +179 -0
- package/dist/components/wave-plan.js +102 -0
- package/dist/composite.d.ts +10 -2
- package/dist/composite.d.ts.map +1 -1
- package/dist/composite.js +313 -0
- package/dist/config-import.d.ts.map +1 -1
- package/dist/config-import.js +22 -0
- package/dist/config-sandbox.js +122 -0
- package/dist/config-static.js +206 -0
- package/dist/config.js +605 -0
- package/dist/content-digest.js +23 -0
- package/dist/declarable.js +114 -0
- package/dist/deep-observation.js +437 -0
- package/dist/detectLexicon.js +54 -0
- package/dist/discovery/cache.js +70 -0
- package/dist/discovery/collect.d.ts +2 -1
- package/dist/discovery/collect.d.ts.map +1 -1
- package/dist/discovery/collect.js +223 -0
- package/dist/discovery/cycles.js +95 -0
- package/dist/discovery/entity-wire-codec.js +391 -0
- package/dist/discovery/entity-wire.js +42 -0
- package/dist/discovery/files.js +121 -0
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/discovery/fold-import.js +4073 -0
- package/dist/discovery/fold-rank.js +292 -0
- package/dist/discovery/graph.js +115 -0
- package/dist/discovery/host-composite.d.ts +35 -0
- package/dist/discovery/host-composite.d.ts.map +1 -0
- package/dist/discovery/host-composite.js +93 -0
- package/dist/discovery/import.d.ts.map +1 -1
- package/dist/discovery/import.js +65 -0
- package/dist/discovery/index.d.ts +9 -0
- package/dist/discovery/index.d.ts.map +1 -1
- package/dist/discovery/index.js +333 -0
- package/dist/discovery/param-deps.js +487 -0
- package/dist/discovery/resolve.js +39 -0
- package/dist/discovery/sandbox/bundle.js +214 -0
- package/dist/discovery/sandbox/child-errors.js +26 -0
- package/dist/discovery/sandbox/config-run.js +100 -0
- package/dist/discovery/sandbox/config-wire.js +183 -0
- package/dist/discovery/sandbox/driver.d.ts.map +1 -1
- package/dist/discovery/sandbox/driver.js +333 -0
- package/dist/discovery/sandbox/fork.js +229 -0
- package/dist/discovery/sandbox/policy-run.js +142 -0
- package/dist/discovery/sandbox/policy-wire.js +152 -0
- package/dist/discovery/sandbox/run.js +77 -0
- package/dist/discovery/specifier-resolve.js +320 -0
- package/dist/discovery/walk.js +284 -0
- package/dist/effect-receipt.js +340 -0
- package/dist/env.js +55 -0
- package/dist/errors.js +62 -0
- package/dist/fold/fold.js +1434 -0
- package/dist/fold/foldable-helpers.js +229 -0
- package/dist/fold/subset.js +696 -0
- package/dist/fold-provenance.js +414 -0
- package/dist/gate-decision.d.ts +20 -9
- package/dist/gate-decision.d.ts.map +1 -1
- package/dist/gate-decision.js +313 -0
- package/dist/gate-resume-run.js +154 -0
- package/dist/gated-waves.js +165 -0
- package/dist/governance.js +43 -0
- package/dist/graph-declared.js +45 -0
- package/dist/graph-detail.js +207 -0
- package/dist/graph-dot.js +54 -0
- package/dist/graph-effective.js +107 -0
- package/dist/graph-ir.js +623 -0
- package/dist/graph-layout.js +184 -0
- package/dist/graph-lens.js +105 -0
- package/dist/graph-mermaid.js +83 -0
- package/dist/graph-ops.js +39 -0
- package/dist/graph-refs.js +156 -0
- package/dist/held-elsewhere.js +87 -0
- package/dist/identity.js +251 -0
- package/dist/import/base-parser.js +34 -0
- package/dist/import/embedded.js +255 -0
- package/dist/import/generator.js +1 -0
- package/dist/import/ir-utils.js +85 -0
- package/dist/import/parser-options.js +45 -0
- package/dist/import/parser.js +1 -0
- package/dist/index.js +118 -0
- package/dist/intrinsic-interpolation.js +73 -0
- package/dist/intrinsic.js +31 -0
- package/dist/join-key.js +35 -0
- package/dist/kubectl-context.js +150 -0
- package/dist/lexicon-config.js +58 -0
- package/dist/lexicon-integrity.js +47 -0
- package/dist/lexicon-manifest.js +70 -0
- package/dist/lexicon-module.d.ts.map +1 -1
- package/dist/lexicon-module.js +241 -0
- package/dist/lexicon-output.js +218 -0
- package/dist/lexicon-plugin-helpers.js +104 -0
- package/dist/lexicon-schema.js +136 -0
- package/dist/lexicon.js +130 -0
- package/dist/lifecycle/affected.js +224 -0
- package/dist/lifecycle/assert-live.js +110 -0
- package/dist/lifecycle/build-ledger-store.js +160 -0
- package/dist/lifecycle/build-ledger.js +155 -0
- package/dist/lifecycle/change-set.js +356 -0
- package/dist/lifecycle/converge-ledger.js +161 -0
- package/dist/lifecycle/deep-diff.js +233 -0
- package/dist/lifecycle/deep-observe.js +147 -0
- package/dist/lifecycle/digest.js +143 -0
- package/dist/lifecycle/disruption.js +135 -0
- package/dist/lifecycle/gate-applied.js +113 -0
- package/dist/lifecycle/gate-audit.d.ts +69 -2
- package/dist/lifecycle/gate-audit.d.ts.map +1 -1
- package/dist/lifecycle/gate-audit.js +415 -0
- package/dist/lifecycle/gate-ledger-select.js +89 -0
- package/dist/lifecycle/gate-ledger.d.ts +24 -0
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.js +201 -0
- package/dist/lifecycle/gate-origin.js +117 -0
- package/dist/lifecycle/gate-pending.js +58 -0
- package/dist/lifecycle/git.js +1182 -0
- package/dist/lifecycle/identity.js +58 -0
- package/dist/lifecycle/index.js +26 -0
- package/dist/lifecycle/lease.js +283 -0
- package/dist/lifecycle/ledger-port.js +1 -0
- package/dist/lifecycle/legacy-digest.js +91 -0
- package/dist/lifecycle/live-diff.js +274 -0
- package/dist/lifecycle/local-note.js +15 -0
- package/dist/lifecycle/member-ledger.js +122 -0
- package/dist/lifecycle/observation-baseline.js +155 -0
- package/dist/lifecycle/observe.js +375 -0
- package/dist/lifecycle/oras-referrer-lookup.js +82 -0
- package/dist/lifecycle/override-ledger.js +54 -0
- package/dist/lifecycle/plan-digest.js +110 -0
- package/dist/lifecycle/plan-ledger.js +129 -0
- package/dist/lifecycle/receipt-plan.js +192 -0
- package/dist/lifecycle/release-ledger.js +155 -0
- package/dist/lifecycle/release-read.js +64 -0
- package/dist/lifecycle/replay.js +193 -0
- package/dist/lifecycle/rollback.js +102 -0
- package/dist/lifecycle/run-ledger.js +183 -0
- package/dist/lifecycle/scenario-eval.js +204 -0
- package/dist/lifecycle/scenario.js +214 -0
- package/dist/lifecycle/snapshot.js +239 -0
- package/dist/lifecycle/status.js +447 -0
- package/dist/lifecycle/symptoms.js +117 -0
- package/dist/lifecycle/teardown.js +255 -0
- package/dist/lifecycle/types.js +1 -0
- package/dist/lifecycle/unobserved-gate.js +33 -0
- package/dist/lifecycle/wave-plans.js +68 -0
- package/dist/lifecycle/work-item-id.js +36 -0
- package/dist/lifecycle/work-lease.js +393 -0
- package/dist/lint/component-checks.js +87 -0
- package/dist/lint/config.js +483 -0
- package/dist/lint/declarative.js +122 -0
- package/dist/lint/discover.d.ts.map +1 -1
- package/dist/lint/discover.js +138 -0
- package/dist/lint/engine.js +246 -0
- package/dist/lint/knowledge-checks.js +64 -0
- package/dist/lint/named-checks.js +22 -0
- package/dist/lint/output-checks.js +87 -0
- package/dist/lint/output-docs.js +163 -0
- package/dist/lint/parser.js +33 -0
- package/dist/lint/pipeline-change-gate.js +70 -0
- package/dist/lint/policy-import.d.ts +1 -1
- package/dist/lint/policy-import.d.ts.map +1 -1
- package/dist/lint/policy-import.js +62 -0
- package/dist/lint/policy-sandbox.js +86 -0
- package/dist/lint/policy.js +122 -0
- package/dist/lint/post-synth.js +87 -0
- package/dist/lint/presets/relaxed.json +26 -0
- package/dist/lint/presets/strict.json +24 -24
- package/dist/lint/receipt-checks.js +83 -0
- package/dist/lint/rule-loader.d.ts.map +1 -1
- package/dist/lint/rule-loader.js +89 -0
- package/dist/lint/rule-registry.js +86 -0
- package/dist/lint/rule.js +1 -0
- package/dist/lint/rules/__fixtures__/comp/comp000-discovery-error/broken-a.component.js +7 -0
- package/dist/lint/rules/__fixtures__/comp/comp000-discovery-error/broken-b.component.js +7 -0
- package/dist/lint/rules/__fixtures__/comp/comp001/fail/orphan-lib.component.js +9 -0
- package/dist/lint/rules/__fixtures__/comp/comp001/pass/emr-job.component.js +11 -0
- package/dist/lint/rules/__fixtures__/comp/comp001/pass/jar-lib.component.js +9 -0
- package/dist/lint/rules/__fixtures__/comp/comp002/fail/search-service.component.js +25 -0
- package/dist/lint/rules/__fixtures__/comp/comp002/pass/jar-lib.component.js +8 -0
- package/dist/lint/rules/__fixtures__/comp/comp002/pass/search-service.component.js +21 -0
- package/dist/lint/rules/__fixtures__/comp/comp003/fail/migrator.component.js +8 -0
- package/dist/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.js +24 -0
- package/dist/lint/rules/__fixtures__/comp/comp003/pass/migrator.component.js +17 -0
- package/dist/lint/rules/__fixtures__/comp/comp003/pass/orders-table.component.js +8 -0
- package/dist/lint/rules/__fixtures__/comp/comp004/fail/neo4j-cluster.component.js +18 -0
- package/dist/lint/rules/__fixtures__/comp/comp004/pass/neo4j-cluster.component.js +20 -0
- package/dist/lint/rules/__fixtures__/comp/comp004/pass/orders-table.component.js +8 -0
- package/dist/lint/rules/__fixtures__/comp/comp005/fail/search-service.component.js +12 -0
- package/dist/lint/rules/__fixtures__/comp/comp005/pass/search-service.component.js +16 -0
- package/dist/lint/rules/__fixtures__/comp/comp006/fail/legacy-tool.component.js +8 -0
- package/dist/lint/rules/__fixtures__/comp/comp006/pass/legacy-tool.component.js +12 -0
- package/dist/lint/rules/__fixtures__/comp/comp007/fail/inventory-table.component.js +11 -0
- package/dist/lint/rules/__fixtures__/comp/comp007/fail/orders-table.component.js +11 -0
- package/dist/lint/rules/__fixtures__/comp/comp007/pass/orders-table.component.js +10 -0
- package/dist/lint/rules/__fixtures__/comp/comp007/pass/search-service.component.js +16 -0
- package/dist/lint/rules/comp/comp001-publishes-never-applies.js +69 -0
- package/dist/lint/rules/comp/comp002-dangling-wiring-ref.js +84 -0
- package/dist/lint/rules/comp/comp003-mutating-no-rollback.js +103 -0
- package/dist/lint/rules/comp/comp004-gate-needs-durable-runtime.js +70 -0
- package/dist/lint/rules/comp/comp005-capability-kind-is-noun.js +109 -0
- package/dist/lint/rules/comp/comp006-shell-needs-reason.js +49 -0
- package/dist/lint/rules/comp/comp007-composition-sprawl.js +64 -0
- package/dist/lint/rules/comp/index.js +33 -0
- package/dist/lint/rules/comp/support.js +129 -0
- package/dist/lint/rules/composite-scope.js +51 -0
- package/dist/lint/rules/cor017-composite-name-match.js +93 -0
- package/dist/lint/rules/cor018-composite-prefer-lexicon-type.js +136 -0
- package/dist/lint/rules/cor021-env-literal-name.js +100 -0
- package/dist/lint/rules/cor022-receipt-leaf.js +118 -0
- package/dist/lint/rules/cor024-receipt-secret-pointer.js +199 -0
- package/dist/lint/rules/declarable-naming-convention.js +62 -0
- package/dist/lint/rules/evl001-non-literal-expression.js +66 -0
- package/dist/lint/rules/evl002-control-flow-resource.js +69 -0
- package/dist/lint/rules/evl003-dynamic-property-access.js +44 -0
- package/dist/lint/rules/evl004-spread-non-const.js +94 -0
- package/dist/lint/rules/evl005-resource-block-body.js +41 -0
- package/dist/lint/rules/evl007-invalid-siblings.js +105 -0
- package/dist/lint/rules/evl009-composite-no-constant.js +158 -0
- package/dist/lint/rules/evl010-composite-no-transform.js +59 -0
- package/dist/lint/rules/evl011-symbolic-in-template.js +106 -0
- package/dist/lint/rules/export-required.js +115 -0
- package/dist/lint/rules/file-declarable-limit.js +88 -0
- package/dist/lint/rules/flat-declarations.js +62 -0
- package/dist/lint/rules/index.js +90 -0
- package/dist/lint/rules/no-cyclic-declarable-ref.js +138 -0
- package/dist/lint/rules/no-redundant-type-import.js +76 -0
- package/dist/lint/rules/no-redundant-value-cast.js +41 -0
- package/dist/lint/rules/no-string-ref.js +59 -0
- package/dist/lint/rules/no-unused-declarable-import.js +75 -0
- package/dist/lint/rules/no-unused-declarable.js +142 -0
- package/dist/lint/rules/op/index.js +28 -0
- package/dist/lint/rules/op/ops012-activity-contract.js +54 -0
- package/dist/lint/rules/op/ops013-step-output-ref.js +88 -0
- package/dist/lint/rules/op/ops014-converge-rule-refusals.js +114 -0
- package/dist/lint/rules/op/ops015-gate-approval.js +45 -0
- package/dist/lint/rules/op/support.js +11 -0
- package/dist/lint/rules/op/sys010-key-over-plain-http.js +100 -0
- package/dist/lint/rules/property-kind.js +56 -0
- package/dist/lint/rules/single-concern-file.js +71 -0
- package/dist/lint/rules/sys001-no-literal-key.js +60 -0
- package/dist/lint/selectors.js +168 -0
- package/dist/lint/suppressions.js +129 -0
- package/dist/lint/typescript.js +17 -0
- package/dist/live-endpoint.js +113 -0
- package/dist/live-read-session.js +76 -0
- package/dist/lsp/lexicon-providers.js +163 -0
- package/dist/lsp/types.js +5 -0
- package/dist/managed-fields.js +293 -0
- package/dist/mcp/types.js +4 -0
- package/dist/notify-verify.js +0 -1
- package/dist/notify.js +0 -1
- package/dist/notify.schema.json +72 -0
- package/dist/observation.js +266 -0
- package/dist/okf-read.js +134 -0
- package/dist/okf.js +275 -0
- package/dist/op/__fixtures__/decide-stub-backend.js +125 -0
- package/dist/op/activities/activity-contracts.d.ts +1 -1
- package/dist/op/activities/activity-contracts.js +313 -0
- package/dist/op/activities/apply.js +582 -0
- package/dist/op/activities/build.js +18 -0
- package/dist/op/activities/change-coverage.js +40 -0
- package/dist/op/activities/change-set.d.ts +8 -3
- package/dist/op/activities/change-set.d.ts.map +1 -1
- package/dist/op/activities/change-set.js +67 -0
- package/dist/op/activities/converge.js +492 -0
- package/dist/op/activities/decide.js +211 -0
- package/dist/op/activities/env-teardown.js +110 -0
- package/dist/op/activities/factory.js +591 -0
- package/dist/op/activities/forge-pull-request.js +82 -0
- package/dist/op/activities/gh-host.js +78 -0
- package/dist/op/activities/guard-validate.js +165 -0
- package/dist/op/activities/http-check.js +40 -0
- package/dist/op/activities/index.js +59 -0
- package/dist/op/activities/lexicon-upgrade.js +629 -0
- package/dist/op/activities/lifecycle.js +109 -0
- package/dist/op/activities/pipeline-audit.js +147 -0
- package/dist/op/activities/policy.js +26 -0
- package/dist/op/activities/predict-behaviour.js +341 -0
- package/dist/op/activities/propose-upgrade.js +165 -0
- package/dist/op/activities/reconcile.d.ts +28 -16
- package/dist/op/activities/reconcile.d.ts.map +1 -1
- package/dist/op/activities/reconcile.js +1011 -0
- package/dist/op/activities/shell.js +118 -0
- package/dist/op/activities/source-release.js +97 -0
- package/dist/op/activities/source-rollback.js +165 -0
- package/dist/op/activities/teardown.js +17 -0
- package/dist/op/activities/util.js +6 -0
- package/dist/op/activities/wait.js +51 -0
- package/dist/op/activities/work-evidence.js +35 -0
- package/dist/op/activities/workflow-audit.js +133 -0
- package/dist/op/activity-contract-registry.js +106 -0
- package/dist/op/activity-contract.js +232 -0
- package/dist/op/activity-failure.js +31 -0
- package/dist/op/activity-profiles.js +129 -0
- package/dist/op/activity-registry.js +105 -0
- package/dist/op/activity-runtime.js +28 -0
- package/dist/op/builders.d.ts +1 -1
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/builders.js +801 -0
- package/dist/op/change-signal.js +118 -0
- package/dist/op/composites/apply-op.js +151 -0
- package/dist/op/composites/behaviour-op.js +65 -0
- package/dist/op/composites/converge-op.js +141 -0
- package/dist/op/composites/index.js +19 -0
- package/dist/op/composites/lexicon-upgrade-op.js +107 -0
- package/dist/op/composites/pipeline-audit-op.js +50 -0
- package/dist/op/composites/reconcile-op.js +84 -0
- package/dist/op/composites/watch-op.js +69 -0
- package/dist/op/composites/workflow-audit-op.js +57 -0
- package/dist/op/converge-rule.js +205 -0
- package/dist/op/cron.js +146 -0
- package/dist/op/decide-backend.js +181 -0
- package/dist/op/decide-config.js +68 -0
- package/dist/op/decide-read-inputs.js +141 -0
- package/dist/op/discover.d.ts.map +1 -1
- package/dist/op/discover.js +213 -0
- package/dist/op/duration.js +21 -0
- package/dist/op/emulator-freshness.js +80 -0
- package/dist/op/emulator-lifecycle.js +269 -0
- package/dist/op/factory.js +83 -0
- package/dist/op/gate-approval.js +172 -0
- package/dist/op/gate-name.js +86 -0
- package/dist/op/gate-plan-context.js +167 -0
- package/dist/op/gate-point-run.js +67 -0
- package/dist/op/gate-point.js +111 -0
- package/dist/op/gate-resume-plan.js +155 -0
- package/dist/op/gate-resume.js +31 -0
- package/dist/op/gate-review.js +290 -0
- package/dist/op/gate-run-locator.js +85 -0
- package/dist/op/gate-summary.js +100 -0
- package/dist/op/gate-wait.js +56 -0
- package/dist/op/gate.js +449 -0
- package/dist/op/generate-pipeline.js +124 -0
- package/dist/op/index.js +51 -0
- package/dist/op/lifecycle-receipt-store.js +45 -0
- package/dist/op/local-executor.js +1164 -0
- package/dist/op/local-output.js +140 -0
- package/dist/op/op-ir.js +317 -0
- package/dist/op/op-verb-class.js +131 -0
- package/dist/op/op-waves-notify.js +136 -0
- package/dist/op/op-waves-run.d.ts +1 -1
- package/dist/op/op-waves-run.d.ts.map +1 -1
- package/dist/op/op-waves-run.js +1657 -0
- package/dist/op/op-waves.js +409 -0
- package/dist/op/operator.js +743 -0
- package/dist/op/receipt-store.js +94 -0
- package/dist/op/resource.js +23 -0
- package/dist/op/run-context.js +28 -0
- package/dist/op/run-live.js +256 -0
- package/dist/op/runtime.js +21 -0
- package/dist/op/runtimes/local.js +195 -0
- package/dist/op/source-archive.js +131 -0
- package/dist/op/step-output-ref.js +397 -0
- package/dist/op/steward-beside.js +169 -0
- package/dist/op/steward-points.js +138 -0
- package/dist/op/steward-turn.js +59 -0
- package/dist/op/steward.js +317 -0
- package/dist/op/types.js +25 -0
- package/dist/op/work-lease-decl.js +81 -0
- package/dist/op/work-lease-run.js +393 -0
- package/dist/override-gate.js +284 -0
- package/dist/owner-chain.js +74 -0
- package/dist/ownership.js +92 -0
- package/dist/params.js +61 -0
- package/dist/plan-summary.js +711 -0
- package/dist/point-decision.js +211 -0
- package/dist/pr-forge.js +810 -0
- package/dist/pr-loop.js +203 -0
- package/dist/project-codegen.js +191 -0
- package/dist/project-root.js +74 -0
- package/dist/project-validation.js +50 -0
- package/dist/provenance.js +157 -0
- package/dist/pseudo-parameter.js +40 -0
- package/dist/reconcile.js +450 -0
- package/dist/resource-attributes.js +33 -0
- package/dist/runtime-adapter.js +77 -0
- package/dist/runtime.js +66 -0
- package/dist/secret-materialization.js +153 -0
- package/dist/secret-provenance.js +250 -0
- package/dist/serializer-walker.js +112 -0
- package/dist/serializer.js +1 -0
- package/dist/sort.js +45 -0
- package/dist/stack-output.js +115 -0
- package/dist/telemetry-attribution.js +149 -0
- package/dist/terraform/__fixtures__/build-graph.js +63 -0
- package/dist/terraform/adopt-state.js +40 -0
- package/dist/terraform/aws-resources.js +384 -0
- package/dist/terraform/bridge.js +204 -0
- package/dist/terraform/carve-provider.js +155 -0
- package/dist/terraform/carve.js +138 -0
- package/dist/terraform/data-source-shape.js +57 -0
- package/dist/terraform/emit-source.js +36 -0
- package/dist/terraform/excise.js +123 -0
- package/dist/terraform/graduate.js +128 -0
- package/dist/terraform/graph.js +435 -0
- package/dist/terraform/hcl2json.js +60 -0
- package/dist/terraform/manifest.js +149 -0
- package/dist/terraform/parse.js +81 -0
- package/dist/terraform/providers/aws.js +146 -0
- package/dist/terraform/providers/gcp.js +294 -0
- package/dist/terraform/providers/index.js +17 -0
- package/dist/terraform/providers/kubernetes.js +253 -0
- package/dist/terraform/score.js +151 -0
- package/dist/terraform/state.js +67 -0
- package/dist/terraform/tier-map.js +74 -0
- package/dist/terraform/types.js +13 -0
- package/dist/terraform/unified-diff.js +132 -0
- package/dist/testing.js +295 -0
- package/dist/toml-emit.js +153 -0
- package/dist/toml-parse.js +355 -0
- package/dist/toml-utils.js +60 -0
- package/dist/toml.js +428 -0
- package/dist/ts-loader.d.ts +46 -0
- package/dist/ts-loader.d.ts.map +1 -0
- package/dist/ts-loader.js +103 -0
- package/dist/types.js +1 -0
- package/dist/utils.js +61 -0
- package/dist/validation.js +20 -0
- package/dist/vendor/js-yaml-load.js +1662 -0
- package/dist/workspace/__fixtures__/contract-repo.d.ts +7 -0
- package/dist/workspace/__fixtures__/contract-repo.d.ts.map +1 -1
- package/dist/workspace/__fixtures__/contract-repo.js +119 -0
- package/dist/workspace/__fixtures__/decide-workspace.js +112 -0
- package/dist/workspace/__fixtures__/sessions.d.ts +1 -1
- package/dist/workspace/__fixtures__/sessions.d.ts.map +1 -1
- package/dist/workspace/__fixtures__/sessions.js +91 -0
- package/dist/workspace/__fixtures__/two-member-workspace.js +34 -0
- package/dist/workspace/agent-cli.js +99 -0
- package/dist/workspace/agent.schema.json +390 -0
- package/dist/workspace/answers-ledger.js +89 -0
- package/dist/workspace/box-cli.js +142 -0
- package/dist/workspace/box-factory-write.js +196 -0
- package/dist/workspace/box-factory-write.schema.json +342 -0
- package/dist/workspace/box-factory.js +88 -0
- package/dist/workspace/box-intent.js +126 -0
- package/dist/workspace/box-isolation.js +169 -0
- package/dist/workspace/box-listing-write.schema.json +204 -0
- package/dist/workspace/box-listing.js +298 -0
- package/dist/workspace/box-publish.js +352 -0
- package/dist/workspace/box-publish.schema.json +498 -0
- package/dist/workspace/box-services.js +39 -0
- package/dist/workspace/broker-protocol.js +359 -0
- package/dist/workspace/broker-protocol.schema.json +228 -0
- package/dist/workspace/change-set.schema.json +462 -0
- package/dist/workspace/changes-cli.js +56 -0
- package/dist/workspace/changes.js +446 -0
- package/dist/workspace/changes.schema.json +642 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-fly-site.js +198 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-live-names.js +155 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-rollback.js +214 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.js +180 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-policy.js +99 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit.js +847 -0
- package/dist/workspace/chant-migrations.js +113 -0
- package/dist/workspace/check.schema.json +433 -0
- package/dist/workspace/checks/box-isolation.js +62 -0
- package/dist/workspace/checks/boxes.js +491 -0
- package/dist/workspace/checks/diagrams.js +104 -0
- package/dist/workspace/checks/generated.js +419 -0
- package/dist/workspace/checks/ledgers.js +212 -0
- package/dist/workspace/checks/links.js +191 -0
- package/dist/workspace/checks/live.js +100 -0
- package/dist/workspace/checks/pipelines.js +188 -0
- package/dist/workspace/checks/records.js +169 -0
- package/dist/workspace/checks.js +426 -0
- package/dist/workspace/ci-green-cli.js +125 -0
- package/dist/workspace/ci-green-forge.js +83 -0
- package/dist/workspace/ci-green-workflow.d.ts +3 -1
- package/dist/workspace/ci-green-workflow.d.ts.map +1 -1
- package/dist/workspace/ci-green-workflow.js +202 -0
- package/dist/workspace/ci-green.js +325 -0
- package/dist/workspace/ci-last-green.schema.json +158 -0
- package/dist/workspace/class-data.d.ts +84 -0
- package/dist/workspace/class-data.d.ts.map +1 -0
- package/dist/workspace/class-data.js +238 -0
- package/dist/workspace/compose-graph.js +235 -0
- package/dist/workspace/compose-reports.js +146 -0
- package/dist/workspace/composites.js +264 -0
- package/dist/workspace/composites.schema.json +617 -0
- package/dist/workspace/conformance/__fixture__/app/package.json +7 -0
- package/dist/workspace/conformance/__fixture__/app/src/server.mjs +29 -0
- package/dist/workspace/conformance/__fixture__/decisions/decision.kind.mjs +54 -0
- package/dist/workspace/conformance/__fixture__/decisions/decision.schema.json +512 -0
- package/dist/workspace/conformance/__fixture__/decisions/fix-001-how-the-app-is-deployed.md +40 -0
- package/dist/workspace/conformance/__fixture__/delivery/chant.config.ts +7 -0
- package/dist/workspace/conformance/__fixture__/delivery/lexicon/index.ts +26 -0
- package/dist/workspace/conformance/__fixture__/delivery/package.json +7 -0
- package/dist/workspace/conformance/__fixture__/delivery/src/app.component.ts +14 -0
- package/dist/workspace/conformance/__fixture__/delivery/src/app.ts +4 -0
- package/dist/workspace/conformance/__fixtures__/reference-broker.js +205 -0
- package/dist/workspace/conformance/__writer_fixture__/answers/answer.kind.mjs +32 -0
- package/dist/workspace/conformance/__writer_fixture__/answers/answer.schema.json +135 -0
- package/dist/workspace/conformance/__writer_fixture__/contracts/contract.kind.mjs +37 -0
- package/dist/workspace/conformance/__writer_fixture__/contracts/contract.schema.json +213 -0
- package/dist/workspace/conformance/__writer_fixture__/decisions/points.json +228 -0
- package/dist/workspace/conformance/__writer_fixture__/drivers/driver.kind.mjs +15 -0
- package/dist/workspace/conformance/__writer_fixture__/drivers/driver.schema.json +38 -0
- package/dist/workspace/conformance/__writer_fixture__/sessions/session.kind.mjs +54 -0
- package/dist/workspace/conformance/__writer_fixture__/sessions/session.schema.json +289 -0
- package/dist/workspace/conformance/__writer_fixture__/work/W-001-the-writer-suite-works-this-item.md +25 -0
- package/dist/workspace/conformance/__writer_fixture__/work/work.kind.mjs +60 -0
- package/dist/workspace/conformance/__writer_fixture__/work/work.schema.json +603 -0
- package/dist/workspace/conformance/broker.js +736 -0
- package/dist/workspace/conformance/index.js +673 -0
- package/dist/workspace/conformance/vitest.js +108 -0
- package/dist/workspace/conformance/writer.js +1080 -0
- package/dist/workspace/decide.js +879 -0
- package/dist/workspace/decision-points.schema.json +135 -135
- package/dist/workspace/declaration-write-cli.js +74 -0
- package/dist/workspace/declaration-write.js +238 -0
- package/dist/workspace/declaration.js +938 -0
- package/dist/workspace/declaration.schema.json +1413 -1413
- package/dist/workspace/declared-kinds.js +106 -0
- package/dist/workspace/environments.js +164 -0
- package/dist/workspace/evidence.schema.json +279 -0
- package/dist/workspace/export-cli.js +185 -0
- package/dist/workspace/export.js +499 -0
- package/dist/workspace/factory-rules.js +175 -0
- package/dist/workspace/generated-files.js +92 -0
- package/dist/workspace/glob.js +45 -0
- package/dist/workspace/graph-cache.js +352 -0
- package/dist/workspace/graph-cli.js +348 -0
- package/dist/workspace/graph.schema.json +1338 -0
- package/dist/workspace/identity-gate-rule.js +75 -0
- package/dist/workspace/identity.js +126 -0
- package/dist/workspace/import.js +283 -0
- package/dist/workspace/init.js +620 -0
- package/dist/workspace/intent-cli.js +263 -0
- package/dist/workspace/intent-joins.js +201 -0
- package/dist/workspace/intent-record.js +352 -0
- package/dist/workspace/intent-record.schema.json +864 -0
- package/dist/workspace/intent.js +1582 -0
- package/dist/workspace/intent.schema.json +2420 -0
- package/dist/workspace/json-edit.js +221 -0
- package/dist/workspace/jsonc.js +235 -0
- package/dist/workspace/kind-readers.js +103 -0
- package/dist/workspace/kinds.d.ts +3 -7
- package/dist/workspace/kinds.d.ts.map +1 -1
- package/dist/workspace/kinds.js +553 -0
- package/dist/workspace/lexicon-symbols.js +112 -0
- package/dist/workspace/lineage-adopt-cli.js +102 -0
- package/dist/workspace/lineage-adopt.js +360 -0
- package/dist/workspace/lineage-check.js +282 -0
- package/dist/workspace/lineage-cli.js +121 -0
- package/dist/workspace/lineage-hash-index.js +284 -0
- package/dist/workspace/lineage-init.js +507 -0
- package/dist/workspace/lineage-lock.js +387 -0
- package/dist/workspace/lineage-merge.js +45 -0
- package/dist/workspace/lineage-migrations.d.ts.map +1 -1
- package/dist/workspace/lineage-migrations.js +329 -0
- package/dist/workspace/lineage-provenance.js +36 -0
- package/dist/workspace/lineage-update.js +138 -0
- package/dist/workspace/lineage-upgrade-cli.js +131 -0
- package/dist/workspace/lineage-upgrade.js +889 -0
- package/dist/workspace/lineage-vendor.js +171 -0
- package/dist/workspace/lineage-version.js +128 -0
- package/dist/workspace/lineage-versions.js +275 -0
- package/dist/workspace/links.js +330 -0
- package/dist/workspace/ls-generated.js +95 -0
- package/dist/workspace/ls.js +277 -0
- package/dist/workspace/ls.schema.json +333 -0
- package/dist/workspace/member-commands.js +508 -0
- package/dist/workspace/member-handles.js +216 -0
- package/dist/workspace/member-pipeline.js +258 -0
- package/dist/workspace/member-run.js +117 -0
- package/dist/workspace/member-write.schema.json +193 -0
- package/dist/workspace/nested-graph.js +156 -0
- package/dist/workspace/nesting.js +38 -0
- package/dist/workspace/patch.js +301 -0
- package/dist/workspace/patch.schema.json +321 -0
- package/dist/workspace/pin-cli.js +55 -0
- package/dist/workspace/pin-integrity.js +100 -0
- package/dist/workspace/plan-summary.schema.json +155 -0
- package/dist/workspace/point-answer.schema.json +129 -129
- package/dist/workspace/points-cli.js +291 -0
- package/dist/workspace/points-write.schema.json +660 -0
- package/dist/workspace/points.js +521 -0
- package/dist/workspace/points.schema.json +916 -0
- package/dist/workspace/pr-report.schema.json +108 -0
- package/dist/workspace/principal-classes.d.ts +4 -11
- package/dist/workspace/principal-classes.d.ts.map +1 -1
- package/dist/workspace/principal-classes.js +162 -0
- package/dist/workspace/reason-codes.js +326 -0
- package/dist/workspace/record-assets.js +151 -0
- package/dist/workspace/record-decided.js +105 -0
- package/dist/workspace/record-sessions.js +96 -0
- package/dist/workspace/record-source.js +114 -0
- package/dist/workspace/records-amend.schema.json +284 -0
- package/dist/workspace/records-checkout.js +76 -0
- package/dist/workspace/records-cli.js +707 -0
- package/dist/workspace/records-close.js +116 -0
- package/dist/workspace/records-close.schema.json +278 -0
- package/dist/workspace/records-new.schema.json +202 -0
- package/dist/workspace/records-review.schema.json +367 -0
- package/dist/workspace/records-since.js +317 -0
- package/dist/workspace/records-since.schema.json +218 -0
- package/dist/workspace/records-write.js +1229 -0
- package/dist/workspace/records.js +1345 -0
- package/dist/workspace/records.schema.json +604 -0
- package/dist/workspace/reference-kinds/app/package.json +10 -0
- package/dist/workspace/reference-kinds/app/workspace-kinds.json +45 -0
- package/dist/workspace/returns.js +303 -0
- package/dist/workspace/root-refusal.js +75 -0
- package/dist/workspace/run-statement.schema.json +622 -0
- package/dist/workspace/runs-cli.js +584 -0
- package/dist/workspace/runs-write.schema.json +994 -0
- package/dist/workspace/runs.js +708 -0
- package/dist/workspace/runs.schema.json +1257 -0
- package/dist/workspace/runtimes.js +76 -0
- package/dist/workspace/session-kinds.js +83 -0
- package/dist/workspace/signers.schema.json +206 -0
- package/dist/workspace/source-block.js +125 -0
- package/dist/workspace/squash.js +177 -0
- package/dist/workspace/status-gates.js +162 -0
- package/dist/workspace/status-stewards.js +330 -0
- package/dist/workspace/status.js +514 -0
- package/dist/workspace/status.schema.json +998 -0
- package/dist/workspace/steward-index.js +191 -0
- package/dist/workspace/symbols.js +188 -0
- package/dist/workspace/template-manifest.js +227 -0
- package/dist/workspace/template-pins.js +115 -0
- package/dist/workspace/trailer-joins.js +65 -0
- package/dist/workspace/trailers.js +111 -0
- package/dist/workspace/tree.js +136 -0
- package/dist/workspace/trust/attestor.js +63 -0
- package/dist/workspace/trust/dsse.js +108 -0
- package/dist/workspace/trust/evidence-cli.js +175 -0
- package/dist/workspace/trust/evidence.js +161 -0
- package/dist/workspace/trust/gate-seal.js +226 -0
- package/dist/workspace/trust/policy.js +190 -0
- package/dist/workspace/trust/provenance.js +180 -0
- package/dist/workspace/trust/rotation.js +298 -0
- package/dist/workspace/trust/run-commits.js +101 -0
- package/dist/workspace/trust/run-statement.js +182 -0
- package/dist/workspace/trust/seal.js +272 -0
- package/dist/workspace/trust/signers-cli.js +152 -0
- package/dist/workspace/trust/ssh-commit.js +178 -0
- package/dist/workspace/trust/test-repo.js +118 -0
- package/dist/workspace/trust/trust-files.js +193 -0
- package/dist/workspace/trust/verify-cli.js +63 -0
- package/dist/workspace/trust/verify.js +192 -0
- package/dist/workspace/which-chant.js +141 -0
- package/dist/workspace/wip-cli.js +95 -0
- package/dist/workspace/wip-write.schema.json +504 -0
- package/dist/workspace/wip.js +631 -0
- package/dist/workspace/wip.schema.json +334 -0
- package/dist/workspace/work-cli.js +448 -0
- package/dist/workspace/work-evidence.js +122 -0
- package/dist/workspace/work-evidence.schema.json +222 -0
- package/dist/workspace/work-history.schema.json +421 -0
- package/dist/workspace/work-lease.schema.json +246 -0
- package/dist/workspace/work-size.js +77 -0
- package/dist/workspace/work.js +283 -0
- package/dist/workspace/workspace-kinds.schema.json +259 -259
- package/dist/workspace/workspace-principals.schema.json +53 -53
- package/dist/workspace/write-lock-cli.js +105 -0
- package/dist/workspace/write-lock.js +424 -0
- package/dist/workspace/write-lock.schema.json +172 -0
- package/dist/workspace/write-scope.js +484 -0
- package/dist/yaml.js +459 -0
- package/package.json +2 -2
- package/src/build.ts +8 -0
- package/src/change-set.test.ts +48 -0
- package/src/change-set.ts +52 -2
- package/src/ci-jobs.test.ts +49 -0
- package/src/ci-jobs.ts +515 -0
- package/src/cli/commands/check-lexicon-plugin.ts +2 -0
- package/src/cli/commands/lint.ts +3 -0
- package/src/cli/commands/migrate.ts +2 -0
- package/src/cli/compiled-cli.e2e.test.ts +165 -0
- package/src/cli/main.ts +121 -27
- package/src/cli/mcp/workspace-tools.ts +1 -1
- package/src/codegen/pinned-upgrade.ts +2 -0
- package/src/components/discover.ts +2 -0
- package/src/components/pr-pipeline.ts +3 -2
- package/src/components/sandbox/driver.ts +3 -1
- package/src/composite.ts +15 -3
- package/src/config-import.ts +3 -0
- package/src/discovery/collect.ts +15 -1
- package/src/discovery/fold-build-parity.test.ts +71 -0
- package/src/discovery/fold-import.ts +25 -127
- package/src/discovery/fold-project-provenance.test.ts +21 -4
- package/src/discovery/host-composite.ts +101 -0
- package/src/discovery/import.ts +2 -0
- package/src/discovery/index.ts +11 -1
- package/src/discovery/sandbox/driver.ts +3 -1
- package/src/gate-decision-bundle.test.ts +5 -4
- package/src/gate-decision-identity.test.ts +59 -6
- package/src/gate-decision.ts +81 -21
- package/src/lexicon-module.ts +3 -0
- package/src/lifecycle/gate-audit.test.ts +55 -1
- package/src/lifecycle/gate-audit.ts +116 -3
- package/src/lifecycle/gate-ledger.test.ts +20 -1
- package/src/lifecycle/gate-ledger.ts +33 -1
- package/src/lint/config.ts +1 -1
- package/src/lint/discover.ts +28 -8
- package/src/lint/policy-import.ts +4 -1
- package/src/lint/rule-loader.ts +2 -0
- package/src/op/activities/activity-contracts.ts +1 -1
- package/src/op/activities/change-set.ts +11 -3
- package/src/op/activities/lexicon-upgrade.test.ts +2 -0
- package/src/op/activities/reconcile.test.ts +62 -2
- package/src/op/activities/reconcile.ts +46 -22
- package/src/op/builders.ts +1 -1
- package/src/op/discover.ts +3 -0
- package/src/op/op-waves-run.ts +4 -3
- package/src/op/op-waves.test.ts +2 -0
- package/src/op/work-lease-run.test.ts +12 -4
- package/src/ts-loader.test.ts +23 -0
- package/src/ts-loader.ts +122 -0
- package/src/workspace/__fixtures__/contract-repo.ts +17 -1
- package/src/workspace/__fixtures__/sessions.ts +3 -3
- package/src/workspace/change-set.schema.json +5 -3
- package/src/workspace/chant-migrations/chud-lexicon-exit.ts +1 -1
- package/src/workspace/ci-green-workflow.test.ts +4 -4
- package/src/workspace/ci-green-workflow.ts +5 -2
- package/src/workspace/class-data.test.ts +64 -0
- package/src/workspace/class-data.ts +236 -0
- package/src/workspace/declaration.ts +1 -1
- package/src/workspace/kinds.ts +7 -36
- package/src/workspace/lineage-migrations.ts +2 -0
- package/src/workspace/points.ts +1 -1
- package/src/workspace/principal-classes.ts +12 -95
- package/src/workspace/records-formats.test.ts +7 -2
- package/src/workspace/records-sessions-write.test.ts +3 -3
- package/src/workspace/status-contract.e2e.test.ts +37 -0
- package/src/workspace/status-contract.test.ts +23 -21
|
@@ -0,0 +1,1229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chant workspace records new|amend|review` (#2670): the commands a UI
|
|
3
|
+
* writes records through, so it never parses or writes a record file itself
|
|
4
|
+
* (ws-052).
|
|
5
|
+
*
|
|
6
|
+
* Each command loads the kind, reads every record it locates, builds the one
|
|
7
|
+
* file it would write, and reads the records again with that file in place,
|
|
8
|
+
* through the same {@link readRecords} a read uses. The write goes ahead only
|
|
9
|
+
* when the written record comes back valid and no other record gains a
|
|
10
|
+
* reason. So a write is refused for exactly what a later read would report,
|
|
11
|
+
* plus the rules only a write has: ids are allocated and never reused, a
|
|
12
|
+
* closed record never changes, an approved one changes only in place of
|
|
13
|
+
* what the approval rule allows, and a dissent needs a note.
|
|
14
|
+
*
|
|
15
|
+
* Every command writes one file or none, never commits, and prints one JSON
|
|
16
|
+
* document with a closed error code on refusal. `--dry-run` prints the
|
|
17
|
+
* document and the text it would write, and writes nothing. The exception is
|
|
18
|
+
* `review --session` (#2693), which appends the verdict to the open session
|
|
19
|
+
* as well, after checking both files, so the two lists never differ.
|
|
20
|
+
* `records close` is in `records-close.ts`.
|
|
21
|
+
*
|
|
22
|
+
* Each write holds the working tree's write lock (`write-lock.ts`, #3173,
|
|
23
|
+
* ws-089) from its first read to its last write, so writes to one working
|
|
24
|
+
* tree never interleave. `amend`, `review` and `close` take `--expect
|
|
25
|
+
* <digest>`: the record's digest the caller last read. When the record has
|
|
26
|
+
* moved on since, the write is refused with record-conflict, naming the
|
|
27
|
+
* digest it has now and the last chant write of it, and the caller re-reads
|
|
28
|
+
* and tries again.
|
|
29
|
+
*
|
|
30
|
+
* `new --sign` and `amend --sign` seal the record's author (#2688), and
|
|
31
|
+
* `review --sign` seals a verdict (#2687): see `trust/seal.ts`. A record of
|
|
32
|
+
* a kind that declares `seal` gets its whole-file seal when `new` or `amend`
|
|
33
|
+
* writes it into a closed state (#2546, ws-063).
|
|
34
|
+
*
|
|
35
|
+
* `new` also writes a record of a content-addressed JSON kind (`format:
|
|
36
|
+
* "json"`, `idFrom: "sha256"`, ws-053), such as the reference workspace's
|
|
37
|
+
* evidence (ws-082): the fields as JSON, in a file named for the SHA-256 of
|
|
38
|
+
* its bytes. Such a record never changes, so amend, review and close refuse
|
|
39
|
+
* the kind.
|
|
40
|
+
*/
|
|
41
|
+
import { createHash } from "node:crypto";
|
|
42
|
+
import { readFileSync, statSync, writeFileSync } from "node:fs";
|
|
43
|
+
import { join, posix, relative, resolve } from "node:path";
|
|
44
|
+
import { gitRoot, workingTreeSource } from "./record-source.js";
|
|
45
|
+
import { computeQuorum, loadRecordKind, parseFrontMatter, readRecords, readSessionSubjects, RECORD_REASON_CODES, RecordReadError, RECORD_SEAL_FIELD, digestFields, recordSeal, recordTextDigest, } from "./records.js";
|
|
46
|
+
import { declaredKindFiles, pinRoot, quorumOptionsFor, realpathOr } from "./records-cli.js";
|
|
47
|
+
import { policyAtBase, resolveBase } from "./trust/provenance.js";
|
|
48
|
+
import { WorkspaceReadError } from "./declaration.js";
|
|
49
|
+
import { findSessionKinds, headCommit, sessionKindsFor } from "./session-kinds.js";
|
|
50
|
+
import { workingTree } from "./tree.js";
|
|
51
|
+
import { AGENT_ENV, refuseRecordWrite, WRITE_SCOPE_CODES, WriteScopeError } from "./write-scope.js";
|
|
52
|
+
import { IDENTITY_CODES, IdentityError, refuseUnidentified } from "./identity.js";
|
|
53
|
+
import { lastWriteReader, noteWrites, withWriteLock, writeFileAtomic, WriteLockError, WRITE_LOCK_CODES } from "./write-lock.js";
|
|
54
|
+
// ── Contract ─────────────────────────────────────────────────────────────────
|
|
55
|
+
/** The version of the write documents this chant prints. */
|
|
56
|
+
export const RECORDS_WRITE_CONTRACT_VERSION = 1;
|
|
57
|
+
export const RECORDS_NEW_SCHEMA_ID = "https://intentius.io/chant/schemas/workspace/records-new/v1/records-new.schema.json";
|
|
58
|
+
export const RECORDS_AMEND_SCHEMA_ID = "https://intentius.io/chant/schemas/workspace/records-amend/v1/records-amend.schema.json";
|
|
59
|
+
export const RECORDS_REVIEW_SCHEMA_ID = "https://intentius.io/chant/schemas/workspace/records-review/v1/records-review.schema.json";
|
|
60
|
+
/** Loading the kind and reading its records, as `records` reads them. */
|
|
61
|
+
export const LOAD_ERROR_CODES = ["kind-unreadable", "kind-invalid", "schema-unreadable", "schema-id-mismatch", "schema-invalid", "location-missing"];
|
|
62
|
+
/** Why `records new` wrote nothing. Closed: a reader may switch on it. */
|
|
63
|
+
export const NEW_ERROR_CODES = [
|
|
64
|
+
...LOAD_ERROR_CODES,
|
|
65
|
+
"write-usage-invalid",
|
|
66
|
+
"write-input-invalid",
|
|
67
|
+
"record-id-taken",
|
|
68
|
+
"record-id-unallocatable",
|
|
69
|
+
"record-path-unmatched",
|
|
70
|
+
"record-sign-failed",
|
|
71
|
+
"source-harvest-not-proposed",
|
|
72
|
+
"record-state-not-initial",
|
|
73
|
+
"ratify-quorum-not-met",
|
|
74
|
+
...WRITE_LOCK_CODES,
|
|
75
|
+
...WRITE_SCOPE_CODES,
|
|
76
|
+
...IDENTITY_CODES,
|
|
77
|
+
...RECORD_REASON_CODES,
|
|
78
|
+
];
|
|
79
|
+
/** Why `records amend` wrote nothing. */
|
|
80
|
+
export const AMEND_ERROR_CODES = [
|
|
81
|
+
...LOAD_ERROR_CODES,
|
|
82
|
+
"write-usage-invalid",
|
|
83
|
+
"write-input-invalid",
|
|
84
|
+
"record-not-found",
|
|
85
|
+
"amend-id-immutable",
|
|
86
|
+
"record-closed",
|
|
87
|
+
"amend-supersede-instead",
|
|
88
|
+
"record-sign-failed",
|
|
89
|
+
"ratify-quorum-not-met",
|
|
90
|
+
"record-conflict",
|
|
91
|
+
...WRITE_LOCK_CODES,
|
|
92
|
+
...WRITE_SCOPE_CODES,
|
|
93
|
+
...IDENTITY_CODES,
|
|
94
|
+
...RECORD_REASON_CODES,
|
|
95
|
+
];
|
|
96
|
+
/** Why `records review` wrote nothing. */
|
|
97
|
+
export const REVIEW_ERROR_CODES = [
|
|
98
|
+
...LOAD_ERROR_CODES,
|
|
99
|
+
"write-usage-invalid",
|
|
100
|
+
"record-not-found",
|
|
101
|
+
"review-unsupported",
|
|
102
|
+
"record-closed",
|
|
103
|
+
"review-note-required",
|
|
104
|
+
"review-sign-failed",
|
|
105
|
+
"session-unknown",
|
|
106
|
+
"session-not-open",
|
|
107
|
+
"record-conflict",
|
|
108
|
+
...WRITE_LOCK_CODES,
|
|
109
|
+
...WRITE_SCOPE_CODES,
|
|
110
|
+
...IDENTITY_CODES,
|
|
111
|
+
...RECORD_REASON_CODES,
|
|
112
|
+
];
|
|
113
|
+
export const VERDICTS = ["agree", "dissent", "abstain"];
|
|
114
|
+
export class RecordWriteError extends Error {
|
|
115
|
+
constructor(code, message,
|
|
116
|
+
/** With record-conflict: the record as it is now. */
|
|
117
|
+
conflict) {
|
|
118
|
+
super(message);
|
|
119
|
+
this.code = code;
|
|
120
|
+
this.conflict = conflict;
|
|
121
|
+
this.name = "RecordWriteError";
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// ── Rendering ────────────────────────────────────────────────────────────────
|
|
125
|
+
const PLAIN_KEY = /^[A-Za-z_][A-Za-z0-9_-]*$/;
|
|
126
|
+
const RESERVED_KEY = new Set(["true", "false", "null"]);
|
|
127
|
+
function yamlKey(k) {
|
|
128
|
+
return PLAIN_KEY.test(k) && !RESERVED_KEY.has(k) ? k : JSON.stringify(k);
|
|
129
|
+
}
|
|
130
|
+
function isScalar(v) {
|
|
131
|
+
return v === null || typeof v !== "object";
|
|
132
|
+
}
|
|
133
|
+
function isEmpty(v) {
|
|
134
|
+
return Array.isArray(v) ? v.length === 0 : v !== null && typeof v === "object" && Object.keys(v).length === 0;
|
|
135
|
+
}
|
|
136
|
+
const scalar = (v) => (v === null ? "null" : JSON.stringify(v));
|
|
137
|
+
const empty = (v) => (Array.isArray(v) ? "[]" : "{}");
|
|
138
|
+
/**
|
|
139
|
+
* YAML limited to what JSON can say, laid out as the decision README writes a
|
|
140
|
+
* record: every string double-quoted with JSON escapes, block mappings and
|
|
141
|
+
* sequences, and `[]` or `{}` for an empty one. `parseFrontMatter` reads it
|
|
142
|
+
* back to the same value.
|
|
143
|
+
*/
|
|
144
|
+
export function toYaml(value, indent = 0) {
|
|
145
|
+
const pad = " ".repeat(indent);
|
|
146
|
+
if (Array.isArray(value)) {
|
|
147
|
+
if (value.length === 0)
|
|
148
|
+
return `${pad}[]`;
|
|
149
|
+
return value
|
|
150
|
+
.map((item) => {
|
|
151
|
+
if (isScalar(item))
|
|
152
|
+
return `${pad}- ${scalar(item)}`;
|
|
153
|
+
if (isEmpty(item))
|
|
154
|
+
return `${pad}- ${empty(item)}`;
|
|
155
|
+
if (Array.isArray(item))
|
|
156
|
+
return `${pad}-\n${toYaml(item, indent + 2)}`;
|
|
157
|
+
return `${pad}- ${toYaml(item, indent + 2).slice(indent + 2)}`;
|
|
158
|
+
})
|
|
159
|
+
.join("\n");
|
|
160
|
+
}
|
|
161
|
+
if (isScalar(value))
|
|
162
|
+
return `${pad}${scalar(value)}`;
|
|
163
|
+
const lines = [];
|
|
164
|
+
for (const [k, v] of Object.entries(value)) {
|
|
165
|
+
if (isScalar(v))
|
|
166
|
+
lines.push(`${pad}${yamlKey(k)}: ${scalar(v)}`);
|
|
167
|
+
else if (isEmpty(v))
|
|
168
|
+
lines.push(`${pad}${yamlKey(k)}: ${empty(v)}`);
|
|
169
|
+
else
|
|
170
|
+
lines.push(`${pad}${yamlKey(k)}:\n${toYaml(v, indent + 2)}`);
|
|
171
|
+
}
|
|
172
|
+
return lines.join("\n");
|
|
173
|
+
}
|
|
174
|
+
/** A record file: the front matter, then `body` as it is. */
|
|
175
|
+
export function renderRecord(data, body) {
|
|
176
|
+
return `---\n${toYaml(data)}\n---\n${body}`;
|
|
177
|
+
}
|
|
178
|
+
/** The text below a file's front matter, line endings normalised. */
|
|
179
|
+
function bodyOf(text) {
|
|
180
|
+
const normalised = text.replace(/\r\n?/g, "\n");
|
|
181
|
+
const m = normalised.match(/^---\n[\s\S]*?\n---(?:\n|$)/);
|
|
182
|
+
return m ? normalised.slice(m[0].length) : "";
|
|
183
|
+
}
|
|
184
|
+
/** JSON with object keys sorted, for comparing two values whatever their key order. */
|
|
185
|
+
export function stableJson(value) {
|
|
186
|
+
if (value === null || typeof value !== "object")
|
|
187
|
+
return JSON.stringify(value);
|
|
188
|
+
if (Array.isArray(value))
|
|
189
|
+
return `[${value.map(stableJson).join(",")}]`;
|
|
190
|
+
const obj = value;
|
|
191
|
+
return `{${Object.keys(obj)
|
|
192
|
+
.sort()
|
|
193
|
+
.map((k) => `${JSON.stringify(k)}:${stableJson(obj[k])}`)
|
|
194
|
+
.join(",")}}`;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* `text` with the top-level fields in `set` replaced in place, and every
|
|
198
|
+
* other byte kept. A field's block is its key line at column 0 and the lines
|
|
199
|
+
* after it, up to the closing `---`, that start with a space, a tab, `#` or
|
|
200
|
+
* `-`: the block {@link recordTextDigest} cuts for the reviews field. Blank
|
|
201
|
+
* lines ending a block stay where they are. A field the front matter lacks is
|
|
202
|
+
* added before the closing `---`. Line endings become LF. Returns undefined
|
|
203
|
+
* when the text has no front matter, or when the result does not read back
|
|
204
|
+
* as `expected`, so a caller never writes a file it did not mean to.
|
|
205
|
+
*/
|
|
206
|
+
export function replaceFields(text, set, expected) {
|
|
207
|
+
const lines = text.replace(/\r\n?/g, "\n").split("\n");
|
|
208
|
+
if (lines[0] !== "---")
|
|
209
|
+
return undefined;
|
|
210
|
+
let close = lines.indexOf("---", 1);
|
|
211
|
+
if (close < 0)
|
|
212
|
+
return undefined;
|
|
213
|
+
for (const [key, value] of Object.entries(set)) {
|
|
214
|
+
const rendered = toYaml({ [key]: value }).split("\n");
|
|
215
|
+
const k = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
216
|
+
const starts = new RegExp(`^(?:${k}|"${k}"|'${k}')[ \\t]*:(?:[ \\t]|$)`);
|
|
217
|
+
const at = lines.findIndex((l, i) => i > 0 && i < close && starts.test(l));
|
|
218
|
+
if (at < 0) {
|
|
219
|
+
lines.splice(close, 0, ...rendered);
|
|
220
|
+
close += rendered.length;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
let end = at;
|
|
224
|
+
while (end + 1 < close && /^(?:$|[ \t#-])/.test(lines[end + 1]))
|
|
225
|
+
end++;
|
|
226
|
+
while (end > at && lines[end] === "")
|
|
227
|
+
end--;
|
|
228
|
+
lines.splice(at, end - at + 1, ...rendered);
|
|
229
|
+
close += rendered.length - (end - at + 1);
|
|
230
|
+
}
|
|
231
|
+
const out = lines.join("\n");
|
|
232
|
+
const back = parseFrontMatter(out);
|
|
233
|
+
return back.ok && stableJson(back.value) === stableJson(expected) ? out : undefined;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* `text` with the top-level field `key` removed: its key line and the lines
|
|
237
|
+
* after it that {@link replaceFields} counts as its block, less the blank
|
|
238
|
+
* lines ending it, which stay. Returns undefined when the result does not
|
|
239
|
+
* read back as `expected`.
|
|
240
|
+
*/
|
|
241
|
+
export function removeField(text, key, expected) {
|
|
242
|
+
const lines = text.replace(/\r\n?/g, "\n").split("\n");
|
|
243
|
+
if (lines[0] !== "---")
|
|
244
|
+
return undefined;
|
|
245
|
+
const close = lines.indexOf("---", 1);
|
|
246
|
+
if (close < 0)
|
|
247
|
+
return undefined;
|
|
248
|
+
const k = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
249
|
+
const starts = new RegExp(`^(?:${k}|"${k}"|'${k}')[ \\t]*:(?:[ \\t]|$)`);
|
|
250
|
+
const at = lines.findIndex((l, i) => i > 0 && i < close && starts.test(l));
|
|
251
|
+
if (at >= 0) {
|
|
252
|
+
let end = at;
|
|
253
|
+
while (end + 1 < close && /^(?:$|[ \t#-])/.test(lines[end + 1]))
|
|
254
|
+
end++;
|
|
255
|
+
while (end > at && lines[end] === "")
|
|
256
|
+
end--;
|
|
257
|
+
lines.splice(at, end - at + 1);
|
|
258
|
+
}
|
|
259
|
+
const out = lines.join("\n");
|
|
260
|
+
const back = parseFrontMatter(out);
|
|
261
|
+
return back.ok && stableJson(back.value) === stableJson(expected) ? out : undefined;
|
|
262
|
+
}
|
|
263
|
+
/** A file-name slug from a title, as `scripts/import-decisions.mjs` makes one. */
|
|
264
|
+
export function slug(title) {
|
|
265
|
+
return title
|
|
266
|
+
.toLowerCase()
|
|
267
|
+
.replace(/`/g, "")
|
|
268
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
269
|
+
.replace(/^-|-$/g, "")
|
|
270
|
+
.slice(0, 48)
|
|
271
|
+
.replace(/-$/, "");
|
|
272
|
+
}
|
|
273
|
+
/** A kind `records new` writes as one JSON file named for the SHA-256 of its bytes (ws-053, ws-082). */
|
|
274
|
+
export function isContentAddressed(kind) {
|
|
275
|
+
return kind.format === "json" && kind.idFrom === "sha256";
|
|
276
|
+
}
|
|
277
|
+
export async function open(kind, cwd, opts = {}) {
|
|
278
|
+
const real = realpathOr(cwd);
|
|
279
|
+
const root = gitRoot(real) ?? real;
|
|
280
|
+
const loaded = await loadRecordKind(kind, real);
|
|
281
|
+
// The writers write Markdown front matter with an id field. `records new`
|
|
282
|
+
// also writes a content-addressed JSON kind (ws-082), whose record never
|
|
283
|
+
// changes once written, so amend, review and close never open one. A JSON
|
|
284
|
+
// kind with an id field is read by records, and written by its own tool.
|
|
285
|
+
const writable = (loaded.kind.format === "markdown-front-matter" && loaded.kind.idField !== undefined) || (!!opts.contentAddressed && isContentAddressed(loaded.kind));
|
|
286
|
+
if (!writable) {
|
|
287
|
+
const what = loaded.kind.format !== "markdown-front-matter" ? `format ${loaded.kind.format}` : `ids from ${loaded.kind.idFrom}`;
|
|
288
|
+
const verbs = isContentAddressed(loaded.kind)
|
|
289
|
+
? "a content-addressed record never changes once records new writes it, so amend, review and close never write one"
|
|
290
|
+
: "records new, amend and review write only Markdown front matter records with an idField, and records new a JSON record named for the sha256 of its bytes";
|
|
291
|
+
throw new RecordWriteError("write-usage-invalid", `the ${loaded.kind.name} kind has ${what}: ${verbs}`);
|
|
292
|
+
}
|
|
293
|
+
const dirRel = relative(root, loaded.dir).split("\\").join("/") || ".";
|
|
294
|
+
return {
|
|
295
|
+
loaded,
|
|
296
|
+
root,
|
|
297
|
+
workspaceRoot: pinRoot(loaded.file, root),
|
|
298
|
+
source: workingTreeSource(root),
|
|
299
|
+
view: { name: loaded.kind.name, schema: loaded.kind.schema.id, file: relative(root, loaded.file).split("\\").join("/") },
|
|
300
|
+
dirRel,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Refuse a write outside the writer's scope (#2548): the kind `o` opened,
|
|
305
|
+
* written with `verb`, by the session `agent` names (`CHANT_AGENT`) or the
|
|
306
|
+
* principal `by` names. Throws a {@link WriteScopeError}.
|
|
307
|
+
*/
|
|
308
|
+
export function refuseOutOfScope(o, verb, cwd, writer) {
|
|
309
|
+
const source = refuseRecordWrite(cwd, { kindName: o.loaded.kind.name, kindFile: o.loaded.file, recordsDir: o.loaded.dir, verb, agent: writer.agent, principal: writer.by });
|
|
310
|
+
// #3163: under identity.attribution "identified" at base, --by names a forge identity or a signer.
|
|
311
|
+
if (writer.by !== undefined)
|
|
312
|
+
refuseUnidentified(source, [writer.by], "--by", { agent: writer.agent });
|
|
313
|
+
return source;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Refuse author fields (the kind's reviews.decider and proposedBy fields)
|
|
317
|
+
* that name a person by a bare name, under identity.attribution
|
|
318
|
+
* "identified" at base (#3163). `flag` is where the fields came from.
|
|
319
|
+
*/
|
|
320
|
+
function refuseUnidentifiedAuthors(source, kind, fields, flag, agent) {
|
|
321
|
+
const names = [kind.reviews?.decider, kind.proposedBy?.field]
|
|
322
|
+
.filter((f) => f !== undefined)
|
|
323
|
+
.map((f) => fields[f])
|
|
324
|
+
.filter((v) => typeof v === "string" && v.trim() !== "");
|
|
325
|
+
refuseUnidentified(source, names, `${flag} author`, { agent });
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* The kind's records as `records` reads them from `source`. A session kind's
|
|
329
|
+
* subject records are read from the same source (#2693), so a write is
|
|
330
|
+
* refused for a verdict naming a record that does not exist.
|
|
331
|
+
*/
|
|
332
|
+
export async function readAll(o, source) {
|
|
333
|
+
const assets = workingTree(o.workspaceRoot === "." ? o.root : join(o.root, ...o.workspaceRoot.split("/")));
|
|
334
|
+
const subjects = await readSessionSubjects(o.loaded, o.root, source);
|
|
335
|
+
return (await readRecords(o.loaded, { root: o.root, source, assets, workspaceRoot: o.workspaceRoot, ...(subjects ? { subjects } : {}) })).records;
|
|
336
|
+
}
|
|
337
|
+
/** `base` with the file at `path` holding `text`, added to its directory when new. */
|
|
338
|
+
export function overlay(base, path, text) {
|
|
339
|
+
const dir = posix.dirname(path);
|
|
340
|
+
const name = posix.basename(path);
|
|
341
|
+
return {
|
|
342
|
+
label: base.label,
|
|
343
|
+
list(d) {
|
|
344
|
+
const names = base.list(d);
|
|
345
|
+
if (d !== dir || !names)
|
|
346
|
+
return names;
|
|
347
|
+
return names.includes(name) ? names : [...names, name];
|
|
348
|
+
},
|
|
349
|
+
read(p) {
|
|
350
|
+
return p === path ? text : base.read(p);
|
|
351
|
+
},
|
|
352
|
+
bytes(p) {
|
|
353
|
+
return p === path ? Buffer.from(text, "utf-8") : base.bytes(p);
|
|
354
|
+
},
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Read the records again with `text` at `path`, over `base` (the working
|
|
359
|
+
* tree unless another write goes with this one). The written record must come
|
|
360
|
+
* back with no reason, and no other record may gain one. Returns the written
|
|
361
|
+
* record's warnings.
|
|
362
|
+
*/
|
|
363
|
+
export async function validateWrite(o, before, path, text, base = o.source) {
|
|
364
|
+
return (await validatedEntry(o, before, path, text, base)).warnings;
|
|
365
|
+
}
|
|
366
|
+
/** {@link validateWrite}, returning the written record as the read found it. */
|
|
367
|
+
export async function validatedEntry(o, before, path, text, base = o.source) {
|
|
368
|
+
const after = await readAll(o, overlay(base, path, text));
|
|
369
|
+
const written = after.find((e) => e.path === path);
|
|
370
|
+
if (!written)
|
|
371
|
+
throw new RecordWriteError("record-path-unmatched", `${path} is not a file the kind ${o.view.name} reads`);
|
|
372
|
+
if (written.reasons.length > 0) {
|
|
373
|
+
throw new RecordWriteError(written.reasons[0].code, written.reasons.map((r) => `${r.code}: ${r.message}`).join("; "));
|
|
374
|
+
}
|
|
375
|
+
const had = new Map(before.map((e) => [e.path, new Set(e.reasons.map((r) => `${r.code}\0${r.message}`))]));
|
|
376
|
+
for (const e of after) {
|
|
377
|
+
if (e.path === path)
|
|
378
|
+
continue;
|
|
379
|
+
const gained = e.reasons.find((r) => !had.get(e.path)?.has(`${r.code}\0${r.message}`));
|
|
380
|
+
if (gained)
|
|
381
|
+
throw new RecordWriteError(gained.code, `writing ${path} would make ${e.path} invalid: ${gained.message}`);
|
|
382
|
+
}
|
|
383
|
+
return written;
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Refuse a written record in the kind's ratified state (`reviews.ratified`,
|
|
387
|
+
* #2873) whose quorum is not met. The quorum is computed on the record as
|
|
388
|
+
* written, the way `records --json` computes it: the need from the
|
|
389
|
+
* declaration in the working tree, and agents and seals from the policy at
|
|
390
|
+
* base. A met quorum with open concerns is met, and the concerns stay listed.
|
|
391
|
+
*/
|
|
392
|
+
async function refuseRatifyBelowQuorum(o, written, id) {
|
|
393
|
+
const ratified = o.loaded.kind.reviews?.ratified;
|
|
394
|
+
if (ratified === undefined || written.state !== ratified)
|
|
395
|
+
return;
|
|
396
|
+
const top = gitRoot(o.root);
|
|
397
|
+
const policy = policyAtBase(top ?? o.root, top ? resolveBase(top) : { commit: null, from: null });
|
|
398
|
+
const tree = workingTree(o.workspaceRoot === "." ? o.root : join(o.root, ...o.workspaceRoot.split("/")));
|
|
399
|
+
const options = await quorumOptionsFor(o.loaded, tree, policy);
|
|
400
|
+
const q = options ? computeQuorum(o.loaded.kind, written, options) : null;
|
|
401
|
+
if (q === null || q.met)
|
|
402
|
+
return;
|
|
403
|
+
const notCounted = q.notCounted.length > 0 ? ` (not counted: ${q.notCounted.map((v) => `${v.reviewer}, ${v.reason.code}`).join("; ")})` : "";
|
|
404
|
+
throw new RecordWriteError("ratify-quorum-not-met", `${id} can't be ${ratified} yet: its quorum needs ${q.need} agreeing verdicts that count, and it has ${q.agreed}${notCounted}. Add verdicts with chant workspace records review ${id} --verdict agree --by <reviewer>, then set ${o.loaded.kind.stateField} to ${ratified} again`);
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Refuse a write to `target` when the caller expected another digest
|
|
408
|
+
* (#3173): someone wrote the record since the caller read it. Called holding
|
|
409
|
+
* the write lock, after the records are read, so the digest compared is the
|
|
410
|
+
* one the write would build on.
|
|
411
|
+
*/
|
|
412
|
+
export function refuseStale(o, target, id, expect) {
|
|
413
|
+
if (expect === undefined || expect === target.digest)
|
|
414
|
+
return;
|
|
415
|
+
const lastWrite = lastWriteReader(o.root)(target.path, o.source.read(target.path));
|
|
416
|
+
const who = lastWrite ? ` It was last written by chant workspace ${lastWrite.verb}${lastWrite.by ? ` for ${lastWrite.by}` : ""}${lastWrite.agent ? ` in agent session ${lastWrite.agent}` : ""} at ${lastWrite.at}.` : "";
|
|
417
|
+
throw new RecordWriteError("record-conflict", `${id} has changed since the digest given with --expect (${expect}): its digest is now ${target.digest}.${who} Read it again with chant workspace records --json, and write again with --expect ${target.digest} if the change still stands`, { id, path: target.path, expected: expect, digest: target.digest, lastWrite });
|
|
418
|
+
}
|
|
419
|
+
/** Refuse a --expect that is not a digest, before anything is read. */
|
|
420
|
+
function refuseExpectShape(expect) {
|
|
421
|
+
if (expect !== undefined && !/^[0-9a-f]{64}$/.test(expect)) {
|
|
422
|
+
throw new RecordWriteError("write-usage-invalid", `--expect takes a record's digest, 64 lowercase hex digits as records --json prints it, not ${JSON.stringify(expect)}`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
/** Who a write names, for the lock's owner file and the journal. */
|
|
426
|
+
export function writeWho(verb, opts) {
|
|
427
|
+
return { verb, by: opts.by ?? null, agent: opts.agent ?? null };
|
|
428
|
+
}
|
|
429
|
+
export function findRecord(entries, id, kind) {
|
|
430
|
+
const hits = entries.filter((e) => e.id === id);
|
|
431
|
+
if (hits.length === 0)
|
|
432
|
+
throw new RecordWriteError("record-not-found", `no ${kind} record has id ${id}`);
|
|
433
|
+
if (hits.length > 1) {
|
|
434
|
+
throw new RecordWriteError("record-id-duplicate", `id ${id} is used by ${hits.map((h) => h.path).join(" and ")}; make the ids unique before writing either`);
|
|
435
|
+
}
|
|
436
|
+
return hits[0];
|
|
437
|
+
}
|
|
438
|
+
function parseFields(text, flag) {
|
|
439
|
+
let value;
|
|
440
|
+
try {
|
|
441
|
+
value = JSON.parse(text);
|
|
442
|
+
}
|
|
443
|
+
catch (err) {
|
|
444
|
+
throw new RecordWriteError("write-input-invalid", `the fields given with ${flag} are not JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
445
|
+
}
|
|
446
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
447
|
+
throw new RecordWriteError("write-input-invalid", `the fields given with ${flag} must be a JSON object`);
|
|
448
|
+
}
|
|
449
|
+
return value;
|
|
450
|
+
}
|
|
451
|
+
/** Refuse fields that set the author seal by hand: only --sign writes it (#2688). */
|
|
452
|
+
function refuseSealField(o, fields, flag) {
|
|
453
|
+
if (o.loaded.kind.reviews && RECORD_SEAL_FIELD in fields) {
|
|
454
|
+
throw new RecordWriteError("write-input-invalid", `the fields given with ${flag} set ${RECORD_SEAL_FIELD}, and only --sign writes a record's seal`);
|
|
455
|
+
}
|
|
456
|
+
const closed = o.loaded.kind.seal?.field;
|
|
457
|
+
if (closed !== undefined && !o.loaded.kind.session && closed in fields) {
|
|
458
|
+
throw new RecordWriteError("write-input-invalid", `the fields given with ${flag} set ${closed}, and chant writes it when the record closes`);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* `text`, the record `data` holds, with its whole-file seal written into the
|
|
463
|
+
* kind's `seal.field` when the record is in a closed state (#2546, ws-063).
|
|
464
|
+
* The seal goes last, after the author seal, so it covers every other value
|
|
465
|
+
* and the body. `text` is returned as it is for a kind without `seal`, a
|
|
466
|
+
* session kind (whose seal `records close` writes), or an open record.
|
|
467
|
+
*/
|
|
468
|
+
function sealClosed(o, text, data, id) {
|
|
469
|
+
const { kind } = o.loaded;
|
|
470
|
+
if (!kind.seal || kind.session || kind.stateField === undefined)
|
|
471
|
+
return { text, data };
|
|
472
|
+
const state = data[kind.stateField];
|
|
473
|
+
if (typeof state !== "string" || !(kind.closedStates ?? []).includes(state))
|
|
474
|
+
return { text, data };
|
|
475
|
+
const field = kind.seal.field;
|
|
476
|
+
const seal = recordSeal(text, field, kind.format);
|
|
477
|
+
if (seal === null)
|
|
478
|
+
throw new RecordWriteError("record-unparseable", `${id} can't be read, so it can't be sealed`);
|
|
479
|
+
const sealedData = { ...data, [field]: seal };
|
|
480
|
+
const sealed = replaceFields(text, { [field]: seal }, sealedData);
|
|
481
|
+
if (sealed === undefined || recordSeal(sealed, field, kind.format) !== seal) {
|
|
482
|
+
throw new RecordWriteError("record-unparseable", `the ${field} field can't be written into ${id} without changing the text it seals`);
|
|
483
|
+
}
|
|
484
|
+
return { text: sealed, data: sealedData };
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Refuse fields that set a session's opening or closing revision by hand
|
|
488
|
+
* (#2693): `new` writes the first from HEAD and `close` the second. A field
|
|
489
|
+
* given with the value the record already holds is not a change, so an
|
|
490
|
+
* amendment that sends the whole record back is taken.
|
|
491
|
+
*/
|
|
492
|
+
function refuseRevisionFields(kind, fields, old, flag) {
|
|
493
|
+
for (const f of [kind.session?.openedRev, kind.session?.closedRev]) {
|
|
494
|
+
if (f === undefined || !(f in fields) || (f in old && stableJson(old[f]) === stableJson(fields[f])))
|
|
495
|
+
continue;
|
|
496
|
+
throw new RecordWriteError("write-input-invalid", `the fields given with ${flag} set ${f}, and chant writes it: records new writes the commit a session opened at, and records close the one it closed at`);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Refuse a harvested record in any state but the kind's first (#2708): a
|
|
501
|
+
* record whose source block says `via: "harvest"` came out of a transcript
|
|
502
|
+
* after the fact, so it is a proposal, and a person decides it.
|
|
503
|
+
*/
|
|
504
|
+
function refuseHarvestedDecision(kind, fields) {
|
|
505
|
+
if (!kind.source || !kind.states || kind.stateField === undefined)
|
|
506
|
+
return;
|
|
507
|
+
const block = fields[kind.source.field];
|
|
508
|
+
if (block === null || typeof block !== "object" || Array.isArray(block) || block.via !== "harvest")
|
|
509
|
+
return;
|
|
510
|
+
const opens = kind.states[0];
|
|
511
|
+
const state = fields[kind.stateField];
|
|
512
|
+
if (state === opens)
|
|
513
|
+
return;
|
|
514
|
+
throw new RecordWriteError("source-harvest-not-proposed", `${kind.source.field}.via is harvest, and a harvested ${kind.name} opens ${opens}, not ${JSON.stringify(state ?? null)}: write it with ${kind.stateField} "${opens}", and let the person who decides it move it on with chant workspace records amend or review`);
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* `text`, the record `data` holds, with its author sealed (#2688): an ssh
|
|
518
|
+
* signature by the key `sign` names over the record id, the digest of
|
|
519
|
+
* `text`, the author (the kind's `reviews.decider` field) and the state, in
|
|
520
|
+
* the `chant-record` namespace. The seal goes in the top-level `seal` field,
|
|
521
|
+
* replacing one already there, or added at the end of the front matter. The
|
|
522
|
+
* digest leaves that field out, so it is the same before and after.
|
|
523
|
+
*/
|
|
524
|
+
async function sealAuthor(o, text, data, id, sign, cwd) {
|
|
525
|
+
const { kind } = o.loaded;
|
|
526
|
+
if (!kind.reviews) {
|
|
527
|
+
throw new RecordWriteError("write-usage-invalid", `--sign seals a record's author, named by the kind's reviews.decider field, and the ${kind.name} kind declares no reviews`);
|
|
528
|
+
}
|
|
529
|
+
const field = kind.reviews.decider;
|
|
530
|
+
const author = data[field];
|
|
531
|
+
if (typeof author !== "string" || author.trim() === "") {
|
|
532
|
+
throw new RecordWriteError("record-sign-failed", `${id} names no ${field}, so there is no author to seal: set ${field}, then seal it with records amend ${id} --sign`);
|
|
533
|
+
}
|
|
534
|
+
const state = kind.stateField !== undefined && typeof data[kind.stateField] === "string" ? data[kind.stateField] : null;
|
|
535
|
+
const digest = recordTextDigest(text, digestFields(kind), kind.format);
|
|
536
|
+
const { resolveSigningKey, sealRecord, SealError } = await import("./trust/seal.js");
|
|
537
|
+
let seal;
|
|
538
|
+
try {
|
|
539
|
+
const key = resolveSigningKey(sign, cwd);
|
|
540
|
+
try {
|
|
541
|
+
seal = { ...sealRecord(key.file, { record: id, digest, author, state }) };
|
|
542
|
+
}
|
|
543
|
+
finally {
|
|
544
|
+
key.cleanup();
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
catch (err) {
|
|
548
|
+
if (err instanceof SealError)
|
|
549
|
+
throw new RecordWriteError("record-sign-failed", err.message);
|
|
550
|
+
throw err;
|
|
551
|
+
}
|
|
552
|
+
const sealed = replaceFields(text, { [RECORD_SEAL_FIELD]: seal }, { ...data, [RECORD_SEAL_FIELD]: seal });
|
|
553
|
+
if (sealed === undefined)
|
|
554
|
+
throw new RecordWriteError("record-unparseable", `the ${RECORD_SEAL_FIELD} field can't be written into ${id} without changing the rest of the file`);
|
|
555
|
+
if (recordTextDigest(sealed, digestFields(kind), kind.format) !== digest) {
|
|
556
|
+
throw new Error(`sealing ${id} moved its digest; the seal block and the digest rule disagree`);
|
|
557
|
+
}
|
|
558
|
+
return { text: sealed, seal };
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* What to do instead of changing a closed record. A supersedes link is
|
|
562
|
+
* advice only when the kind's schema has the field: the session schema has
|
|
563
|
+
* none, so a closed session is followed by a new session (#2693).
|
|
564
|
+
*/
|
|
565
|
+
function closedAdvice(o, id, supersede) {
|
|
566
|
+
const { kind, schema } = o.loaded;
|
|
567
|
+
if (kind.session)
|
|
568
|
+
return `A closed session is sealed and stays as it is: open a new session for what follows, with chant workspace records new ${o.view.file}`;
|
|
569
|
+
const props = schema.properties !== null && typeof schema.properties === "object" ? schema.properties : {};
|
|
570
|
+
if (kind.supersedes && (schema.additionalProperties !== false || kind.supersedes.field in props))
|
|
571
|
+
return `Write a new record that supersedes it: ${supersede}`;
|
|
572
|
+
return `Write a new record in its place with chant workspace records new ${o.view.file}; the ${kind.name} schema has no field that links it to ${id}`;
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* The opening revision a session lacks (#2693): when the kind names one and
|
|
576
|
+
* the record holds null there, HEAD, once the repository has a commit.
|
|
577
|
+
* Every write to the session fills it.
|
|
578
|
+
*/
|
|
579
|
+
export function openedRevFill(kind, data, root) {
|
|
580
|
+
const f = kind.session?.openedRev;
|
|
581
|
+
if (f === undefined || data[f] !== null)
|
|
582
|
+
return {};
|
|
583
|
+
const head = headCommit(root);
|
|
584
|
+
return head === null ? {} : { [f]: head };
|
|
585
|
+
}
|
|
586
|
+
/** Keys in the schema's `required` order, then its `properties` order, then the rest as given. */
|
|
587
|
+
export function schemaOrder(data, schema) {
|
|
588
|
+
const required = Array.isArray(schema.required) ? schema.required.filter((k) => typeof k === "string") : [];
|
|
589
|
+
const props = schema.properties !== null && typeof schema.properties === "object" ? Object.keys(schema.properties) : [];
|
|
590
|
+
const order = [...new Set([...required, ...props])];
|
|
591
|
+
const keys = Object.keys(data);
|
|
592
|
+
return [...order.filter((k) => keys.includes(k)), ...keys.filter((k) => !order.includes(k))];
|
|
593
|
+
}
|
|
594
|
+
export function pick(data, keys) {
|
|
595
|
+
return Object.fromEntries(keys.map((k) => [k, data[k]]));
|
|
596
|
+
}
|
|
597
|
+
export function abs(o, path) {
|
|
598
|
+
return join(o.root, ...path.split("/"));
|
|
599
|
+
}
|
|
600
|
+
function today() {
|
|
601
|
+
return new Date().toISOString().slice(0, 10);
|
|
602
|
+
}
|
|
603
|
+
export function failure(schema, err) {
|
|
604
|
+
if (err instanceof RecordWriteError || err instanceof RecordReadError || err instanceof WriteScopeError || err instanceof IdentityError || err instanceof WriteLockError) {
|
|
605
|
+
const conflict = err instanceof RecordWriteError ? err.conflict : undefined;
|
|
606
|
+
return { $schema: schema, contract: RECORDS_WRITE_CONTRACT_VERSION, error: { code: err.code, message: err.message }, ...(conflict ? { conflict } : {}) };
|
|
607
|
+
}
|
|
608
|
+
throw err;
|
|
609
|
+
}
|
|
610
|
+
// ── records new ──────────────────────────────────────────────────────────────
|
|
611
|
+
const ALLOCATABLE = /^([A-Za-z][A-Za-z0-9]*)-([0-9]+)$/;
|
|
612
|
+
/**
|
|
613
|
+
* The next id: `<prefix>-<n>`, one above the highest number any record in the
|
|
614
|
+
* directory has with that prefix, padded to at least three digits, or to the
|
|
615
|
+
* widest number those records use. Ids come from the records' own id field
|
|
616
|
+
* and, for a file that can't be read, from its name, so an id in use is never
|
|
617
|
+
* handed out again. The shape is derived from the records, with the prefix's
|
|
618
|
+
* case kept: a work kind's `W-001` and `W-002` give `W-003` (#2683), and a
|
|
619
|
+
* decision's `ws-052` gives `ws-053`. The kind's schema still judges the id
|
|
620
|
+
* written. Without `prefix`, every record must share one prefix.
|
|
621
|
+
*/
|
|
622
|
+
export function allocateId(entries, prefix, kind) {
|
|
623
|
+
const seen = [];
|
|
624
|
+
for (const e of entries) {
|
|
625
|
+
const stem = e.id ?? posix.basename(e.path).match(/^([A-Za-z][A-Za-z0-9]*-[0-9]+)/)?.[1] ?? null;
|
|
626
|
+
const m = stem?.match(ALLOCATABLE);
|
|
627
|
+
if (m)
|
|
628
|
+
seen.push({ prefix: m[1], digits: m[2] });
|
|
629
|
+
}
|
|
630
|
+
if (prefix === undefined) {
|
|
631
|
+
const prefixes = [...new Set(seen.map((s) => s.prefix))];
|
|
632
|
+
if (prefixes.length !== 1) {
|
|
633
|
+
throw new RecordWriteError("record-id-unallocatable", prefixes.length === 0
|
|
634
|
+
? `no ${kind} record has an id of the form <prefix>-<number> to follow; pass --prefix <prefix>, or give the id in the fields`
|
|
635
|
+
: `the ${kind} records use the prefixes ${prefixes.join(", ")}; pass --prefix with one of them, or give the id in the fields`);
|
|
636
|
+
}
|
|
637
|
+
prefix = prefixes[0];
|
|
638
|
+
}
|
|
639
|
+
const mine = seen.filter((s) => s.prefix === prefix);
|
|
640
|
+
const max = mine.reduce((m, s) => Math.max(m, Number(s.digits)), 0);
|
|
641
|
+
const width = Math.max(3, ...mine.map((s) => s.digits.length));
|
|
642
|
+
return `${prefix}-${String(max + 1).padStart(width, "0")}`;
|
|
643
|
+
}
|
|
644
|
+
/** `fields` with the channel's author and source laid over them (#2707). `isNew` for `records new`. */
|
|
645
|
+
function applyChannel(o, fields, opts, isNew, flag) {
|
|
646
|
+
const { kind } = o.loaded;
|
|
647
|
+
const out = { ...fields };
|
|
648
|
+
const through = opts.through;
|
|
649
|
+
if (isNew && through?.opensInitial && kind.states && kind.stateField !== undefined) {
|
|
650
|
+
const first = kind.states[0];
|
|
651
|
+
const state = out[kind.stateField];
|
|
652
|
+
if (state === undefined)
|
|
653
|
+
out[kind.stateField] = first;
|
|
654
|
+
else if (state !== first) {
|
|
655
|
+
throw new RecordWriteError("record-state-not-initial", `a new ${kind.name} opens ${first}, and the fields give ${kind.stateField} ${JSON.stringify(state)}: write it ${first}, and let the person who decides it move it on with a review or an amendment`);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
if (opts.by !== undefined) {
|
|
659
|
+
if (opts.by.trim() === "")
|
|
660
|
+
throw new RecordWriteError("write-usage-invalid", "by needs the name of the person or agent that proposed or decided");
|
|
661
|
+
// A new record that opens in the kind's first state is a proposal, when the kind opts in with proposedBy (#2756):
|
|
662
|
+
// by names the proposer, kept apart from reviews.decider, which stays null until the record is decided.
|
|
663
|
+
const opensInitial = isNew && !!kind.states && kind.stateField !== undefined && (out[kind.stateField] === undefined || out[kind.stateField] === kind.states[0]);
|
|
664
|
+
if (kind.proposedBy && opensInitial) {
|
|
665
|
+
const f = kind.proposedBy.field;
|
|
666
|
+
if (out[f] !== undefined && out[f] !== null && out[f] !== opts.by) {
|
|
667
|
+
throw new RecordWriteError("write-input-invalid", `the fields given with ${flag} set ${f} to ${JSON.stringify(out[f])}, and by names ${JSON.stringify(opts.by)}: give one proposer`);
|
|
668
|
+
}
|
|
669
|
+
out[f] = opts.by;
|
|
670
|
+
}
|
|
671
|
+
else {
|
|
672
|
+
if (!kind.reviews)
|
|
673
|
+
throw new RecordWriteError("write-usage-invalid", `by names a record's author, the kind's reviews.decider field, and the ${kind.name} kind declares no reviews`);
|
|
674
|
+
const f = kind.reviews.decider;
|
|
675
|
+
if (out[f] !== undefined && out[f] !== null && out[f] !== opts.by) {
|
|
676
|
+
throw new RecordWriteError("write-input-invalid", `the fields given with ${flag} set ${f} to ${JSON.stringify(out[f])}, and by names ${JSON.stringify(opts.by)}: give one author`);
|
|
677
|
+
}
|
|
678
|
+
out[f] = opts.by;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
if (!through)
|
|
682
|
+
return out;
|
|
683
|
+
if (kind.source && (isNew || kind.source.field in out)) {
|
|
684
|
+
const f = kind.source.field;
|
|
685
|
+
const block = out[f];
|
|
686
|
+
if (block === undefined || block === null)
|
|
687
|
+
out[f] = { ...through.source };
|
|
688
|
+
else if (typeof block === "object" && !Array.isArray(block))
|
|
689
|
+
out[f] = { ...block, ...through.source };
|
|
690
|
+
}
|
|
691
|
+
return out;
|
|
692
|
+
}
|
|
693
|
+
/** `records new`: write one new record from validated fields, sealed by its author with `sign`. Holds the write lock (#3173). */
|
|
694
|
+
export async function newRecord(opts) {
|
|
695
|
+
try {
|
|
696
|
+
return await withWriteLock(opts.cwd, writeWho("records new", opts), opts.dryRun, () => newRecordLocked(opts));
|
|
697
|
+
}
|
|
698
|
+
catch (err) {
|
|
699
|
+
return failure(RECORDS_NEW_SCHEMA_ID, err);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
async function newRecordLocked(opts) {
|
|
703
|
+
try {
|
|
704
|
+
if (opts.prefix !== undefined && !/^[A-Za-z][A-Za-z0-9]*$/.test(opts.prefix)) {
|
|
705
|
+
throw new RecordWriteError("write-usage-invalid", `--prefix takes letters and digits, starting with a letter, not ${JSON.stringify(opts.prefix)}`);
|
|
706
|
+
}
|
|
707
|
+
const o = await open(opts.kind, opts.cwd, { contentAddressed: true });
|
|
708
|
+
const scope = refuseOutOfScope(o, "new", opts.cwd, opts);
|
|
709
|
+
const fields = applyChannel(o, parseFields(opts.fields, "--from"), opts, true, "--from");
|
|
710
|
+
const { kind, schema } = o.loaded;
|
|
711
|
+
refuseUnidentifiedAuthors(scope, kind, fields, "--from", opts.agent);
|
|
712
|
+
refuseSealField(o, fields, "--from");
|
|
713
|
+
refuseRevisionFields(kind, fields, {}, "--from");
|
|
714
|
+
if (isContentAddressed(kind))
|
|
715
|
+
return await newContentAddressed(o, fields, opts);
|
|
716
|
+
const idField = kind.idField;
|
|
717
|
+
const before = await readAll(o, o.source);
|
|
718
|
+
const given = fields[idField];
|
|
719
|
+
let id;
|
|
720
|
+
if (given === undefined) {
|
|
721
|
+
id = allocateId(before, opts.prefix, kind.name);
|
|
722
|
+
}
|
|
723
|
+
else {
|
|
724
|
+
if (typeof given !== "string" || given === "")
|
|
725
|
+
throw new RecordWriteError("write-input-invalid", `${idField} must be a non-empty string when it is given`);
|
|
726
|
+
const taken = before.find((e) => e.id === given || posix.basename(e.path).startsWith(`${given}-`) || posix.basename(e.path) === `${given}.md`);
|
|
727
|
+
if (taken)
|
|
728
|
+
throw new RecordWriteError("record-id-taken", `id ${given} is already used by ${taken.path}; ids are never reused, so leave ${idField} out to have the next one allocated`);
|
|
729
|
+
id = given;
|
|
730
|
+
}
|
|
731
|
+
refuseHarvestedDecision(kind, fields);
|
|
732
|
+
// A session records the commit it opened at (#2693): HEAD now, or null before the first commit.
|
|
733
|
+
const opened = kind.session?.openedRev ? { [kind.session.openedRev]: headCommit(o.root) } : {};
|
|
734
|
+
const full = { ...fields, ...opened, [idField]: id };
|
|
735
|
+
const data = pick(full, schemaOrder(full, schema));
|
|
736
|
+
const title = typeof data.title === "string" ? data.title : "";
|
|
737
|
+
const match = new RegExp(kind.location.match);
|
|
738
|
+
const names = [slug(title) ? `${id}-${slug(title)}.md` : null, `${id}.md`].filter((n) => n !== null);
|
|
739
|
+
const name = names.find((n) => match.test(n));
|
|
740
|
+
if (!name)
|
|
741
|
+
throw new RecordWriteError("record-path-unmatched", `the kind's location.match ${kind.location.match} matches none of ${names.join(", ")}`);
|
|
742
|
+
const path = o.dirRel === "." ? name : `${o.dirRel}/${name}`;
|
|
743
|
+
if (o.source.list(o.dirRel)?.includes(name))
|
|
744
|
+
throw new RecordWriteError("record-id-taken", `${path} already exists`);
|
|
745
|
+
let text = renderRecord(data, title ? `\n# ${title}\n` : "");
|
|
746
|
+
let seal;
|
|
747
|
+
if (opts.sign !== undefined)
|
|
748
|
+
({ text, seal } = await sealAuthor(o, text, data, id, opts.sign, opts.cwd));
|
|
749
|
+
({ text } = sealClosed(o, text, seal ? { ...data, [RECORD_SEAL_FIELD]: seal } : data, id));
|
|
750
|
+
const written = await validatedEntry(o, before, path, text);
|
|
751
|
+
await refuseRatifyBelowQuorum(o, written, id);
|
|
752
|
+
const warnings = written.warnings;
|
|
753
|
+
if (!opts.dryRun) {
|
|
754
|
+
writeFileSync(abs(o, path), text, { flag: "wx" });
|
|
755
|
+
noteWrites(o.root, [{ path, text }], writeWho("records new", opts));
|
|
756
|
+
}
|
|
757
|
+
return {
|
|
758
|
+
$schema: RECORDS_NEW_SCHEMA_ID,
|
|
759
|
+
contract: RECORDS_WRITE_CONTRACT_VERSION,
|
|
760
|
+
kind: o.view,
|
|
761
|
+
path,
|
|
762
|
+
id,
|
|
763
|
+
...(seal ? { seal } : {}),
|
|
764
|
+
dryRun: !!opts.dryRun,
|
|
765
|
+
warnings,
|
|
766
|
+
digest: written.digest,
|
|
767
|
+
...(opts.dryRun ? { text } : {}),
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
catch (err) {
|
|
771
|
+
return failure(RECORDS_NEW_SCHEMA_ID, err);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* `records new` on a content-addressed JSON kind (ws-053, ws-082), such as
|
|
776
|
+
* the reference workspace's evidence: the fields, in the schema's order, as
|
|
777
|
+
* JSON with two-space indents and a final newline, in a file named for the
|
|
778
|
+
* SHA-256 of those bytes. The id is that hash, so the fields give no id, and
|
|
779
|
+
* the record never changes afterwards.
|
|
780
|
+
*/
|
|
781
|
+
async function newContentAddressed(o, fields, opts) {
|
|
782
|
+
const { kind, schema } = o.loaded;
|
|
783
|
+
if (opts.prefix !== undefined)
|
|
784
|
+
throw new RecordWriteError("write-usage-invalid", `the ${kind.name} kind's ids are the sha256 of each record's bytes, so --prefix allocates nothing`);
|
|
785
|
+
if (opts.sign !== undefined)
|
|
786
|
+
throw new RecordWriteError("write-usage-invalid", `the ${kind.name} kind names no author to seal: its records are named for the sha256 of their bytes`);
|
|
787
|
+
const data = pick(fields, schemaOrder(fields, schema));
|
|
788
|
+
const text = `${JSON.stringify(data, null, 2)}\n`;
|
|
789
|
+
const id = createHash("sha256").update(text, "utf-8").digest("hex");
|
|
790
|
+
const name = `${id}.json`;
|
|
791
|
+
if (!new RegExp(kind.location.match).test(name))
|
|
792
|
+
throw new RecordWriteError("record-path-unmatched", `the kind's location.match ${kind.location.match} does not match ${name}`);
|
|
793
|
+
const path = o.dirRel === "." ? name : `${o.dirRel}/${name}`;
|
|
794
|
+
if (o.source.list(o.dirRel)?.includes(name))
|
|
795
|
+
throw new RecordWriteError("record-id-taken", `${path} already exists, with the same bytes: the record is already written`);
|
|
796
|
+
const before = await readAll(o, o.source);
|
|
797
|
+
const written = await validatedEntry(o, before, path, text);
|
|
798
|
+
if (!opts.dryRun) {
|
|
799
|
+
writeFileSync(abs(o, path), text, { flag: "wx" });
|
|
800
|
+
noteWrites(o.root, [{ path, text }], writeWho("records new", opts));
|
|
801
|
+
}
|
|
802
|
+
return {
|
|
803
|
+
$schema: RECORDS_NEW_SCHEMA_ID,
|
|
804
|
+
contract: RECORDS_WRITE_CONTRACT_VERSION,
|
|
805
|
+
kind: o.view,
|
|
806
|
+
path,
|
|
807
|
+
id,
|
|
808
|
+
dryRun: !!opts.dryRun,
|
|
809
|
+
warnings: written.warnings,
|
|
810
|
+
digest: written.digest,
|
|
811
|
+
...(opts.dryRun ? { text } : {}),
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* `records amend`: set top-level fields of one record. A record in a closed
|
|
816
|
+
* state never changes. With approval ranks, a record ranked above 0 (such as
|
|
817
|
+
* a decided decision) changes in place only in its state, to one ranked at
|
|
818
|
+
* least as high, its pins field and its reviews: anything else is a new
|
|
819
|
+
* decision, written as a record that supersedes it (#2524 D4).
|
|
820
|
+
*
|
|
821
|
+
* An amendment moves the record's digest, so an author seal no longer holds
|
|
822
|
+
* (#2688). With `sign` the record is sealed again over the new text; without
|
|
823
|
+
* it, an amendment that moves the digest removes the seal and says so in
|
|
824
|
+
* `sealDropped`, so a record never carries a seal that fails. One that
|
|
825
|
+
* changes only the reviews leaves the digest, and the seal, as they were.
|
|
826
|
+
* `--sign` with nothing to change seals the record as it is.
|
|
827
|
+
*/
|
|
828
|
+
export async function amendRecord(opts) {
|
|
829
|
+
try {
|
|
830
|
+
refuseExpectShape(opts.expect);
|
|
831
|
+
return await withWriteLock(opts.cwd, writeWho("records amend", opts), opts.dryRun, () => amendRecordLocked(opts));
|
|
832
|
+
}
|
|
833
|
+
catch (err) {
|
|
834
|
+
return failure(RECORDS_AMEND_SCHEMA_ID, err);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
async function amendRecordLocked(opts) {
|
|
838
|
+
try {
|
|
839
|
+
const o = await open(opts.kind, opts.cwd);
|
|
840
|
+
const scope = refuseOutOfScope(o, "amend", opts.cwd, opts);
|
|
841
|
+
const given = applyChannel(o, parseFields(opts.fields, "--set"), opts, false, "--set");
|
|
842
|
+
const { kind } = o.loaded;
|
|
843
|
+
refuseUnidentifiedAuthors(scope, kind, given, "--set", opts.agent);
|
|
844
|
+
refuseSealField(o, given, "--set");
|
|
845
|
+
const before = await readAll(o, o.source);
|
|
846
|
+
const target = findRecord(before, opts.id, kind.name);
|
|
847
|
+
refuseStale(o, target, opts.id, opts.expect);
|
|
848
|
+
const old = target.data;
|
|
849
|
+
refuseRevisionFields(kind, given, old, "--set");
|
|
850
|
+
const isClosed = target.state !== null && (kind.closedStates ?? []).includes(target.state);
|
|
851
|
+
const patch = { ...(isClosed ? {} : openedRevFill(kind, old, o.root)), ...given };
|
|
852
|
+
const added = Object.keys(patch).filter((k) => !(k in old));
|
|
853
|
+
const merged = pick({ ...old, ...patch }, [...Object.keys(old), ...schemaOrder(pick(patch, added), o.loaded.schema)]);
|
|
854
|
+
const changed = Object.keys(merged).filter((k) => stableJson(old[k]) !== stableJson(merged[k]));
|
|
855
|
+
if (changed.includes(kind.idField)) {
|
|
856
|
+
throw new RecordWriteError("amend-id-immutable", `${kind.idField} never changes: ids are never renumbered. Write a new record that supersedes ${opts.id} instead`);
|
|
857
|
+
}
|
|
858
|
+
const state = target.state;
|
|
859
|
+
const link = kind.supersedes?.key === undefined ? JSON.stringify(opts.id) : `[{"${kind.supersedes.key}": "${opts.id}"}]`;
|
|
860
|
+
const supersede = kind.supersedes ? `chant workspace records new with ${kind.supersedes.field}: ${link}` : `chant workspace records new`;
|
|
861
|
+
if ((changed.length > 0 || opts.sign !== undefined) && state !== null && (kind.closedStates ?? []).includes(state)) {
|
|
862
|
+
throw new RecordWriteError("record-closed", `${opts.id} is ${state}, a closed state, so nothing in it changes. ${closedAdvice(o, opts.id, supersede)}`);
|
|
863
|
+
}
|
|
864
|
+
const rank = (s) => (typeof s === "string" ? (kind.approval?.[s] ?? 0) : 0);
|
|
865
|
+
if (changed.length > 0 && kind.approval && rank(state) > 0) {
|
|
866
|
+
const allowed = [kind.stateField, kind.pins?.field, kind.reviews?.field].filter((f) => typeof f === "string");
|
|
867
|
+
const stronger = (kind.states ?? []).filter((s) => rank(s) >= rank(state));
|
|
868
|
+
const bad = changed.filter((k) => !allowed.includes(k));
|
|
869
|
+
if (bad.length > 0) {
|
|
870
|
+
throw new RecordWriteError("amend-supersede-instead", `${opts.id} is ${state}, so ${bad.join(", ")} can't change in place: only ${allowed.join(", ")} may. Write the change as a new record that supersedes it (${supersede}); it replaces ${opts.id} once it is ${stronger.filter((s) => rank(s) > 0).join(" or ")}`);
|
|
871
|
+
}
|
|
872
|
+
if (kind.stateField !== undefined && changed.includes(kind.stateField) && rank(merged[kind.stateField]) < rank(state)) {
|
|
873
|
+
throw new RecordWriteError("amend-supersede-instead", `${opts.id} is ${state}, and ${JSON.stringify(merged[kind.stateField])} is approved less strongly: a state only moves to ${stronger.join(", ")}. Write a new record that supersedes it (${supersede})`);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
// Only the changed fields' blocks are rewritten, so the rest of the file keeps its bytes.
|
|
877
|
+
const current = o.source.read(target.path);
|
|
878
|
+
let text = changed.length === 0 ? current : (replaceFields(current, pick(merged, changed), merged) ?? renderRecord(merged, bodyOf(current)));
|
|
879
|
+
// The author seal (#2688): signed again over the new text, or dropped once the text moves.
|
|
880
|
+
let seal;
|
|
881
|
+
let sealDropped;
|
|
882
|
+
const sealable = kind.reviews !== undefined && RECORD_SEAL_FIELD in old;
|
|
883
|
+
if (opts.sign !== undefined) {
|
|
884
|
+
({ text, seal } = await sealAuthor(o, text, merged, opts.id, opts.sign, opts.cwd));
|
|
885
|
+
merged[RECORD_SEAL_FIELD] = seal;
|
|
886
|
+
}
|
|
887
|
+
else if (sealable &&
|
|
888
|
+
(recordTextDigest(text, digestFields(kind), kind.format) !== recordTextDigest(current, digestFields(kind), kind.format) ||
|
|
889
|
+
(kind.stateField !== undefined && stableJson(old[kind.stateField]) !== stableJson(merged[kind.stateField])))) {
|
|
890
|
+
// The seal signs the state as well as the digest, and a kind with reviews.ratified leaves the state out of the digest (#2873).
|
|
891
|
+
const signer = old[RECORD_SEAL_FIELD]?.signer;
|
|
892
|
+
delete merged[RECORD_SEAL_FIELD];
|
|
893
|
+
const dropped = removeField(text, RECORD_SEAL_FIELD, merged);
|
|
894
|
+
if (dropped === undefined)
|
|
895
|
+
throw new RecordWriteError("record-unparseable", `${target.path}: the ${RECORD_SEAL_FIELD} block can't be removed without changing the rest of the file`);
|
|
896
|
+
text = dropped;
|
|
897
|
+
sealDropped = `${opts.id} was sealed${typeof signer === "string" ? ` by ${signer}` : ""}, and the amendment moves its digest or its state, so the seal was removed: seal it again with records amend ${opts.id} --sign`;
|
|
898
|
+
}
|
|
899
|
+
if (stableJson(old[RECORD_SEAL_FIELD]) !== stableJson(merged[RECORD_SEAL_FIELD]))
|
|
900
|
+
changed.push(RECORD_SEAL_FIELD);
|
|
901
|
+
// Entering a closed state seals the whole record, last (#2546, ws-063).
|
|
902
|
+
if (changed.length > 0 && kind.seal && !kind.session) {
|
|
903
|
+
const closedSeal = sealClosed(o, text, merged, opts.id);
|
|
904
|
+
text = closedSeal.text;
|
|
905
|
+
if (stableJson(old[kind.seal.field]) !== stableJson(closedSeal.data[kind.seal.field]))
|
|
906
|
+
changed.push(kind.seal.field);
|
|
907
|
+
}
|
|
908
|
+
let warnings = target.warnings;
|
|
909
|
+
let digest = target.digest;
|
|
910
|
+
if (changed.length > 0) {
|
|
911
|
+
const written = await validatedEntry(o, before, target.path, text);
|
|
912
|
+
if (kind.stateField !== undefined && changed.includes(kind.stateField))
|
|
913
|
+
await refuseRatifyBelowQuorum(o, written, opts.id);
|
|
914
|
+
warnings = written.warnings;
|
|
915
|
+
digest = written.digest;
|
|
916
|
+
}
|
|
917
|
+
if (!opts.dryRun && changed.length > 0) {
|
|
918
|
+
writeFileAtomic(abs(o, target.path), text);
|
|
919
|
+
noteWrites(o.root, [{ path: target.path, text }], writeWho("records amend", opts));
|
|
920
|
+
}
|
|
921
|
+
return {
|
|
922
|
+
$schema: RECORDS_AMEND_SCHEMA_ID,
|
|
923
|
+
contract: RECORDS_WRITE_CONTRACT_VERSION,
|
|
924
|
+
kind: o.view,
|
|
925
|
+
path: target.path,
|
|
926
|
+
id: opts.id,
|
|
927
|
+
changed,
|
|
928
|
+
...(seal ? { seal } : {}),
|
|
929
|
+
...(sealDropped ? { sealDropped } : {}),
|
|
930
|
+
dryRun: !!opts.dryRun,
|
|
931
|
+
warnings,
|
|
932
|
+
digest,
|
|
933
|
+
...(opts.dryRun ? { text } : {}),
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
catch (err) {
|
|
937
|
+
return failure(RECORDS_AMEND_SCHEMA_ID, err);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
/**
|
|
941
|
+
* `records review`: append one verdict to a record's reviews, with the date
|
|
942
|
+
* and the digest of the record text it judged ({@link recordTextDigest}).
|
|
943
|
+
* With `sign`, the verdict carries a seal: an ssh signature over the record
|
|
944
|
+
* id, the digest, the verdict, the reviewer and the date (`trust/seal.ts`).
|
|
945
|
+
*/
|
|
946
|
+
export async function reviewRecord(opts) {
|
|
947
|
+
try {
|
|
948
|
+
refuseExpectShape(opts.expect);
|
|
949
|
+
return await withWriteLock(opts.cwd, writeWho("records review", opts), opts.dryRun, () => reviewRecordLocked(opts));
|
|
950
|
+
}
|
|
951
|
+
catch (err) {
|
|
952
|
+
return failure(RECORDS_REVIEW_SCHEMA_ID, err);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
async function reviewRecordLocked(opts) {
|
|
956
|
+
try {
|
|
957
|
+
if (!VERDICTS.includes(opts.verdict)) {
|
|
958
|
+
throw new RecordWriteError("write-usage-invalid", `--verdict takes ${VERDICTS.join(", ")}, not ${JSON.stringify(opts.verdict)}`);
|
|
959
|
+
}
|
|
960
|
+
if (opts.by.trim() === "")
|
|
961
|
+
throw new RecordWriteError("write-usage-invalid", "--by needs the reviewer's name");
|
|
962
|
+
if (opts.session !== undefined && opts.session === "")
|
|
963
|
+
throw new RecordWriteError("write-usage-invalid", "--session needs a session id");
|
|
964
|
+
const o = await open(opts.kind, opts.cwd);
|
|
965
|
+
refuseOutOfScope(o, "review", opts.cwd, opts);
|
|
966
|
+
const { kind } = o.loaded;
|
|
967
|
+
if (!kind.reviews) {
|
|
968
|
+
throw new RecordWriteError("review-unsupported", `the ${kind.name} kind declares no reviews field, so its records take no review`);
|
|
969
|
+
}
|
|
970
|
+
const field = kind.reviews.field;
|
|
971
|
+
const before = await readAll(o, o.source);
|
|
972
|
+
const target = findRecord(before, opts.id, kind.name);
|
|
973
|
+
refuseStale(o, target, opts.id, opts.expect);
|
|
974
|
+
if (target.state !== null && (kind.closedStates ?? []).includes(target.state)) {
|
|
975
|
+
throw new RecordWriteError("record-closed", `${opts.id} is ${target.state}, a closed state, so it takes no more reviews`);
|
|
976
|
+
}
|
|
977
|
+
if (opts.verdict === "dissent" && !(opts.note ?? "").trim()) {
|
|
978
|
+
throw new RecordWriteError("review-note-required", `a dissent needs a reason: pass --note <text> with the concern`);
|
|
979
|
+
}
|
|
980
|
+
const inSession = opts.session !== undefined ? await openSession(o, opts.session, opts.cwd) : undefined;
|
|
981
|
+
// The verdict is appended to the session too, so the session's kind must be in scope as well.
|
|
982
|
+
if (inSession)
|
|
983
|
+
refuseOutOfScope(inSession.so, "review", opts.cwd, opts);
|
|
984
|
+
const reviews = target.data[field] ?? [];
|
|
985
|
+
if (!Array.isArray(reviews))
|
|
986
|
+
throw new RecordWriteError("record-schema-invalid", `${target.path}: ${field} is not a list`);
|
|
987
|
+
const current = o.source.read(target.path);
|
|
988
|
+
const review = {
|
|
989
|
+
reviewer: opts.by,
|
|
990
|
+
verdict: opts.verdict,
|
|
991
|
+
...(opts.note !== undefined ? { note: opts.note } : {}),
|
|
992
|
+
on: opts.on ?? today(),
|
|
993
|
+
digest: recordTextDigest(current, digestFields(kind), kind.format),
|
|
994
|
+
...(opts.session !== undefined ? { session: opts.session } : {}),
|
|
995
|
+
};
|
|
996
|
+
if (opts.sign !== undefined)
|
|
997
|
+
review.seal = await seal(opts.sign, opts.cwd, { record: opts.id, digest: review.digest, verdict: opts.verdict, reviewer: opts.by, on: review.on });
|
|
998
|
+
// Only the reviews block changes, so the digest the verdict names stays the record's digest (#2672),
|
|
999
|
+
// and a record's author seal, which the digest leaves out too, still holds (#2688).
|
|
1000
|
+
const list = [...reviews, review];
|
|
1001
|
+
const text = replaceFields(current, { [field]: list }, { ...target.data, [field]: list });
|
|
1002
|
+
if (text === undefined)
|
|
1003
|
+
throw new RecordWriteError("record-unparseable", `${target.path}: the ${field} block can't be rewritten in place without changing the rest of the file`);
|
|
1004
|
+
const writtenEntry = await validatedEntry(o, before, target.path, text);
|
|
1005
|
+
const warnings = writtenEntry.warnings;
|
|
1006
|
+
// The same verdict on the session's own list (#2693), validated with the review in place, so the two lists never differ.
|
|
1007
|
+
let session;
|
|
1008
|
+
if (inSession) {
|
|
1009
|
+
const { so, before: sessions, target: st } = inSession;
|
|
1010
|
+
const decl = so.loaded.kind.session;
|
|
1011
|
+
const verdicts = st.data[decl.verdicts] ?? [];
|
|
1012
|
+
if (!Array.isArray(verdicts))
|
|
1013
|
+
throw new RecordWriteError("record-schema-invalid", `${st.path}: ${decl.verdicts} is not a list`);
|
|
1014
|
+
const verdict = { record: opts.id, principal: opts.by, verdict: opts.verdict, digest: review.digest };
|
|
1015
|
+
const set = { ...openedRevFill(so.loaded.kind, st.data, so.root), [decl.verdicts]: [...verdicts, verdict] };
|
|
1016
|
+
const sText = replaceFields(so.source.read(st.path), set, { ...st.data, ...set });
|
|
1017
|
+
if (sText === undefined)
|
|
1018
|
+
throw new RecordWriteError("record-unparseable", `${st.path}: the ${decl.verdicts} block can't be rewritten in place without changing the rest of the file`);
|
|
1019
|
+
await validateWrite(so, sessions, st.path, sText, overlay(o.source, target.path, text));
|
|
1020
|
+
session = { path: st.path, text: sText, out: { id: opts.session, path: st.path, verdict, ...(opts.dryRun ? { text: sText } : {}) } };
|
|
1021
|
+
}
|
|
1022
|
+
if (!opts.dryRun) {
|
|
1023
|
+
writeFileAtomic(abs(o, target.path), text);
|
|
1024
|
+
if (session)
|
|
1025
|
+
writeFileAtomic(abs(o, session.path), session.text);
|
|
1026
|
+
noteWrites(o.root, [{ path: target.path, text }, ...(session ? [{ path: session.path, text: session.text }] : [])], writeWho("records review", opts));
|
|
1027
|
+
}
|
|
1028
|
+
return {
|
|
1029
|
+
$schema: RECORDS_REVIEW_SCHEMA_ID,
|
|
1030
|
+
contract: RECORDS_WRITE_CONTRACT_VERSION,
|
|
1031
|
+
kind: o.view,
|
|
1032
|
+
path: target.path,
|
|
1033
|
+
id: opts.id,
|
|
1034
|
+
review,
|
|
1035
|
+
...(session ? { session: session.out } : {}),
|
|
1036
|
+
dryRun: !!opts.dryRun,
|
|
1037
|
+
warnings,
|
|
1038
|
+
digest: writtenEntry.digest,
|
|
1039
|
+
...(opts.dryRun ? { text } : {}),
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
catch (err) {
|
|
1043
|
+
return failure(RECORDS_REVIEW_SCHEMA_ID, err);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
/**
|
|
1047
|
+
* The open session `id` of a session kind whose subjects are the kind `o`
|
|
1048
|
+
* writes (#2693), found through the workspace declaration. Refused with
|
|
1049
|
+
* session-unknown when no such session exists, and session-not-open when it
|
|
1050
|
+
* is in a closed state.
|
|
1051
|
+
*/
|
|
1052
|
+
async function openSession(o, id, cwd) {
|
|
1053
|
+
const kinds = await sessionKindsFor(o.loaded.file, cwd);
|
|
1054
|
+
for (const k of kinds) {
|
|
1055
|
+
const so = await open(k.file, cwd);
|
|
1056
|
+
const before = await readAll(so, so.source);
|
|
1057
|
+
if (!before.some((e) => e.id === id))
|
|
1058
|
+
continue;
|
|
1059
|
+
const target = findRecord(before, id, so.view.name);
|
|
1060
|
+
if (target.data === null)
|
|
1061
|
+
throw new RecordWriteError("record-unparseable", `session ${id} (${target.path}) can't be read, so the verdict can't be added to it`);
|
|
1062
|
+
if (target.state !== null && (so.loaded.kind.closedStates ?? []).includes(target.state)) {
|
|
1063
|
+
throw new RecordWriteError("session-not-open", `session ${id} is ${target.state}, so it takes no more verdicts: give the verdict in an open session, or without --session`);
|
|
1064
|
+
}
|
|
1065
|
+
return { so, before, target };
|
|
1066
|
+
}
|
|
1067
|
+
throw new RecordWriteError("session-unknown", kinds.length === 0
|
|
1068
|
+
? `no session kind the workspace declaration names has ${o.view.file} as its subjects, so there is no session ${id} to give the verdict in`
|
|
1069
|
+
: `no ${kinds.map((k) => k.kind.name).join(" or ")} record has id ${id}`);
|
|
1070
|
+
}
|
|
1071
|
+
/** A seal over one verdict, or a refusal with review-sign-failed. Loaded only when --sign is given. */
|
|
1072
|
+
async function seal(sign, cwd, v) {
|
|
1073
|
+
const { resolveSigningKey, sealVerdict, SealError } = await import("./trust/seal.js");
|
|
1074
|
+
try {
|
|
1075
|
+
const key = resolveSigningKey(sign, cwd);
|
|
1076
|
+
try {
|
|
1077
|
+
return { ...sealVerdict(key.file, v) };
|
|
1078
|
+
}
|
|
1079
|
+
finally {
|
|
1080
|
+
key.cleanup();
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
catch (err) {
|
|
1084
|
+
if (err instanceof SealError)
|
|
1085
|
+
throw new RecordWriteError("review-sign-failed", err.message);
|
|
1086
|
+
throw err;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
// ── The command ──────────────────────────────────────────────────────────────
|
|
1090
|
+
export const WRITE_USAGE = [
|
|
1091
|
+
"chant workspace records new [<kind file or declared kind>] --from <file|-> [--prefix <prefix>] [--by <name>] [--sign [<key file>]] [--dry-run]",
|
|
1092
|
+
"chant workspace records amend <id> [--kind <kind file>] --set <file|-> [--expect <digest>] [--sign [<key file>]] [--dry-run]",
|
|
1093
|
+
"chant workspace records review <id> [--kind <kind file>] --verdict agree|dissent|abstain --by <principal> [--note <text>] [--session <id>] [--expect <digest>] [--sign [<key file>]] [--dry-run]",
|
|
1094
|
+
"chant workspace records close <session id> [--kind <session kind file>] [--expect <digest>] [--dry-run]",
|
|
1095
|
+
].join("\n");
|
|
1096
|
+
function usage(schema, message) {
|
|
1097
|
+
return { $schema: schema, contract: RECORDS_WRITE_CONTRACT_VERSION, error: { code: "write-usage-invalid", message: `${message}\n${WRITE_USAGE}` } };
|
|
1098
|
+
}
|
|
1099
|
+
/**
|
|
1100
|
+
* The kind a write goes through when none is named (#2680): the one record
|
|
1101
|
+
* kind the declaration nearest above `cwd` names. None declared keeps the
|
|
1102
|
+
* message the verb has always given; several are refused, since a write
|
|
1103
|
+
* never guesses which kind it means.
|
|
1104
|
+
*/
|
|
1105
|
+
export function declaredWriteKind(schema, cwd, missing) {
|
|
1106
|
+
let kinds;
|
|
1107
|
+
try {
|
|
1108
|
+
kinds = declaredKindFiles(cwd);
|
|
1109
|
+
}
|
|
1110
|
+
catch (err) {
|
|
1111
|
+
if (!(err instanceof WorkspaceReadError))
|
|
1112
|
+
throw err;
|
|
1113
|
+
return usage(schema, `${missing}; the declaration can't name one: ${err.code}: ${err.describe()}`);
|
|
1114
|
+
}
|
|
1115
|
+
if (kinds.length === 0)
|
|
1116
|
+
return usage(schema, missing);
|
|
1117
|
+
if (kinds.length > 1) {
|
|
1118
|
+
return usage(schema, `the declaration names ${kinds.length} record kinds (${kinds.map((k) => k.declared.path).join(", ")}), so name the one to write with --kind`);
|
|
1119
|
+
}
|
|
1120
|
+
return kinds[0].file;
|
|
1121
|
+
}
|
|
1122
|
+
/**
|
|
1123
|
+
* The kind file a write names: `arg` itself when it is a file, or else the
|
|
1124
|
+
* declared record kind it names (#2683), by the name the declaration gives it
|
|
1125
|
+
* or its file's name without `.kind.mjs`, so `records new work` finds
|
|
1126
|
+
* `work/work.kind.mjs`. Anything else is returned as given, and loading it
|
|
1127
|
+
* fails with kind-unreadable.
|
|
1128
|
+
*/
|
|
1129
|
+
export function resolveWriteKind(arg, cwd) {
|
|
1130
|
+
try {
|
|
1131
|
+
if (statSync(resolve(cwd, arg)).isFile())
|
|
1132
|
+
return arg;
|
|
1133
|
+
}
|
|
1134
|
+
catch {
|
|
1135
|
+
// Not a file: try the declared kinds.
|
|
1136
|
+
}
|
|
1137
|
+
let kinds;
|
|
1138
|
+
try {
|
|
1139
|
+
kinds = declaredKindFiles(cwd);
|
|
1140
|
+
}
|
|
1141
|
+
catch {
|
|
1142
|
+
return arg;
|
|
1143
|
+
}
|
|
1144
|
+
const hit = kinds.filter((k) => (k.declared.name ?? posix.basename(k.declared.path).replace(/(?:\.kind)?\.[cm]?[jt]s$/, "")) === arg);
|
|
1145
|
+
return hit.length === 1 ? hit[0].file : arg;
|
|
1146
|
+
}
|
|
1147
|
+
/** The text of `--from` or `--set`: a file, or standard input for `-`. */
|
|
1148
|
+
function readInput(schema, flag, value, cwd) {
|
|
1149
|
+
if (value === undefined || value === "")
|
|
1150
|
+
return usage(schema, `${flag} <file|-> is required`);
|
|
1151
|
+
try {
|
|
1152
|
+
return readFileSync(value === "-" ? 0 : resolve(cwd, value), "utf-8");
|
|
1153
|
+
}
|
|
1154
|
+
catch (err) {
|
|
1155
|
+
return {
|
|
1156
|
+
$schema: schema,
|
|
1157
|
+
contract: RECORDS_WRITE_CONTRACT_VERSION,
|
|
1158
|
+
error: { code: "write-input-invalid", message: `${flag} ${value} could not be read: ${err instanceof Error ? err.message : String(err)}` },
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
/**
|
|
1163
|
+
* The session kind `records close` goes through when none is named (#2693):
|
|
1164
|
+
* the one session kind the declaration names.
|
|
1165
|
+
*/
|
|
1166
|
+
export async function declaredSessionKind(schema, cwd) {
|
|
1167
|
+
const kinds = (await findSessionKinds(cwd)).map((k) => k.file);
|
|
1168
|
+
if (kinds.length === 1)
|
|
1169
|
+
return kinds[0];
|
|
1170
|
+
if (kinds.length === 0)
|
|
1171
|
+
return usage(schema, "--kind <session kind file> is required: the declaration names no session kind");
|
|
1172
|
+
return usage(schema, `the declaration names ${kinds.length} session kinds (${kinds.join(", ")}), so name the one to close with --kind`);
|
|
1173
|
+
}
|
|
1174
|
+
/** `chant workspace records new|amend|review|close`. Prints one JSON document; exits 0 when it wrote, or would have with --dry-run. */
|
|
1175
|
+
export async function runRecordsWrite(ctx) {
|
|
1176
|
+
const { args } = ctx;
|
|
1177
|
+
const cwd = process.cwd();
|
|
1178
|
+
const verb = args.extraPositional;
|
|
1179
|
+
// The agent session the write is made in (#2548): set by the harness that runs the agent.
|
|
1180
|
+
const agent = process.env[AGENT_ENV] || undefined;
|
|
1181
|
+
const print = (doc) => {
|
|
1182
|
+
console.log(JSON.stringify(doc, null, 2));
|
|
1183
|
+
return "error" in doc ? 1 : 0;
|
|
1184
|
+
};
|
|
1185
|
+
if (verb === "close") {
|
|
1186
|
+
const { closeRecord, RECORDS_CLOSE_SCHEMA_ID } = await import("./records-close.js");
|
|
1187
|
+
if (args.sign !== undefined)
|
|
1188
|
+
return print(usage(RECORDS_CLOSE_SCHEMA_ID, "--sign is not taken by close: the seal close writes is the digest of the session text"));
|
|
1189
|
+
const id = args.extraPositional2;
|
|
1190
|
+
if (!id)
|
|
1191
|
+
return print(usage(RECORDS_CLOSE_SCHEMA_ID, "close needs the session's id"));
|
|
1192
|
+
const kind = args.kind !== undefined ? resolveWriteKind(args.kind, cwd) : await declaredSessionKind(RECORDS_CLOSE_SCHEMA_ID, cwd);
|
|
1193
|
+
if (typeof kind !== "string")
|
|
1194
|
+
return print(kind);
|
|
1195
|
+
return print(await closeRecord({ kind, id, dryRun: args.dryRun, cwd, agent, expect: args.expect }));
|
|
1196
|
+
}
|
|
1197
|
+
if (verb === "new") {
|
|
1198
|
+
const named = args.extraPositional2 ?? args.kind;
|
|
1199
|
+
const kind = named !== undefined ? resolveWriteKind(named, cwd) : declaredWriteKind(RECORDS_NEW_SCHEMA_ID, cwd, "new needs the kind file");
|
|
1200
|
+
if (typeof kind !== "string")
|
|
1201
|
+
return print(kind);
|
|
1202
|
+
if (args.expect !== undefined)
|
|
1203
|
+
return print(usage(RECORDS_NEW_SCHEMA_ID, "--expect is not taken by new: a new record has no digest yet, and its id is allocated under the write lock"));
|
|
1204
|
+
const input = readInput(RECORDS_NEW_SCHEMA_ID, "--from", args.migrateFrom, cwd);
|
|
1205
|
+
if (typeof input !== "string")
|
|
1206
|
+
return print(input);
|
|
1207
|
+
return print(await newRecord({ kind, fields: input, prefix: args.prefix, by: args.by, sign: args.sign, dryRun: args.dryRun, cwd, agent }));
|
|
1208
|
+
}
|
|
1209
|
+
const schema = verb === "amend" ? RECORDS_AMEND_SCHEMA_ID : RECORDS_REVIEW_SCHEMA_ID;
|
|
1210
|
+
const id = args.extraPositional2;
|
|
1211
|
+
if (!id)
|
|
1212
|
+
return print(usage(schema, `${verb} needs the record's id`));
|
|
1213
|
+
const kind = args.kind !== undefined ? resolveWriteKind(args.kind, cwd) : declaredWriteKind(schema, cwd, "--kind <kind file> is required");
|
|
1214
|
+
if (typeof kind !== "string")
|
|
1215
|
+
return print(kind);
|
|
1216
|
+
if (verb === "amend") {
|
|
1217
|
+
if (args.by !== undefined)
|
|
1218
|
+
return print(usage(schema, "--by is not taken by amend: set decided_by, or a kind's proposedBy field, with --set"));
|
|
1219
|
+
const input = readInput(schema, "--set", args.set, cwd);
|
|
1220
|
+
if (typeof input !== "string")
|
|
1221
|
+
return print(input);
|
|
1222
|
+
return print(await amendRecord({ kind, id, fields: input, sign: args.sign, dryRun: args.dryRun, cwd, agent, expect: args.expect }));
|
|
1223
|
+
}
|
|
1224
|
+
if (args.verdict === undefined)
|
|
1225
|
+
return print(usage(schema, "--verdict agree|dissent|abstain is required"));
|
|
1226
|
+
if (args.by === undefined)
|
|
1227
|
+
return print(usage(schema, "--by <principal> is required"));
|
|
1228
|
+
return print(await reviewRecord({ kind, id, verdict: args.verdict, by: args.by, note: args.note, session: args.session, sign: args.sign, dryRun: args.dryRun, cwd, agent, expect: args.expect }));
|
|
1229
|
+
}
|