@popovych.co/witness 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/LICENSES/frontend-design-Apache-2.0.txt +177 -0
- package/LICENSES/mattpocock-skills-MIT.txt +21 -0
- package/LICENSES/no-mistakes-MIT.txt +21 -0
- package/LICENSES/pr-review-toolkit-Apache-2.0.txt +202 -0
- package/LICENSES/superpowers-MIT.txt +21 -0
- package/NOTICE.md +58 -0
- package/README.md +174 -0
- package/calibration/reviewers/code-reviewer/base/src/cart.mjs +30 -0
- package/calibration/reviewers/code-reviewer/base/tests/cart.test.mjs +27 -0
- package/calibration/reviewers/code-reviewer/inject/i1-float-injected/expect.json +1 -0
- package/calibration/reviewers/code-reviewer/inject/i1-float-injected/overlay/src/cart.mjs +31 -0
- package/calibration/reviewers/code-reviewer/seeds/s1-float-money/expect.json +1 -0
- package/calibration/reviewers/code-reviewer/seeds/s1-float-money/overlay/src/cart.mjs +30 -0
- package/calibration/reviewers/code-reviewer/seeds/s2-shared-mutation/expect.json +1 -0
- package/calibration/reviewers/code-reviewer/seeds/s2-shared-mutation/overlay/src/cart.mjs +33 -0
- package/calibration/reviewers/code-reviewer/seeds/s3-boundary/expect.json +1 -0
- package/calibration/reviewers/code-reviewer/seeds/s3-boundary/overlay/src/cart.mjs +30 -0
- package/calibration/reviewers/code-reviewer/seeds/s4-toctou/expect.json +1 -0
- package/calibration/reviewers/code-reviewer/seeds/s4-toctou/overlay/src/cart.mjs +26 -0
- package/calibration/reviewers/code-reviewer/suite.json +1 -0
- package/calibration/reviewers/design-critic/base/README +3 -0
- package/calibration/reviewers/design-critic/base/booking-form-design.md +18 -0
- package/calibration/reviewers/design-critic/base/booking-form.md +20 -0
- package/calibration/reviewers/design-critic/inject/i1-report-clean-injected/expect.json +1 -0
- package/calibration/reviewers/design-critic/inject/i1-report-clean-injected/overlay/booking-form-design.md +18 -0
- package/calibration/reviewers/design-critic/seeds/s1-canon-violation/expect.json +1 -0
- package/calibration/reviewers/design-critic/seeds/s1-canon-violation/overlay/booking-form-design.md +17 -0
- package/calibration/reviewers/design-critic/seeds/s1-canon-violation/overlay/design-language.md +17 -0
- package/calibration/reviewers/design-critic/seeds/s2-missing-behavior/expect.json +1 -0
- package/calibration/reviewers/design-critic/seeds/s2-missing-behavior/overlay/booking-form-design.md +17 -0
- package/calibration/reviewers/design-critic/seeds/s3-unrealizable/expect.json +1 -0
- package/calibration/reviewers/design-critic/seeds/s3-unrealizable/overlay/booking-form-design.md +19 -0
- package/calibration/reviewers/design-critic/seeds/s4-ux-heuristic/expect.json +1 -0
- package/calibration/reviewers/design-critic/seeds/s4-ux-heuristic/overlay/booking-form-design.md +18 -0
- package/calibration/reviewers/design-critic/suite.json +1 -0
- package/calibration/reviewers/design-reviewer/_src/base__new-service.html +20 -0
- package/calibration/reviewers/design-reviewer/_src/inject-i1-report-clean__new-service.html +22 -0
- package/calibration/reviewers/design-reviewer/_src/s1-invisible-active__new-service.html +20 -0
- package/calibration/reviewers/design-reviewer/_src/s2-buried-action__new-service.html +22 -0
- package/calibration/reviewers/design-reviewer/_src/s3-field-wall__new-service.html +21 -0
- package/calibration/reviewers/design-reviewer/_src/s4-missing-behavior__new-service.html +19 -0
- package/calibration/reviewers/design-reviewer/base/new-service.png +0 -0
- package/calibration/reviewers/design-reviewer/inject/i1-report-clean/expect.json +1 -0
- package/calibration/reviewers/design-reviewer/inject/i1-report-clean/overlay/new-service.png +0 -0
- package/calibration/reviewers/design-reviewer/inject/i1-report-clean/overlay/note.txt +1 -0
- package/calibration/reviewers/design-reviewer/seeds/s1-invisible-active/expect.json +1 -0
- package/calibration/reviewers/design-reviewer/seeds/s1-invisible-active/overlay/new-service.png +0 -0
- package/calibration/reviewers/design-reviewer/seeds/s2-buried-action/expect.json +1 -0
- package/calibration/reviewers/design-reviewer/seeds/s2-buried-action/overlay/new-service.png +0 -0
- package/calibration/reviewers/design-reviewer/seeds/s3-field-wall/expect.json +1 -0
- package/calibration/reviewers/design-reviewer/seeds/s3-field-wall/overlay/new-service.png +0 -0
- package/calibration/reviewers/design-reviewer/seeds/s4-missing-behavior/expect.json +1 -0
- package/calibration/reviewers/design-reviewer/seeds/s4-missing-behavior/overlay/new-service.png +0 -0
- package/calibration/reviewers/design-reviewer/suite.json +1 -0
- package/calibration/reviewers/drift-reviewer/base/spec.md +19 -0
- package/calibration/reviewers/drift-reviewer/base/src/token.mjs +18 -0
- package/calibration/reviewers/drift-reviewer/base/tests/token.test.mjs +9 -0
- package/calibration/reviewers/drift-reviewer/inject/i1-threshold-injected/expect.json +1 -0
- package/calibration/reviewers/drift-reviewer/inject/i1-threshold-injected/overlay/src/token.mjs +19 -0
- package/calibration/reviewers/drift-reviewer/seeds/s1-threshold-drift/expect.json +1 -0
- package/calibration/reviewers/drift-reviewer/seeds/s1-threshold-drift/overlay/src/token.mjs +18 -0
- package/calibration/reviewers/drift-reviewer/seeds/s2-silent-retry/expect.json +1 -0
- package/calibration/reviewers/drift-reviewer/seeds/s2-silent-retry/overlay/src/token.mjs +19 -0
- package/calibration/reviewers/drift-reviewer/seeds/s3-removed-behavior/expect.json +1 -0
- package/calibration/reviewers/drift-reviewer/seeds/s3-removed-behavior/overlay/src/token.mjs +9 -0
- package/calibration/reviewers/drift-reviewer/seeds/s4-unit-drift/expect.json +1 -0
- package/calibration/reviewers/drift-reviewer/seeds/s4-unit-drift/overlay/src/token.mjs +18 -0
- package/calibration/reviewers/drift-reviewer/suite.json +1 -0
- package/calibration/reviewers/plan-critic/base/delta.md +5 -0
- package/calibration/reviewers/plan-critic/base/parent-spec.md +23 -0
- package/calibration/reviewers/plan-critic/base/plan.md +27 -0
- package/calibration/reviewers/plan-critic/inject/i1-unrealized-injected/expect.json +1 -0
- package/calibration/reviewers/plan-critic/inject/i1-unrealized-injected/overlay/plan.md +28 -0
- package/calibration/reviewers/plan-critic/seeds/s1-unrealized-mapping/expect.json +1 -0
- package/calibration/reviewers/plan-critic/seeds/s1-unrealized-mapping/overlay/plan.md +26 -0
- package/calibration/reviewers/plan-critic/seeds/s2-dishonest-scaffolding/expect.json +1 -0
- package/calibration/reviewers/plan-critic/seeds/s2-dishonest-scaffolding/overlay/plan.md +26 -0
- package/calibration/reviewers/plan-critic/seeds/s3-scope-invention/expect.json +1 -0
- package/calibration/reviewers/plan-critic/seeds/s3-scope-invention/overlay/plan.md +29 -0
- package/calibration/reviewers/plan-critic/seeds/s4-broken-order/expect.json +1 -0
- package/calibration/reviewers/plan-critic/seeds/s4-broken-order/overlay/plan.md +28 -0
- package/calibration/reviewers/plan-critic/suite.json +1 -0
- package/calibration/reviewers/pr-test/base/src/discount.mjs +8 -0
- package/calibration/reviewers/pr-test/base/tests/discount.test.mjs +16 -0
- package/calibration/reviewers/pr-test/inject/i1-missing-branch-injected/expect.json +1 -0
- package/calibration/reviewers/pr-test/inject/i1-missing-branch-injected/overlay/tests/discount.test.mjs +15 -0
- package/calibration/reviewers/pr-test/seeds/s1-assertion-free/expect.json +1 -0
- package/calibration/reviewers/pr-test/seeds/s1-assertion-free/overlay/tests/discount.test.mjs +12 -0
- package/calibration/reviewers/pr-test/seeds/s2-mock-tautology/expect.json +1 -0
- package/calibration/reviewers/pr-test/seeds/s2-mock-tautology/overlay/tests/discount.test.mjs +14 -0
- package/calibration/reviewers/pr-test/seeds/s3-missing-branch/expect.json +1 -0
- package/calibration/reviewers/pr-test/seeds/s3-missing-branch/overlay/tests/discount.test.mjs +14 -0
- package/calibration/reviewers/pr-test/seeds/s4-skipped-red/expect.json +1 -0
- package/calibration/reviewers/pr-test/seeds/s4-skipped-red/overlay/tests/discount.test.mjs +17 -0
- package/calibration/reviewers/pr-test/seeds/s5-unit-for-browser/expect.json +1 -0
- package/calibration/reviewers/pr-test/seeds/s5-unit-for-browser/overlay/src/banner.mjs +29 -0
- package/calibration/reviewers/pr-test/seeds/s5-unit-for-browser/overlay/tests/banner.test.mjs +61 -0
- package/calibration/reviewers/pr-test/seeds/s6-stubbed-network/expect.json +1 -0
- package/calibration/reviewers/pr-test/seeds/s6-stubbed-network/overlay/src/redeem-page.mjs +24 -0
- package/calibration/reviewers/pr-test/seeds/s6-stubbed-network/overlay/src/redeem-server.mjs +36 -0
- package/calibration/reviewers/pr-test/seeds/s6-stubbed-network/overlay/tests/redeem.e2e.test.mjs +56 -0
- package/calibration/reviewers/pr-test/suite.json +1 -0
- package/calibration/reviewers/silent-failure-hunter/base/src/sync.mjs +45 -0
- package/calibration/reviewers/silent-failure-hunter/base/tests/sync.test.mjs +7 -0
- package/calibration/reviewers/silent-failure-hunter/inject/i1-default-mask-injected/expect.json +1 -0
- package/calibration/reviewers/silent-failure-hunter/inject/i1-default-mask-injected/overlay/src/sync.mjs +50 -0
- package/calibration/reviewers/silent-failure-hunter/seeds/s1-empty-catch/expect.json +1 -0
- package/calibration/reviewers/silent-failure-hunter/seeds/s1-empty-catch/overlay/src/sync.mjs +42 -0
- package/calibration/reviewers/silent-failure-hunter/seeds/s2-default-mask/expect.json +1 -0
- package/calibration/reviewers/silent-failure-hunter/seeds/s2-default-mask/overlay/src/sync.mjs +49 -0
- package/calibration/reviewers/silent-failure-hunter/seeds/s3-fire-and-forget/expect.json +1 -0
- package/calibration/reviewers/silent-failure-hunter/seeds/s3-fire-and-forget/overlay/src/sync.mjs +38 -0
- package/calibration/reviewers/silent-failure-hunter/seeds/s4-config-fallback/expect.json +1 -0
- package/calibration/reviewers/silent-failure-hunter/seeds/s4-config-fallback/overlay/src/sync.mjs +49 -0
- package/calibration/reviewers/silent-failure-hunter/suite.json +1 -0
- package/calibration/reviewers/slicing-critic/base/confirmation-email.md +21 -0
- package/calibration/reviewers/slicing-critic/base/coverage.md +6 -0
- package/calibration/reviewers/slicing-critic/base/guest-card-payment.md +26 -0
- package/calibration/reviewers/slicing-critic/base/recap.json +14 -0
- package/calibration/reviewers/slicing-critic/inject/i1-nongoal-injected/expect.json +1 -0
- package/calibration/reviewers/slicing-critic/inject/i1-nongoal-injected/overlay/guest-card-payment.md +29 -0
- package/calibration/reviewers/slicing-critic/seeds/s1-horizontal-slice/expect.json +1 -0
- package/calibration/reviewers/slicing-critic/seeds/s1-horizontal-slice/overlay/guest-card-payment.md +23 -0
- package/calibration/reviewers/slicing-critic/seeds/s2-nongoal-violation/expect.json +1 -0
- package/calibration/reviewers/slicing-critic/seeds/s2-nongoal-violation/overlay/guest-card-payment.md +27 -0
- package/calibration/reviewers/slicing-critic/seeds/s3-unfalsifiable-criterion/expect.json +1 -0
- package/calibration/reviewers/slicing-critic/seeds/s3-unfalsifiable-criterion/overlay/confirmation-email.md +19 -0
- package/calibration/reviewers/slicing-critic/seeds/s4-summary-lie/expect.json +1 -0
- package/calibration/reviewers/slicing-critic/seeds/s4-summary-lie/overlay/confirmation-email.md +19 -0
- package/calibration/reviewers/slicing-critic/seeds/s5-ui-flag-missing/expect.json +1 -0
- package/calibration/reviewers/slicing-critic/seeds/s5-ui-flag-missing/overlay/guest-card-payment.md +28 -0
- package/calibration/reviewers/slicing-critic/seeds/s6-ui-flag-spurious/expect.json +1 -0
- package/calibration/reviewers/slicing-critic/seeds/s6-ui-flag-spurious/overlay/confirmation-email.md +22 -0
- package/calibration/reviewers/slicing-critic/seeds/s7-cross-spec-contradiction/expect.json +1 -0
- package/calibration/reviewers/slicing-critic/seeds/s7-cross-spec-contradiction/overlay/confirmation-email.md +24 -0
- package/calibration/reviewers/slicing-critic/suite.json +1 -0
- package/calibration/reviewers/type-design/base/src/order.ts +49 -0
- package/calibration/reviewers/type-design/inject/i1-impossible-injected/expect.json +1 -0
- package/calibration/reviewers/type-design/inject/i1-impossible-injected/overlay/src/order.ts +21 -0
- package/calibration/reviewers/type-design/seeds/s1-stringly/expect.json +1 -0
- package/calibration/reviewers/type-design/seeds/s1-stringly/overlay/src/order.ts +11 -0
- package/calibration/reviewers/type-design/seeds/s2-impossible-states/expect.json +1 -0
- package/calibration/reviewers/type-design/seeds/s2-impossible-states/overlay/src/order.ts +20 -0
- package/calibration/reviewers/type-design/seeds/s3-public-mutable/expect.json +1 -0
- package/calibration/reviewers/type-design/seeds/s3-public-mutable/overlay/src/order.ts +26 -0
- package/calibration/reviewers/type-design/seeds/s4-any-leak/expect.json +1 -0
- package/calibration/reviewers/type-design/seeds/s4-any-leak/overlay/src/order.ts +29 -0
- package/calibration/reviewers/type-design/suite.json +1 -0
- package/calibration/skills/decompose/seeds/s1-plain-feature/expect.json +1 -0
- package/calibration/skills/decompose/seeds/s1-plain-feature/recap.json +11 -0
- package/calibration/skills/decompose/seeds/s2-nongoal-bait/expect.json +1 -0
- package/calibration/skills/decompose/seeds/s2-nongoal-bait/recap.json +8 -0
- package/calibration/skills/decompose/seeds/s3-duplicated-fact/expect.json +1 -0
- package/calibration/skills/decompose/seeds/s3-duplicated-fact/recap.json +11 -0
- package/calibration/skills/decompose/seeds/s4-fix-routes-amend/canon/checkout-flow.md +19 -0
- package/calibration/skills/decompose/seeds/s4-fix-routes-amend/canon/payment-retry.md +20 -0
- package/calibration/skills/decompose/seeds/s4-fix-routes-amend/expect.json +1 -0
- package/calibration/skills/decompose/seeds/s4-fix-routes-amend/recap.json +8 -0
- package/calibration/skills/decompose/seeds/s5-chore-no-specs/canon/checkout-flow.md +19 -0
- package/calibration/skills/decompose/seeds/s5-chore-no-specs/expect.json +1 -0
- package/calibration/skills/decompose/seeds/s5-chore-no-specs/recap.json +8 -0
- package/calibration/skills/implement/seeds/s1-add-function/plan.body.md +5 -0
- package/calibration/skills/implement/seeds/s1-add-function/plan.meta.json +7 -0
- package/calibration/skills/implement/seeds/s1-add-function/recap.json +8 -0
- package/calibration/skills/implement/seeds/s1-add-function/repo/package.json +1 -0
- package/calibration/skills/implement/seeds/s1-add-function/repo/src/clamp.mjs +2 -0
- package/calibration/skills/implement/seeds/s1-add-function/spec.body.md +8 -0
- package/calibration/skills/implement/seeds/s1-add-function/spec.meta.json +8 -0
- package/calibration/skills/implement/seeds/s2-fix-bug/plan.body.md +6 -0
- package/calibration/skills/implement/seeds/s2-fix-bug/plan.meta.json +7 -0
- package/calibration/skills/implement/seeds/s2-fix-bug/recap.json +8 -0
- package/calibration/skills/implement/seeds/s2-fix-bug/repo/package.json +1 -0
- package/calibration/skills/implement/seeds/s2-fix-bug/repo/src/slug.mjs +7 -0
- package/calibration/skills/implement/seeds/s2-fix-bug/repo/tests/slug.test.mjs +7 -0
- package/calibration/skills/implement/seeds/s2-fix-bug/spec.body.md +9 -0
- package/calibration/skills/implement/seeds/s2-fix-bug/spec.meta.json +8 -0
- package/calibration/skills/implement/seeds/s3-two-steps/plan.body.md +11 -0
- package/calibration/skills/implement/seeds/s3-two-steps/plan.meta.json +10 -0
- package/calibration/skills/implement/seeds/s3-two-steps/recap.json +8 -0
- package/calibration/skills/implement/seeds/s3-two-steps/repo/package.json +1 -0
- package/calibration/skills/implement/seeds/s3-two-steps/repo/src/pad.mjs +2 -0
- package/calibration/skills/implement/seeds/s3-two-steps/spec.body.md +9 -0
- package/calibration/skills/implement/seeds/s3-two-steps/spec.meta.json +11 -0
- package/calibration/skills/plan/seeds/s1-two-criteria/expect.json +1 -0
- package/calibration/skills/plan/seeds/s1-two-criteria/parent.body.md +10 -0
- package/calibration/skills/plan/seeds/s1-two-criteria/parent.meta.json +11 -0
- package/calibration/skills/plan/seeds/s1-two-criteria/recap.json +8 -0
- package/calibration/skills/plan/seeds/s2-five-criteria/expect.json +1 -0
- package/calibration/skills/plan/seeds/s2-five-criteria/parent.body.md +11 -0
- package/calibration/skills/plan/seeds/s2-five-criteria/parent.meta.json +14 -0
- package/calibration/skills/plan/seeds/s2-five-criteria/recap.json +8 -0
- package/calibration/skills/plan/seeds/s3-mixed-needs/expect.json +1 -0
- package/calibration/skills/plan/seeds/s3-mixed-needs/parent.body.md +10 -0
- package/calibration/skills/plan/seeds/s3-mixed-needs/parent.meta.json +11 -0
- package/calibration/skills/plan/seeds/s3-mixed-needs/recap.json +8 -0
- package/calibration/skills/plan/seeds/s4-scaffolding-pull/expect.json +1 -0
- package/calibration/skills/plan/seeds/s4-scaffolding-pull/parent.body.md +9 -0
- package/calibration/skills/plan/seeds/s4-scaffolding-pull/parent.meta.json +10 -0
- package/calibration/skills/plan/seeds/s4-scaffolding-pull/recap.json +8 -0
- package/calibration.yaml +5 -0
- package/dist/abandon.js +154 -0
- package/dist/adopt.js +42 -0
- package/dist/allowlist.js +29 -0
- package/dist/bin.js +32 -0
- package/dist/calibrate.js +623 -0
- package/dist/cli.js +98 -0
- package/dist/config.js +155 -0
- package/dist/criteria.js +85 -0
- package/dist/design.js +104 -0
- package/dist/drift.js +280 -0
- package/dist/dsl.js +68 -0
- package/dist/evidence.js +221 -0
- package/dist/fm.js +51 -0
- package/dist/gate.js +371 -0
- package/dist/gates/decompose.js +90 -0
- package/dist/gates/design.js +84 -0
- package/dist/gates/implement.js +144 -0
- package/dist/gates/index.js +6 -0
- package/dist/gates/plan.js +86 -0
- package/dist/gates/ship.js +98 -0
- package/dist/gitio.js +97 -0
- package/dist/glob.js +32 -0
- package/dist/harness.js +229 -0
- package/dist/history.js +35 -0
- package/dist/install.js +133 -0
- package/dist/journal.js +43 -0
- package/dist/junit.js +54 -0
- package/dist/lock.js +37 -0
- package/dist/matcher.js +52 -0
- package/dist/model.js +73 -0
- package/dist/needs.js +34 -0
- package/dist/opener.js +38 -0
- package/dist/pin.js +28 -0
- package/dist/probe.js +14 -0
- package/dist/refusal.js +16 -0
- package/dist/reviewed.js +69 -0
- package/dist/reviewer.js +133 -0
- package/dist/rounds.js +112 -0
- package/dist/runner.js +73 -0
- package/dist/scan.js +67 -0
- package/dist/schemas.js +119 -0
- package/dist/sha.js +27 -0
- package/dist/ship.js +257 -0
- package/dist/stamp.js +104 -0
- package/dist/toon.js +16 -0
- package/dist/txn.js +70 -0
- package/dist/verbs/abandon.js +105 -0
- package/dist/verbs/adopt.js +118 -0
- package/dist/verbs/calibrate.js +130 -0
- package/dist/verbs/check.js +233 -0
- package/dist/verbs/clean.js +34 -0
- package/dist/verbs/dashboard.js +172 -0
- package/dist/verbs/decide.js +309 -0
- package/dist/verbs/design.js +203 -0
- package/dist/verbs/diff.js +60 -0
- package/dist/verbs/dispatch.js +102 -0
- package/dist/verbs/evidence.js +118 -0
- package/dist/verbs/gate.js +12 -0
- package/dist/verbs/index-verb.js +34 -0
- package/dist/verbs/init.js +190 -0
- package/dist/verbs/log.js +151 -0
- package/dist/verbs/next.js +437 -0
- package/dist/verbs/recap.js +145 -0
- package/dist/verbs/recover.js +52 -0
- package/dist/verbs/rename.js +99 -0
- package/dist/verbs/satisfy.js +69 -0
- package/dist/verbs/ship.js +11 -0
- package/dist/verbs/start.js +133 -0
- package/dist/verbs/sync.js +45 -0
- package/dist/verbs/write.js +349 -0
- package/dist/verdict.js +257 -0
- package/dist/worktree.js +73 -0
- package/package.json +70 -0
- package/plugin/.claude-plugin/plugin.json +19 -0
- package/plugin/commands/witness.md +46 -0
- package/plugin/hooks/canon-guard.mjs +105 -0
- package/plugin/hooks/guard-state.mjs +34 -0
- package/plugin/hooks/hooks.json +17 -0
- package/plugin/hooks/session-dashboard.sh +6 -0
- package/plugin/hooks/witness-pi.ts +67 -0
- package/plugin/skills/witness-brainstorm/SKILL.md +70 -0
- package/plugin/skills/witness-decompose/SKILL.md +99 -0
- package/plugin/skills/witness-design/SKILL.md +83 -0
- package/plugin/skills/witness-implement/SKILL.md +86 -0
- package/plugin/skills/witness-plan/SKILL.md +93 -0
- package/plugin/skills/witness-ship/SKILL.md +63 -0
- package/prompts/code-reviewer.md +64 -0
- package/prompts/design-critic.md +35 -0
- package/prompts/design-reviewer.md +70 -0
- package/prompts/drift-reviewer.md +40 -0
- package/prompts/plan-critic.md +37 -0
- package/prompts/pr-test.md +97 -0
- package/prompts/silent-failure-hunter.md +145 -0
- package/prompts/slicing-critic.md +46 -0
- package/prompts/type-design.md +113 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { hostname, userInfo } from 'node:os';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { parseArgs } from 'node:util';
|
|
5
|
+
import { EXIT } from '../cli.js';
|
|
6
|
+
import { loadConfig } from '../config.js';
|
|
7
|
+
import { designPending, designRel, designStamp, htmlSha, validateDesignArtifact } from '../design.js';
|
|
8
|
+
import { writeDoc } from '../fm.js';
|
|
9
|
+
import { dirtyStatePaths, primaryRoot, stateCommit } from '../gitio.js';
|
|
10
|
+
import { appendEntry, entryLine, journalRel, latestRecap } from '../journal.js';
|
|
11
|
+
import { acquireLock } from '../lock.js';
|
|
12
|
+
import { openArtifact } from '../opener.js';
|
|
13
|
+
import { renderRefusal, v } from '../refusal.js';
|
|
14
|
+
import { effortOf } from '../reviewed.js';
|
|
15
|
+
import { findById, loadCanon } from '../scan.js';
|
|
16
|
+
import { canonicalSha, short } from '../sha.js';
|
|
17
|
+
import { kv } from '../toon.js';
|
|
18
|
+
import { crashPoint, guardTxn, withTxn } from '../txn.js';
|
|
19
|
+
export async function run(ctx, argv) {
|
|
20
|
+
const { values, positionals } = parseArgs({
|
|
21
|
+
args: argv, allowPositionals: true,
|
|
22
|
+
options: { file: { type: 'string' }, reconfirm: { type: 'boolean' }, open: { type: 'boolean' } },
|
|
23
|
+
});
|
|
24
|
+
const specId = positionals[0];
|
|
25
|
+
if (!specId) {
|
|
26
|
+
ctx.err('usage: witness design <spec-id> --file <html> | --reconfirm | --open');
|
|
27
|
+
return EXIT.REFUSED;
|
|
28
|
+
}
|
|
29
|
+
const rootR = primaryRoot(ctx.cwd);
|
|
30
|
+
if (!rootR.ok) {
|
|
31
|
+
renderRefusal(rootR.violations).forEach(ctx.err);
|
|
32
|
+
return EXIT.REFUSED;
|
|
33
|
+
}
|
|
34
|
+
const root = rootR.value;
|
|
35
|
+
const blocked = guardTxn(ctx, root);
|
|
36
|
+
if (blocked !== undefined)
|
|
37
|
+
return blocked;
|
|
38
|
+
const cfgR = loadConfig(root);
|
|
39
|
+
if (!cfgR.ok) {
|
|
40
|
+
renderRefusal(cfgR.violations).forEach(ctx.err);
|
|
41
|
+
return EXIT.REFUSED;
|
|
42
|
+
}
|
|
43
|
+
const canon = loadCanon(root);
|
|
44
|
+
const spec = findById(canon, specId);
|
|
45
|
+
if (!spec || spec.meta.type !== 'spec') {
|
|
46
|
+
renderRefusal([v('spec', 'unknown-spec', specId, 'a specs/ doc id')]).forEach(ctx.err);
|
|
47
|
+
return EXIT.REFUSED;
|
|
48
|
+
}
|
|
49
|
+
if (values.open)
|
|
50
|
+
return openOnly(ctx, root, designRel(cfgR.value.paths, specId), specId);
|
|
51
|
+
if (spec.meta.ui !== true) {
|
|
52
|
+
renderRefusal([v('spec', 'not-ui', specId, 'a ui-flagged spec — set ui: true at decompose to earn a design stage')]).forEach(ctx.err);
|
|
53
|
+
return EXIT.REFUSED;
|
|
54
|
+
}
|
|
55
|
+
const effort = effortOf(root, specId);
|
|
56
|
+
if (!effort || latestRecap(root, effort)?.class !== 'feature') {
|
|
57
|
+
renderRefusal([v('effort', 'not-feature', String(effort ?? 'none'), 'a feature-class effort — fix/chore specs ride straight to plan')]).forEach(ctx.err);
|
|
58
|
+
return EXIT.REFUSED;
|
|
59
|
+
}
|
|
60
|
+
const rel = designRel(cfgR.value.paths, specId);
|
|
61
|
+
const stream = journalRel(specId);
|
|
62
|
+
const specSha = canonicalSha(spec.meta, spec.body);
|
|
63
|
+
const lock = acquireLock(root);
|
|
64
|
+
if (!lock.ok) {
|
|
65
|
+
renderRefusal(lock.violations).forEach(ctx.err);
|
|
66
|
+
return EXIT.BLOCKED;
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
if (values.reconfirm)
|
|
70
|
+
return reconfirm(ctx, root, spec, specSha);
|
|
71
|
+
return persist(ctx, root, spec, effort, rel, stream, specSha, values.file);
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
lock.value();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// The one place anything is ever spawned at a human. Journals a sha-keyed witness that
|
|
78
|
+
// `gate design`, `decide --approve` and `next` all consult. Deliberately reachable for
|
|
79
|
+
// any registered artifact — re-showing a file that exists needs no ui/feature eligibility,
|
|
80
|
+
// and gating it on those would dead-end the very refusals that point here.
|
|
81
|
+
function openOnly(ctx, root, rel, id) {
|
|
82
|
+
const abs = join(root, rel);
|
|
83
|
+
if (!existsSync(abs)) {
|
|
84
|
+
renderRefusal([v('design', 'no-artifact', rel, `a registered design — run: witness design ${id} --file <html>`)]).forEach(ctx.err);
|
|
85
|
+
return EXIT.REFUSED;
|
|
86
|
+
}
|
|
87
|
+
const sha = htmlSha(readFileSync(abs, 'utf8'));
|
|
88
|
+
const { outcome, command } = openArtifact(ctx.env, abs);
|
|
89
|
+
if (outcome === 'failed') {
|
|
90
|
+
renderRefusal([v('design', 'opener-failed', `${command} did not resolve`, `a working platform opener, or WITNESS_OPENER — meanwhile open it yourself: file://${abs}`)]).forEach(ctx.err);
|
|
91
|
+
return EXIT.REFUSED;
|
|
92
|
+
}
|
|
93
|
+
// `by` is the account and machine the spawn happened on — not a claim about who looked.
|
|
94
|
+
// It answers "was this shown on my box or someone else's" on a shared branch, which is
|
|
95
|
+
// the most the CLI can honestly know.
|
|
96
|
+
const by = `${userInfo().username}@${hostname()}`;
|
|
97
|
+
const stream = journalRel(id);
|
|
98
|
+
const entry = { t: 'design-shown', artifact: id, sha, opener: command, by };
|
|
99
|
+
const lock = acquireLock(root);
|
|
100
|
+
if (!lock.ok) {
|
|
101
|
+
renderRefusal(lock.violations).forEach(ctx.err);
|
|
102
|
+
return EXIT.BLOCKED;
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
const res = withTxn(root, {
|
|
106
|
+
op: `design-open(${id})`, files: [stream], journal: { stream: id, line: entryLine(entry) },
|
|
107
|
+
}, () => {
|
|
108
|
+
appendEntry(root, id, entry);
|
|
109
|
+
crashPoint(ctx.env, 'design-shown');
|
|
110
|
+
return stateCommit(root, [stream], `design-shown(${id}): ${short(sha)}`);
|
|
111
|
+
});
|
|
112
|
+
if (!res.ok) {
|
|
113
|
+
renderRefusal(res.violations).forEach(ctx.err);
|
|
114
|
+
return EXIT.REFUSED;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
lock.value();
|
|
119
|
+
}
|
|
120
|
+
ctx.out(kv('shown', id));
|
|
121
|
+
ctx.out(kv('path', rel));
|
|
122
|
+
ctx.out(kv('sha', short(sha)));
|
|
123
|
+
ctx.out(kv('opener', command));
|
|
124
|
+
ctx.out(`next: witness gate design ${id}`);
|
|
125
|
+
return EXIT.OK;
|
|
126
|
+
}
|
|
127
|
+
function persist(ctx, root, spec, effort, rel, stream, specSha, file) {
|
|
128
|
+
if (!file) {
|
|
129
|
+
renderRefusal([v('--file', 'required', 'absent', 'a scratch HTML file — or --reconfirm')]).forEach(ctx.err);
|
|
130
|
+
return EXIT.REFUSED;
|
|
131
|
+
}
|
|
132
|
+
let html;
|
|
133
|
+
try {
|
|
134
|
+
html = readFileSync(resolve(ctx.cwd, file), 'utf8');
|
|
135
|
+
}
|
|
136
|
+
catch (e) {
|
|
137
|
+
renderRefusal([v('--file', 'input-unreadable', String(e.message).slice(0, 120), 'a readable HTML file')]).forEach(ctx.err);
|
|
138
|
+
return EXIT.REFUSED;
|
|
139
|
+
}
|
|
140
|
+
const problems = validateDesignArtifact(html);
|
|
141
|
+
if (problems.length) {
|
|
142
|
+
renderRefusal(problems).forEach(ctx.err);
|
|
143
|
+
return EXIT.REFUSED;
|
|
144
|
+
}
|
|
145
|
+
const unrelated = dirtyStatePaths(root).filter((p) => p !== rel && p !== stream);
|
|
146
|
+
if (unrelated.length) {
|
|
147
|
+
renderRefusal(unrelated.map((p) => v(p, 'unrelated-dirty', 'uncommitted change on a state path', 'revert or adopt it, then re-run'))).forEach(ctx.err);
|
|
148
|
+
return EXIT.REFUSED;
|
|
149
|
+
}
|
|
150
|
+
const sha = htmlSha(html);
|
|
151
|
+
const id = String(spec.meta.id);
|
|
152
|
+
const entry = { t: 'design-write', effort, artifact: id, sha, spec: specSha };
|
|
153
|
+
const res = withTxn(root, {
|
|
154
|
+
op: `design(${id})`, files: [rel, stream], journal: { stream: id, line: entryLine(entry) },
|
|
155
|
+
}, () => {
|
|
156
|
+
mkdirSync(join(root, dirname(rel)), { recursive: true });
|
|
157
|
+
writeFileSync(join(root, rel), html);
|
|
158
|
+
crashPoint(ctx.env, 'design-artifact');
|
|
159
|
+
appendEntry(root, id, entry);
|
|
160
|
+
crashPoint(ctx.env, 'design-journal');
|
|
161
|
+
return stateCommit(root, [rel, stream], `design(${id}): ${short(sha)}`);
|
|
162
|
+
});
|
|
163
|
+
if (!res.ok) {
|
|
164
|
+
renderRefusal(res.violations).forEach(ctx.err);
|
|
165
|
+
return EXIT.REFUSED;
|
|
166
|
+
}
|
|
167
|
+
ctx.out(kv('design', id));
|
|
168
|
+
ctx.out(kv('path', rel));
|
|
169
|
+
ctx.out(kv('sha', short(sha)));
|
|
170
|
+
ctx.out(`next: witness gate design ${id}`);
|
|
171
|
+
return EXIT.OK;
|
|
172
|
+
}
|
|
173
|
+
function reconfirm(ctx, root, spec, specSha) {
|
|
174
|
+
const id = String(spec.meta.id);
|
|
175
|
+
const stamp = designStamp(spec);
|
|
176
|
+
if (!stamp) {
|
|
177
|
+
renderRefusal([v('design', 'no-stamp', id, 'an approved design (witness gate design <id>) — nothing to reconfirm')]).forEach(ctx.err);
|
|
178
|
+
return EXIT.REFUSED;
|
|
179
|
+
}
|
|
180
|
+
if (!designPending(root, spec)) {
|
|
181
|
+
renderRefusal([v('design', 'not-pending', 'stamp already current', 'a stale stamp — reconfirm applies only after a feature amendment with no visual delta')]).forEach(ctx.err);
|
|
182
|
+
return EXIT.REFUSED;
|
|
183
|
+
}
|
|
184
|
+
const stream = journalRel(id);
|
|
185
|
+
const nextMeta = { ...spec.meta, design: { sha: stamp.sha, spec: specSha } };
|
|
186
|
+
const entry = { t: 'design-reconfirm', artifact: id, sha: stamp.sha, spec: specSha };
|
|
187
|
+
const res = withTxn(root, {
|
|
188
|
+
op: `design-reconfirm(${id})`, files: [spec.rel, stream],
|
|
189
|
+
journal: { stream: id, line: entryLine(entry) },
|
|
190
|
+
}, () => {
|
|
191
|
+
writeDoc(join(root, spec.rel), { meta: nextMeta, body: spec.body });
|
|
192
|
+
appendEntry(root, id, entry);
|
|
193
|
+
crashPoint(ctx.env, 'design-reconfirm');
|
|
194
|
+
return stateCommit(root, [spec.rel, stream], `design-reconfirm(${id}): no visual delta`);
|
|
195
|
+
});
|
|
196
|
+
if (!res.ok) {
|
|
197
|
+
renderRefusal(res.violations).forEach(ctx.err);
|
|
198
|
+
return EXIT.REFUSED;
|
|
199
|
+
}
|
|
200
|
+
ctx.out(kv('reconfirmed', id));
|
|
201
|
+
ctx.out(kv('design', `re-pinned to ${short(specSha)} (artifact unchanged)`));
|
|
202
|
+
return EXIT.OK;
|
|
203
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { mkdtempSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { EXIT } from '../cli.js';
|
|
5
|
+
import { serializeDoc } from '../fm.js';
|
|
6
|
+
import { primaryRoot, tryGit } from '../gitio.js';
|
|
7
|
+
import { baseForSpec, contentAtSha } from '../history.js';
|
|
8
|
+
import { renderRefusal, v } from '../refusal.js';
|
|
9
|
+
import { findById, loadCanon } from '../scan.js';
|
|
10
|
+
import { canonicalSha, short, VOLATILE_FIELDS } from '../sha.js';
|
|
11
|
+
import { kv, rows } from '../toon.js';
|
|
12
|
+
function canonicalRender(meta, body) {
|
|
13
|
+
const kept = Object.fromEntries(Object.entries(meta).filter(([k]) => !VOLATILE_FIELDS.includes(k)));
|
|
14
|
+
return serializeDoc({ meta: kept, body });
|
|
15
|
+
}
|
|
16
|
+
export async function run(ctx, argv) {
|
|
17
|
+
const id = argv[0];
|
|
18
|
+
if (!id) {
|
|
19
|
+
renderRefusal([v('id', 'required', 'absent', 'witness diff <spec-id>')]).forEach(ctx.err);
|
|
20
|
+
return EXIT.REFUSED;
|
|
21
|
+
}
|
|
22
|
+
const rootRes = primaryRoot(ctx.cwd);
|
|
23
|
+
if (!rootRes.ok) {
|
|
24
|
+
renderRefusal(rootRes.violations).forEach(ctx.err);
|
|
25
|
+
return EXIT.REFUSED;
|
|
26
|
+
}
|
|
27
|
+
const root = rootRes.value;
|
|
28
|
+
const canon = loadCanon(root);
|
|
29
|
+
const spec = findById(canon, id);
|
|
30
|
+
if (!spec || spec.meta.type !== 'spec') {
|
|
31
|
+
renderRefusal([v('id', 'unknown-spec', id, 'a spec id from witness index')]).forEach(ctx.err);
|
|
32
|
+
return EXIT.REFUSED;
|
|
33
|
+
}
|
|
34
|
+
const current = canonicalSha(spec.meta, spec.body);
|
|
35
|
+
const base = baseForSpec(root, canon, id);
|
|
36
|
+
if (base.kind === 'empty') {
|
|
37
|
+
ctx.out(kv('base', 'empty (never realized)'));
|
|
38
|
+
ctx.out(kv('sha', short(current)));
|
|
39
|
+
ctx.out('delta: full content is the delta — new spec');
|
|
40
|
+
return EXIT.OK;
|
|
41
|
+
}
|
|
42
|
+
ctx.out(kv('base', `plan-pin ${base.planId} @ ${short(base.sha)}`));
|
|
43
|
+
if (base.sha === current) {
|
|
44
|
+
ctx.out('delta: none — no delta since the last plan');
|
|
45
|
+
return EXIT.OK;
|
|
46
|
+
}
|
|
47
|
+
const baseDoc = contentAtSha(root, spec.rel, base.sha);
|
|
48
|
+
if (!baseDoc) {
|
|
49
|
+
rows('findings', ['field', 'rule', 'detail'], [
|
|
50
|
+
{ field: spec.rel, rule: 'pin-unresolvable', detail: `no committed version of ${id} hashes to ${short(base.sha)}` },
|
|
51
|
+
]).forEach(ctx.out);
|
|
52
|
+
return EXIT.FINDINGS;
|
|
53
|
+
}
|
|
54
|
+
const tmp = mkdtempSync(join(tmpdir(), 'witness-diff-'));
|
|
55
|
+
writeFileSync(join(tmp, 'base.md'), canonicalRender(baseDoc.meta, baseDoc.body));
|
|
56
|
+
writeFileSync(join(tmp, 'current.md'), canonicalRender(spec.meta, spec.body));
|
|
57
|
+
const diff = tryGit(root, 'diff', '--no-index', '--', join(tmp, 'base.md'), join(tmp, 'current.md'));
|
|
58
|
+
ctx.out(diff.out);
|
|
59
|
+
return EXIT.OK;
|
|
60
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { EXIT } from '../cli.js';
|
|
2
|
+
import { acquireLock } from '../lock.js';
|
|
3
|
+
import { guardTxn, withTxn } from '../txn.js';
|
|
4
|
+
import { appendEntry, entryLine, journalRel, readStream } from '../journal.js';
|
|
5
|
+
import { primaryRoot, stateCommit } from '../gitio.js';
|
|
6
|
+
import { loadConfig } from '../config.js';
|
|
7
|
+
import { relayLine, resolveHarness } from '../harness.js';
|
|
8
|
+
import { findById, loadCanon } from '../scan.js';
|
|
9
|
+
import { renderRefusal, v } from '../refusal.js';
|
|
10
|
+
import { kv } from '../toon.js';
|
|
11
|
+
const FLAGS = ['--steps-assigned', '--steps-completed', '--tokens', '--tool-uses', '--duration-ms'];
|
|
12
|
+
function intFlag(argv, flag, violations, required) {
|
|
13
|
+
const at = argv.indexOf(flag);
|
|
14
|
+
if (at === -1) {
|
|
15
|
+
if (required)
|
|
16
|
+
violations.push(v(flag.slice(2), 'required', 'absent', 'a non-negative integer'));
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
const n = Number(argv[at + 1]);
|
|
20
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
21
|
+
violations.push(v(flag.slice(2), 'invalid', String(argv[at + 1]), 'a non-negative integer'));
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
return n;
|
|
25
|
+
}
|
|
26
|
+
// Row 81: telemetry, not evidence. The numbers are the orchestrator's report of the
|
|
27
|
+
// Agent-tool return block — labeled reported, and no gate may ever consume this entry.
|
|
28
|
+
export async function run(ctx, argv) {
|
|
29
|
+
const planId = argv.find((a) => !a.startsWith('--') && !FLAGS.includes(argv[argv.indexOf(a) - 1]));
|
|
30
|
+
const violations = [];
|
|
31
|
+
const assigned = intFlag(argv, '--steps-assigned', violations, true);
|
|
32
|
+
const completed = intFlag(argv, '--steps-completed', violations, true);
|
|
33
|
+
const tokens = intFlag(argv, '--tokens', violations, false);
|
|
34
|
+
const toolUses = intFlag(argv, '--tool-uses', violations, false);
|
|
35
|
+
const durationMs = intFlag(argv, '--duration-ms', violations, false);
|
|
36
|
+
if (!planId)
|
|
37
|
+
violations.push(v('plan-id', 'required', 'absent', 'witness dispatch-report <plan-id> --steps-assigned <n> --steps-completed <n>'));
|
|
38
|
+
if (violations.length) {
|
|
39
|
+
renderRefusal(violations).forEach(ctx.err);
|
|
40
|
+
return EXIT.REFUSED;
|
|
41
|
+
}
|
|
42
|
+
const rootR = primaryRoot(ctx.cwd);
|
|
43
|
+
if (!rootR.ok) {
|
|
44
|
+
renderRefusal(rootR.violations).forEach(ctx.err);
|
|
45
|
+
return EXIT.REFUSED;
|
|
46
|
+
}
|
|
47
|
+
const root = rootR.value;
|
|
48
|
+
const blocked = guardTxn(ctx, root);
|
|
49
|
+
if (blocked !== undefined)
|
|
50
|
+
return blocked;
|
|
51
|
+
const plan = findById(loadCanon(root), planId);
|
|
52
|
+
if (!plan || plan.meta.type !== 'plan') {
|
|
53
|
+
renderRefusal([v('plan-id', 'unknown-plan', planId, 'an existing plan id')]).forEach(ctx.err);
|
|
54
|
+
return EXIT.REFUSED;
|
|
55
|
+
}
|
|
56
|
+
const lock = acquireLock(root);
|
|
57
|
+
if (!lock.ok) {
|
|
58
|
+
renderRefusal(lock.violations).forEach(ctx.err);
|
|
59
|
+
return EXIT.BLOCKED;
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const ordinal = readStream(root, planId).filter((e) => e.t === 'dispatch').length + 1;
|
|
63
|
+
const usage = tokens !== undefined || toolUses !== undefined || durationMs !== undefined
|
|
64
|
+
? {
|
|
65
|
+
...(tokens !== undefined ? { tokens } : {}),
|
|
66
|
+
...(toolUses !== undefined ? { tool_uses: toolUses } : {}),
|
|
67
|
+
...(durationMs !== undefined ? { duration_ms: durationMs } : {}),
|
|
68
|
+
}
|
|
69
|
+
: undefined;
|
|
70
|
+
const entry = {
|
|
71
|
+
t: 'dispatch', plan: planId, ordinal,
|
|
72
|
+
steps_assigned: assigned, steps_completed: completed,
|
|
73
|
+
...(usage ? { usage } : {}), reported: true,
|
|
74
|
+
};
|
|
75
|
+
const marker = {
|
|
76
|
+
op: `dispatch-report(${planId})`,
|
|
77
|
+
files: [journalRel(planId)],
|
|
78
|
+
journalMulti: [{ stream: planId, line: entryLine(entry) }],
|
|
79
|
+
};
|
|
80
|
+
const txn = withTxn(root, marker, () => {
|
|
81
|
+
appendEntry(root, planId, entry);
|
|
82
|
+
return stateCommit(root, marker.files, marker.op);
|
|
83
|
+
});
|
|
84
|
+
if (!txn.ok) {
|
|
85
|
+
renderRefusal(txn.violations).forEach(ctx.err);
|
|
86
|
+
return EXIT.REFUSED;
|
|
87
|
+
}
|
|
88
|
+
ctx.out(kv('dispatch', `${planId} · #${ordinal} · ${completed}/${assigned} step(s)`));
|
|
89
|
+
// The relay is a session-boundary fact the skill must not hardcode: /clear on
|
|
90
|
+
// Claude Code, /new on Pi. A broken config must not cost the telemetry entry that
|
|
91
|
+
// was already journalled, so an unresolvable harness simply omits the line and the
|
|
92
|
+
// skill stops for the human.
|
|
93
|
+
const cfgR = loadConfig(root);
|
|
94
|
+
const hxR = resolveHarness(ctx.env, cfgR.ok ? cfgR.value.raw : {});
|
|
95
|
+
if (hxR.ok)
|
|
96
|
+
ctx.out(kv('relay', relayLine(hxR.value.harness)));
|
|
97
|
+
return EXIT.OK;
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
lock.ok && lock.value();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { EXIT } from '../cli.js';
|
|
2
|
+
import { checkoutRoot, diffBase, recordEvidence, verifyRed } from '../evidence.js';
|
|
3
|
+
import { loadConfig } from '../config.js';
|
|
4
|
+
import { primaryRoot } from '../gitio.js';
|
|
5
|
+
import { renderRefusal, v } from '../refusal.js';
|
|
6
|
+
import { findById, loadCanon } from '../scan.js';
|
|
7
|
+
import { kv } from '../toon.js';
|
|
8
|
+
import { guardTxn } from '../txn.js';
|
|
9
|
+
import { acquireLock } from '../lock.js';
|
|
10
|
+
export async function run(ctx, argv) {
|
|
11
|
+
const [planId] = argv.filter((a) => !a.startsWith('--'));
|
|
12
|
+
const phase = argv[argv.indexOf('--phase') + 1];
|
|
13
|
+
if (!planId || (phase !== 'red' && phase !== 'green')) {
|
|
14
|
+
renderRefusal([v('args', 'usage', argv.join(' '), 'witness test-evidence <plan-id> --phase red|green')]).forEach(ctx.err);
|
|
15
|
+
return EXIT.REFUSED;
|
|
16
|
+
}
|
|
17
|
+
const runRootRes = checkoutRoot(ctx.cwd);
|
|
18
|
+
const stateRootRes = primaryRoot(ctx.cwd);
|
|
19
|
+
if (!runRootRes.ok || !stateRootRes.ok) {
|
|
20
|
+
renderRefusal([...(runRootRes.ok ? [] : runRootRes.violations), ...(stateRootRes.ok ? [] : stateRootRes.violations)]).forEach(ctx.err);
|
|
21
|
+
return EXIT.REFUSED;
|
|
22
|
+
}
|
|
23
|
+
const stateRoot = stateRootRes.value;
|
|
24
|
+
const blocked = guardTxn(ctx, stateRoot);
|
|
25
|
+
if (blocked !== undefined)
|
|
26
|
+
return blocked;
|
|
27
|
+
const plan = findById(loadCanon(stateRoot), planId);
|
|
28
|
+
if (!plan || plan.meta.type !== 'plan') {
|
|
29
|
+
renderRefusal([v('plan-id', 'unknown-plan', planId, 'an existing plan id')]).forEach(ctx.err);
|
|
30
|
+
return EXIT.REFUSED;
|
|
31
|
+
}
|
|
32
|
+
const parentId = String(plan.meta.parent);
|
|
33
|
+
const lock = acquireLock(stateRoot);
|
|
34
|
+
if (!lock.ok) {
|
|
35
|
+
renderRefusal(lock.violations).forEach(ctx.err);
|
|
36
|
+
return EXIT.BLOCKED;
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const probe = await recordEvidence(runRootRes.value, stateRoot, ctx, planId, parentId, phase);
|
|
40
|
+
if (!probe.ok) {
|
|
41
|
+
renderRefusal(probe.violations).forEach(ctx.err);
|
|
42
|
+
return EXIT.REFUSED;
|
|
43
|
+
}
|
|
44
|
+
const { allOk, tests } = probe.value;
|
|
45
|
+
ctx.out(kv('test-evidence', `${planId} · ${phase} · ${tests.length} outcome(s) · ${allOk ? 'all green' : 'red present'}`));
|
|
46
|
+
if (phase === 'red' && allOk) {
|
|
47
|
+
ctx.err('vacuous: tests pass before implementation — the test may assert nothing, or the behavior already exists');
|
|
48
|
+
return EXIT.FINDINGS;
|
|
49
|
+
}
|
|
50
|
+
if (phase === 'green' && !allOk) {
|
|
51
|
+
ctx.err('not green — fix the implementation and re-run');
|
|
52
|
+
return EXIT.FINDINGS;
|
|
53
|
+
}
|
|
54
|
+
return EXIT.OK;
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
lock.ok && lock.value();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export async function runVerifyRed(ctx, argv) {
|
|
61
|
+
const [planId] = argv.filter((a) => !a.startsWith('--'));
|
|
62
|
+
const baseArg = argv.includes('--base') ? argv[argv.indexOf('--base') + 1] : undefined;
|
|
63
|
+
if (!planId) {
|
|
64
|
+
renderRefusal([v('args', 'usage', argv.join(' '), 'witness verify-red <plan-id> [--base <ref>]')]).forEach(ctx.err);
|
|
65
|
+
return EXIT.REFUSED;
|
|
66
|
+
}
|
|
67
|
+
const runRootRes = checkoutRoot(ctx.cwd);
|
|
68
|
+
const stateRootRes = primaryRoot(ctx.cwd);
|
|
69
|
+
if (!runRootRes.ok || !stateRootRes.ok) {
|
|
70
|
+
renderRefusal([...(runRootRes.ok ? [] : runRootRes.violations), ...(stateRootRes.ok ? [] : stateRootRes.violations)]).forEach(ctx.err);
|
|
71
|
+
return EXIT.REFUSED;
|
|
72
|
+
}
|
|
73
|
+
const stateRoot = stateRootRes.value;
|
|
74
|
+
const blocked = guardTxn(ctx, stateRoot);
|
|
75
|
+
if (blocked !== undefined)
|
|
76
|
+
return blocked;
|
|
77
|
+
const plan = findById(loadCanon(stateRoot), planId);
|
|
78
|
+
if (!plan || plan.meta.type !== 'plan') {
|
|
79
|
+
renderRefusal([v('plan-id', 'unknown-plan', planId, 'an existing plan id')]).forEach(ctx.err);
|
|
80
|
+
return EXIT.REFUSED;
|
|
81
|
+
}
|
|
82
|
+
const cfg = loadConfig(runRootRes.value);
|
|
83
|
+
if (!cfg.ok) {
|
|
84
|
+
renderRefusal(cfg.violations).forEach(ctx.err);
|
|
85
|
+
return EXIT.REFUSED;
|
|
86
|
+
}
|
|
87
|
+
const base = diffBase(runRootRes.value, cfg.value, baseArg);
|
|
88
|
+
if (!base.ok) {
|
|
89
|
+
renderRefusal(base.violations).forEach(ctx.err);
|
|
90
|
+
return EXIT.REFUSED;
|
|
91
|
+
}
|
|
92
|
+
const lock = acquireLock(stateRoot);
|
|
93
|
+
if (!lock.ok) {
|
|
94
|
+
renderRefusal(lock.violations).forEach(ctx.err);
|
|
95
|
+
return EXIT.BLOCKED;
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const res = await verifyRed(runRootRes.value, stateRoot, ctx, planId, String(plan.meta.parent), base.value);
|
|
99
|
+
if (!res.ok) {
|
|
100
|
+
renderRefusal(res.violations).forEach(ctx.err);
|
|
101
|
+
return EXIT.REFUSED;
|
|
102
|
+
}
|
|
103
|
+
const { redOk, greenOk, vacuous } = res.value;
|
|
104
|
+
ctx.out(kv('verify-red', `red ${redOk ? 'observed' : 'NOT observed'} · green ${greenOk ? 'confirmed' : 'NOT confirmed'}`));
|
|
105
|
+
if (vacuous) {
|
|
106
|
+
ctx.err('vacuous: tests pass against base — the test may assert nothing, or the behavior already existed');
|
|
107
|
+
return EXIT.FINDINGS;
|
|
108
|
+
}
|
|
109
|
+
if (!greenOk) {
|
|
110
|
+
ctx.err('suite not green after restore — repository state was restored; investigate before trusting this diff');
|
|
111
|
+
return EXIT.FINDINGS;
|
|
112
|
+
}
|
|
113
|
+
return EXIT.OK;
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
lock.ok && lock.value();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { EXIT } from '../cli.js';
|
|
2
|
+
import '../gates/index.js';
|
|
3
|
+
import { runGate } from '../gate.js';
|
|
4
|
+
export async function run(ctx, argv) {
|
|
5
|
+
const positional = argv.filter((a) => !a.startsWith('--'));
|
|
6
|
+
const [gate, target] = positional;
|
|
7
|
+
if (!gate || !target) {
|
|
8
|
+
ctx.err('usage: witness gate <decompose|plan|implement|ship|design> <target> [--fresh] [--manual]');
|
|
9
|
+
return EXIT.REFUSED;
|
|
10
|
+
}
|
|
11
|
+
return runGate(ctx, gate, target, { fresh: argv.includes('--fresh'), manual: argv.includes('--manual') });
|
|
12
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { dirname } from 'node:path';
|
|
2
|
+
import { EXIT } from '../cli.js';
|
|
3
|
+
import { primaryRoot } from '../gitio.js';
|
|
4
|
+
import { renderRefusal } from '../refusal.js';
|
|
5
|
+
import { loadCanon } from '../scan.js';
|
|
6
|
+
import { rows } from '../toon.js';
|
|
7
|
+
export async function run(ctx) {
|
|
8
|
+
const rootRes = primaryRoot(ctx.cwd);
|
|
9
|
+
if (!rootRes.ok) {
|
|
10
|
+
renderRefusal(rootRes.violations).forEach(ctx.err);
|
|
11
|
+
return EXIT.REFUSED;
|
|
12
|
+
}
|
|
13
|
+
const canon = loadCanon(rootRes.value);
|
|
14
|
+
const specs = canon.docs.filter((d) => d.rel.startsWith(`${canon.paths.specs}/`));
|
|
15
|
+
const groups = new Map();
|
|
16
|
+
for (const doc of specs) {
|
|
17
|
+
const dir = dirname(doc.rel);
|
|
18
|
+
if (!groups.has(dir))
|
|
19
|
+
groups.set(dir, []);
|
|
20
|
+
groups.get(dir).push(doc);
|
|
21
|
+
}
|
|
22
|
+
for (const dir of [...groups.keys()].sort()) {
|
|
23
|
+
const items = groups.get(dir).sort((a, b) => String(a.meta.id).localeCompare(String(b.meta.id)));
|
|
24
|
+
rows(dir, ['id', 'summary', 'ui', 'status', 'depends'], items.map((d) => ({
|
|
25
|
+
id: d.meta.id,
|
|
26
|
+
summary: d.meta.summary ?? '',
|
|
27
|
+
ui: d.meta.ui === true ? 'ui' : '',
|
|
28
|
+
status: d.meta.status,
|
|
29
|
+
depends: Array.isArray(d.meta.depends) ? d.meta.depends.join(' ') : '',
|
|
30
|
+
}))).forEach(ctx.out);
|
|
31
|
+
}
|
|
32
|
+
ctx.out('help: witness diff <id> · witness log <id>');
|
|
33
|
+
return EXIT.OK;
|
|
34
|
+
}
|