@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,144 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { ok, refuse, v } from '../refusal.js';
|
|
5
|
+
import { serializeDoc } from '../fm.js';
|
|
6
|
+
import { canonicalSha } from '../sha.js';
|
|
7
|
+
import { designRel } from '../design.js';
|
|
8
|
+
import { git } from '../gitio.js';
|
|
9
|
+
import { latestRecap, readStream } from '../journal.js';
|
|
10
|
+
import { findById } from '../scan.js';
|
|
11
|
+
import { changedFiles, diffBase, evidenceForDiff, screensDir } from '../evidence.js';
|
|
12
|
+
import { runCriteria } from '../criteria.js';
|
|
13
|
+
import { effortOf, worktreeTreeSha } from '../reviewed.js';
|
|
14
|
+
import { worktreePath } from '../worktree.js';
|
|
15
|
+
import { registerGate } from '../gate.js';
|
|
16
|
+
export function codePromptBody(wt, base, files, header) {
|
|
17
|
+
const diff = git(wt, 'diff', base);
|
|
18
|
+
const untracked = files.filter((f) => {
|
|
19
|
+
const abs = join(wt, f);
|
|
20
|
+
if (!existsSync(abs) || !statSync(abs).isFile())
|
|
21
|
+
return false;
|
|
22
|
+
return git(wt, 'ls-files', '--', f).trim() === '';
|
|
23
|
+
});
|
|
24
|
+
const untrackedBlocks = untracked.map((f) => `#### untracked: ${f}\n${readFileSync(join(wt, f), 'utf8')}`);
|
|
25
|
+
return [
|
|
26
|
+
header,
|
|
27
|
+
`### Changed files\n${files.join('\n')}`,
|
|
28
|
+
`### Diff vs base ${base.slice(0, 7)}\n${diff}`,
|
|
29
|
+
...untrackedBlocks,
|
|
30
|
+
].join('\n\n');
|
|
31
|
+
}
|
|
32
|
+
// A UI plan (design-from pinned) must ship screenshots the CLI witnessed: ≥1 capture
|
|
33
|
+
// from the latest green evidence cycle whose on-disk bytes still match the journaled sha.
|
|
34
|
+
// Zero verified → screens-matched-nothing (the D67 filter-matched-nothing pattern) before
|
|
35
|
+
// a reviewer spends tokens; bytes-drift → capture-sha-mismatch (a hand-swapped screenshot
|
|
36
|
+
// is doctorable by exactly the adversary CLI-witnessing exists for).
|
|
37
|
+
function resolveDesignCaptures(stateRoot, wt, planId) {
|
|
38
|
+
const green = readStream(stateRoot, planId)
|
|
39
|
+
.filter((e) => e.t === 'test-evidence' && e.phase === 'green');
|
|
40
|
+
const journaled = (green.at(-1)?.captures) ?? [];
|
|
41
|
+
const dir = screensDir(wt, planId);
|
|
42
|
+
const out = [];
|
|
43
|
+
for (const c of journaled) {
|
|
44
|
+
const path = join(dir, c.name);
|
|
45
|
+
if (!existsSync(path) || !statSync(path).isFile())
|
|
46
|
+
continue; // regenerable — a missing file is just "no capture"
|
|
47
|
+
const sha = createHash('sha256').update(readFileSync(path)).digest('hex');
|
|
48
|
+
if (sha !== c.sha) {
|
|
49
|
+
return refuse([v('screens', 'capture-sha-mismatch', c.name, 'a capture whose bytes match the sha test-evidence witnessed — re-run the browser suite to regenerate; never hand-edit a screenshot')]);
|
|
50
|
+
}
|
|
51
|
+
out.push({ name: c.name, path });
|
|
52
|
+
}
|
|
53
|
+
if (out.length === 0) {
|
|
54
|
+
return refuse([v('screens', 'screens-matched-nothing', `0 verified captures for the latest green cycle of ${planId}`, 'a UI plan (design-from pinned) must witness ≥1 screenshot — name captures in the browser test, then re-run: witness test-evidence <plan> --phase green')]);
|
|
55
|
+
}
|
|
56
|
+
return ok(out);
|
|
57
|
+
}
|
|
58
|
+
function screensPromptBody(caps) {
|
|
59
|
+
return [
|
|
60
|
+
'The reviewed content is the screenshots below. **Read each PNG at its absolute path with the Read tool before judging** — the images are the artifact, not this text.',
|
|
61
|
+
'',
|
|
62
|
+
...caps.map((c) => `- ${c.name} — ${c.path}`),
|
|
63
|
+
].join('\n');
|
|
64
|
+
}
|
|
65
|
+
registerGate({
|
|
66
|
+
gate: 'implement',
|
|
67
|
+
targetKind: 'plan',
|
|
68
|
+
async resolve(root, ctx, canon, cfg, planId) {
|
|
69
|
+
const plan = findById(canon, planId);
|
|
70
|
+
if (!plan || plan.meta.type !== 'plan') {
|
|
71
|
+
return refuse([v('plan', 'unknown-plan', planId, 'a plans/ doc id')]);
|
|
72
|
+
}
|
|
73
|
+
const wt = worktreePath(root, planId);
|
|
74
|
+
if (String(plan.meta.status) !== 'in-progress' || !existsSync(wt)) {
|
|
75
|
+
return refuse([v('plan', 'not-started', String(plan.meta.status), `an in-progress plan with a worktree — run: witness start ${planId}`)]);
|
|
76
|
+
}
|
|
77
|
+
const parent = findById(canon, String(plan.meta.parent));
|
|
78
|
+
if (!parent) {
|
|
79
|
+
return refuse([v('parent', 'unknown-parent', String(plan.meta.parent), 'an existing canon doc')]);
|
|
80
|
+
}
|
|
81
|
+
const baseR = diffBase(wt, cfg);
|
|
82
|
+
if (!baseR.ok)
|
|
83
|
+
return baseR;
|
|
84
|
+
const base = baseR.value;
|
|
85
|
+
const files = changedFiles(wt, base);
|
|
86
|
+
const pin = plan.meta['design-from'];
|
|
87
|
+
const lensOverrides = {};
|
|
88
|
+
const skipLenses = [];
|
|
89
|
+
if (typeof pin === 'string' && pin !== '') {
|
|
90
|
+
const capR = resolveDesignCaptures(root, wt, planId);
|
|
91
|
+
if (!capR.ok)
|
|
92
|
+
return capR;
|
|
93
|
+
const artRel = designRel(cfg.paths, String(plan.meta.parent));
|
|
94
|
+
const artAbs = join(root, artRel);
|
|
95
|
+
if (!existsSync(artAbs) || !statSync(artAbs).isFile()) {
|
|
96
|
+
return refuse([v('design', 'design-artifact-missing', artRel, 'the approved living design the plan pins — restore it or re-run the design stage')]);
|
|
97
|
+
}
|
|
98
|
+
const living = { path: artRel, contents: readFileSync(artAbs, 'utf8') };
|
|
99
|
+
lensOverrides['design-reviewer'] = {
|
|
100
|
+
reviewed: { kind: 'screens', captures: capR.value },
|
|
101
|
+
promptBody: screensPromptBody(capR.value),
|
|
102
|
+
docs: [living], // joins prompts_sha → an amended design re-rolls on unchanged code
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
// not UI-work: drop the lens if a battery names it (stages shrink, never skip)
|
|
107
|
+
skipLenses.push('design-reviewer');
|
|
108
|
+
}
|
|
109
|
+
const checks = [];
|
|
110
|
+
checks.push({ name: 'diff', ok: files.length > 0,
|
|
111
|
+
detail: files.length ? `${files.length} files changed vs ${base.slice(0, 7)}` : 'nothing changed vs base' });
|
|
112
|
+
const report = evidenceForDiff(wt, root, plan, base);
|
|
113
|
+
const missing = report.required.filter((r) => !(r.red && r.green && !r.vacuous));
|
|
114
|
+
checks.push({ name: 'evidence', ok: report.satisfied,
|
|
115
|
+
detail: report.satisfied
|
|
116
|
+
? `${report.required.length} tags with red→green pairs`
|
|
117
|
+
: missing.map((m) => `${m.tag}: red=${m.red} green=${m.green}${m.vacuous ? ' vacuous' : ''}`).join(' · ') });
|
|
118
|
+
const lane = await runCriteria(wt, ctx, parent, { trustRoot: root });
|
|
119
|
+
checks.push({
|
|
120
|
+
name: 'drift-lane',
|
|
121
|
+
ok: lane.ok && lane.value.ok,
|
|
122
|
+
detail: lane.ok
|
|
123
|
+
? lane.value.criteria.map((c) => `${c.id}:${c.ok ? 'ok' : 'fail'}`).join(' · ')
|
|
124
|
+
: lane.violations.map((x) => `${x.field}:${x.rule}`).join(' · '),
|
|
125
|
+
});
|
|
126
|
+
const effort = effortOf(root, planId);
|
|
127
|
+
const recap = effort ? latestRecap(root, effort) : undefined;
|
|
128
|
+
return ok({
|
|
129
|
+
class: (recap?.class ?? 'feature'),
|
|
130
|
+
reviewedSha: worktreeTreeSha(wt),
|
|
131
|
+
artifactSha: canonicalSha(plan.meta, plan.body),
|
|
132
|
+
reviewed: { kind: 'tree', root: wt, files },
|
|
133
|
+
promptBody: codePromptBody(wt, base, files, `### Plan under implementation: ${planId}\n${serializeDoc({ meta: plan.meta, body: plan.body })}`),
|
|
134
|
+
checks,
|
|
135
|
+
stamps: [],
|
|
136
|
+
lensOverrides,
|
|
137
|
+
skipLenses,
|
|
138
|
+
});
|
|
139
|
+
},
|
|
140
|
+
currentSha(root, _canon, _cfg, planId) {
|
|
141
|
+
const wt = worktreePath(root, planId);
|
|
142
|
+
return existsSync(wt) ? worktreeTreeSha(wt) : undefined;
|
|
143
|
+
},
|
|
144
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { ok, refuse, v } from '../refusal.js';
|
|
2
|
+
import { serializeDoc } from '../fm.js';
|
|
3
|
+
import { canonicalSha, short } from '../sha.js';
|
|
4
|
+
import { latestRecap } from '../journal.js';
|
|
5
|
+
import { baseForSpec } from '../history.js';
|
|
6
|
+
import { findById } from '../scan.js';
|
|
7
|
+
import { effortOf, planPairSha } from '../reviewed.js';
|
|
8
|
+
import { validateDoc } from '../schemas.js';
|
|
9
|
+
import { registerGate } from '../gate.js';
|
|
10
|
+
import { designPending, designStamp } from '../design.js';
|
|
11
|
+
registerGate({
|
|
12
|
+
gate: 'plan',
|
|
13
|
+
targetKind: 'plan',
|
|
14
|
+
async resolve(root, _ctx, canon, _cfg, planId) {
|
|
15
|
+
const plan = findById(canon, planId);
|
|
16
|
+
if (!plan || plan.meta.type !== 'plan') {
|
|
17
|
+
return refuse([v('plan', 'unknown-plan', planId, 'a plans/ doc id')]);
|
|
18
|
+
}
|
|
19
|
+
const effort = effortOf(root, planId);
|
|
20
|
+
if (!effort) {
|
|
21
|
+
return refuse([v('plan', 'no-effort', planId, 'a plan written via witness write --effort <slug> (class is read from the effort stream)')]);
|
|
22
|
+
}
|
|
23
|
+
const recap = latestRecap(root, effort);
|
|
24
|
+
const parent = findById(canon, String(plan.meta.parent));
|
|
25
|
+
if (!parent) {
|
|
26
|
+
return refuse([v('parent', 'unknown-parent', String(plan.meta.parent), 'an existing canon doc')]);
|
|
27
|
+
}
|
|
28
|
+
const checks = [];
|
|
29
|
+
const schemaProblems = validateDoc(plan.meta, plan.body).map((x) => `${x.field}:${x.rule}`);
|
|
30
|
+
checks.push({ name: 'schema', ok: schemaProblems.length === 0, detail: schemaProblems.slice(0, 5).join(' · ') });
|
|
31
|
+
const parentStatus = String(parent.meta.status);
|
|
32
|
+
const parentOk = parent.meta.type === 'principles'
|
|
33
|
+
? parentStatus === 'approved'
|
|
34
|
+
: parentStatus === 'approved' || parentStatus === 'live';
|
|
35
|
+
checks.push({ name: 'parent-approved', ok: parentOk,
|
|
36
|
+
detail: `${String(parent.meta.id)} is ${parentStatus}` });
|
|
37
|
+
const parentSha = canonicalSha(parent.meta, parent.body);
|
|
38
|
+
const pin = String(plan.meta['derives-from']);
|
|
39
|
+
checks.push({ name: 'pin-fresh', ok: pin === parentSha,
|
|
40
|
+
detail: pin === parentSha
|
|
41
|
+
? `pin ${short(parentSha)}`
|
|
42
|
+
: `derives-from ${short(pin)} but parent is at ${short(parentSha)} — rewrite the plan (witness write re-stamps the pin)` });
|
|
43
|
+
if (parent.meta.ui === true) {
|
|
44
|
+
const stamp = designStamp(parent);
|
|
45
|
+
const planPin = plan.meta['design-from'];
|
|
46
|
+
const okPin = stamp !== undefined && !designPending(root, parent) && planPin === stamp.sha;
|
|
47
|
+
checks.push({ name: 'design-pin', ok: okPin,
|
|
48
|
+
detail: okPin ? `design ${short(stamp.sha)}`
|
|
49
|
+
: !stamp ? `${String(parent.meta.id)} has no approved design — gate design first`
|
|
50
|
+
: designPending(root, parent) ? `${String(parent.meta.id)} design is stale — re-approve or reconfirm`
|
|
51
|
+
: `design-from ${short(String(planPin ?? 'absent'))} ≠ current design ${short(stamp.sha)}` });
|
|
52
|
+
}
|
|
53
|
+
const base = baseForSpec(root, canon, String(parent.meta.id), planId);
|
|
54
|
+
const promptBody = [
|
|
55
|
+
`### Parent spec: ${String(parent.meta.id)}`, serializeDoc({ meta: parent.meta, body: parent.body }),
|
|
56
|
+
`### Delta base`, base.kind === 'empty'
|
|
57
|
+
? 'new spec — the delta is the whole spec'
|
|
58
|
+
: `previous plan ${base.planId} pinned ${short(base.sha ?? '')} — the plan must realize what changed since`,
|
|
59
|
+
`### Plan: ${planId}`, serializeDoc({ meta: plan.meta, body: plan.body }),
|
|
60
|
+
].join('\n\n');
|
|
61
|
+
return ok({
|
|
62
|
+
class: (recap?.class ?? 'feature'),
|
|
63
|
+
reviewedSha: planPairSha(plan, parent),
|
|
64
|
+
artifactSha: canonicalSha(plan.meta, plan.body),
|
|
65
|
+
reviewed: {
|
|
66
|
+
kind: 'docs',
|
|
67
|
+
docs: [
|
|
68
|
+
{ id: planId, body: plan.body },
|
|
69
|
+
{ id: String(parent.meta.id), body: parent.body },
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
promptBody,
|
|
73
|
+
checks,
|
|
74
|
+
stamps: [{ artifact: planId, to: 'approved' }],
|
|
75
|
+
});
|
|
76
|
+
},
|
|
77
|
+
currentSha(_root, canon, _cfg, planId) {
|
|
78
|
+
const plan = findById(canon, planId);
|
|
79
|
+
const parent = plan ? findById(canon, String(plan.meta.parent)) : undefined;
|
|
80
|
+
return plan && parent ? planPairSha(plan, parent) : undefined;
|
|
81
|
+
},
|
|
82
|
+
approveStamps(_root, canon, planId) {
|
|
83
|
+
const doc = findById(canon, planId);
|
|
84
|
+
return doc && String(doc.meta.status) === 'draft' ? [{ artifact: planId, to: 'approved' }] : [];
|
|
85
|
+
},
|
|
86
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { ok, refuse, v } from '../refusal.js';
|
|
3
|
+
import { canonicalSha } from '../sha.js';
|
|
4
|
+
import { latestRecap, readStream } from '../journal.js';
|
|
5
|
+
import { findById } from '../scan.js';
|
|
6
|
+
import { changedFiles, diffBase } from '../evidence.js';
|
|
7
|
+
import { runCriteria } from '../criteria.js';
|
|
8
|
+
import { execCommand } from '../runner.js';
|
|
9
|
+
import { ensureTrusted } from '../allowlist.js';
|
|
10
|
+
import { effortOf, worktreeTreeSha } from '../reviewed.js';
|
|
11
|
+
import { worktreePath } from '../worktree.js';
|
|
12
|
+
import { codePromptBody } from './implement.js';
|
|
13
|
+
import { registerGate } from '../gate.js';
|
|
14
|
+
import { lastGateRun } from '../rounds.js';
|
|
15
|
+
import { serializeDoc } from '../fm.js';
|
|
16
|
+
async function commandLane(wt, root, ctx, name, cmd) {
|
|
17
|
+
if (!cmd)
|
|
18
|
+
return { name, ok: false, detail: `ship.${name === 'tests' ? 'test' : 'lint'} not configured` };
|
|
19
|
+
const trust = await ensureTrusted(root, ctx, cmd);
|
|
20
|
+
if (trust !== 'trusted')
|
|
21
|
+
return { name, ok: false, detail: `${cmd}: ${trust}` };
|
|
22
|
+
const run = execCommand(wt, ctx, cmd);
|
|
23
|
+
return { name, ok: run.exitZero, detail: run.exitZero ? cmd : run.output.slice(-300) };
|
|
24
|
+
}
|
|
25
|
+
registerGate({
|
|
26
|
+
gate: 'ship',
|
|
27
|
+
targetKind: 'plan',
|
|
28
|
+
async resolve(root, ctx, canon, cfg, planId) {
|
|
29
|
+
const plan = findById(canon, planId);
|
|
30
|
+
if (!plan || plan.meta.type !== 'plan') {
|
|
31
|
+
return refuse([v('plan', 'unknown-plan', planId, 'a plans/ doc id')]);
|
|
32
|
+
}
|
|
33
|
+
const wt = worktreePath(root, planId);
|
|
34
|
+
if (String(plan.meta.status) !== 'in-progress' || !existsSync(wt)) {
|
|
35
|
+
return refuse([v('plan', 'not-started', String(plan.meta.status), `an in-progress plan with a worktree — run: witness start ${planId}`)]);
|
|
36
|
+
}
|
|
37
|
+
const parent = findById(canon, String(plan.meta.parent));
|
|
38
|
+
if (!parent) {
|
|
39
|
+
return refuse([v('parent', 'unknown-parent', String(plan.meta.parent), 'an existing canon doc')]);
|
|
40
|
+
}
|
|
41
|
+
const baseR = diffBase(wt, cfg);
|
|
42
|
+
if (!baseR.ok)
|
|
43
|
+
return baseR;
|
|
44
|
+
const base = baseR.value;
|
|
45
|
+
const files = changedFiles(wt, base);
|
|
46
|
+
const checks = [];
|
|
47
|
+
const entries = readStream(root, planId);
|
|
48
|
+
const lastImplement = lastGateRun(entries, 'implement');
|
|
49
|
+
const implementSettled = (() => {
|
|
50
|
+
if (!lastImplement)
|
|
51
|
+
return false;
|
|
52
|
+
if (lastImplement.outcome === 'passed')
|
|
53
|
+
return true;
|
|
54
|
+
const after = entries.slice(entries.lastIndexOf(lastImplement) + 1);
|
|
55
|
+
return after.some((e) => e.t === 'human-decision' &&
|
|
56
|
+
e.gate === 'implement' &&
|
|
57
|
+
e.decision === 'approve');
|
|
58
|
+
})();
|
|
59
|
+
checks.push({ name: 'implement-gate', ok: implementSettled,
|
|
60
|
+
detail: lastImplement
|
|
61
|
+
? `last implement round ${lastImplement.round}: ${lastImplement.outcome}`
|
|
62
|
+
: 'implement gate never ran — witness gate implement ' + planId });
|
|
63
|
+
const ship = (cfg.raw.ship ?? {});
|
|
64
|
+
checks.push(await commandLane(wt, root, ctx, 'tests', ship.test));
|
|
65
|
+
checks.push(await commandLane(wt, root, ctx, 'lint', ship.lint));
|
|
66
|
+
// deterministic drift lane: spec content from MAIN, execution in the worktree
|
|
67
|
+
const lane = await runCriteria(wt, ctx, parent, { trustRoot: root });
|
|
68
|
+
const laneOk = lane.ok && lane.value.ok;
|
|
69
|
+
checks.push({
|
|
70
|
+
name: 'drift-lane',
|
|
71
|
+
ok: laneOk,
|
|
72
|
+
detail: lane.ok
|
|
73
|
+
? lane.value.criteria.map((c) => `${c.id}:${c.ok ? 'ok' : 'fail'}`).join(' · ')
|
|
74
|
+
: lane.violations.map((x) => `${x.field}:${x.rule}`).join(' · '),
|
|
75
|
+
});
|
|
76
|
+
const pin = String(plan.meta['derives-from']);
|
|
77
|
+
const repin = lane.ok && lane.value.ok && pin !== lane.value.sha
|
|
78
|
+
? { rel: plan.rel, meta: plan.meta, body: plan.body, sha: lane.value.sha }
|
|
79
|
+
: undefined;
|
|
80
|
+
const effort = effortOf(root, planId);
|
|
81
|
+
const recap = effort ? latestRecap(root, effort) : undefined;
|
|
82
|
+
return ok({
|
|
83
|
+
class: (recap?.class ?? 'feature'),
|
|
84
|
+
reviewedSha: worktreeTreeSha(wt),
|
|
85
|
+
artifactSha: canonicalSha(plan.meta, plan.body),
|
|
86
|
+
reviewed: { kind: 'tree', root: wt, files },
|
|
87
|
+
promptBody: codePromptBody(wt, base, files, `### Parent spec (from main): ${String(parent.meta.id)}\n${serializeDoc({ meta: parent.meta, body: parent.body })}\n\n### Plan: ${planId}\n${serializeDoc({ meta: plan.meta, body: plan.body })}`),
|
|
88
|
+
checks,
|
|
89
|
+
standingStop: 'ship always stops — a human sends every PR',
|
|
90
|
+
stamps: [],
|
|
91
|
+
repin,
|
|
92
|
+
});
|
|
93
|
+
},
|
|
94
|
+
currentSha(root, _canon, _cfg, planId) {
|
|
95
|
+
const wt = worktreePath(root, planId);
|
|
96
|
+
return existsSync(wt) ? worktreeTreeSha(wt) : undefined;
|
|
97
|
+
},
|
|
98
|
+
});
|
package/dist/gitio.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { canonPaths } from './config.js';
|
|
4
|
+
import { ok, refuse, v } from './refusal.js';
|
|
5
|
+
export function git(root, ...args) {
|
|
6
|
+
return execFileSync('git', args, {
|
|
7
|
+
cwd: root,
|
|
8
|
+
encoding: 'utf8',
|
|
9
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
10
|
+
}).replace(/\n$/, '');
|
|
11
|
+
}
|
|
12
|
+
export function tryGit(root, ...args) {
|
|
13
|
+
try {
|
|
14
|
+
return { ok: true, out: git(root, ...args) };
|
|
15
|
+
}
|
|
16
|
+
catch (e) {
|
|
17
|
+
const err = e;
|
|
18
|
+
// most git failures (like `pull --rebase` with no upstream) write to stderr with
|
|
19
|
+
// an empty stdout — `??` only falls through on nullish, so it must prefer the
|
|
20
|
+
// first NON-EMPTY stream, not just the first non-nullish one
|
|
21
|
+
return { ok: false, out: String(err.stdout || err.stderr || e).trim() };
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function primaryRoot(cwd) {
|
|
25
|
+
const top = tryGit(cwd, 'rev-parse', '--show-toplevel');
|
|
26
|
+
if (!top.ok) {
|
|
27
|
+
return refuse([v('repo', 'not-a-git-repo', cwd, 'run inside a git repository (git init first)')]);
|
|
28
|
+
}
|
|
29
|
+
const common = git(cwd, 'rev-parse', '--path-format=absolute', '--git-common-dir');
|
|
30
|
+
const gitDir = git(cwd, 'rev-parse', '--path-format=absolute', '--git-dir');
|
|
31
|
+
if (common === gitDir)
|
|
32
|
+
return ok(top.out);
|
|
33
|
+
return ok(dirname(common));
|
|
34
|
+
}
|
|
35
|
+
export function stateDirs(root) {
|
|
36
|
+
const p = canonPaths(root);
|
|
37
|
+
return [p.specs, p.plans, p.designs, '.witness'];
|
|
38
|
+
}
|
|
39
|
+
export function isStatePath(root, rel) {
|
|
40
|
+
return stateDirs(root).some((d) => rel === d || rel.startsWith(d + '/'));
|
|
41
|
+
}
|
|
42
|
+
const LOCAL_STATE_FILES = new Set([
|
|
43
|
+
'.witness/lock',
|
|
44
|
+
'.witness/txn.json',
|
|
45
|
+
'.witness/allow.json',
|
|
46
|
+
'.witness/calibration.local.yaml',
|
|
47
|
+
]);
|
|
48
|
+
export function dirtyStatePaths(root) {
|
|
49
|
+
const res = tryGit(root, 'status', '--porcelain', '--untracked-files=all', '--', ...stateDirs(root));
|
|
50
|
+
if (!res.ok || res.out === '')
|
|
51
|
+
return [];
|
|
52
|
+
return res.out
|
|
53
|
+
.split('\n')
|
|
54
|
+
// a rename (e.g. from `git mv`) porcelain-formats as "old -> new" on one line —
|
|
55
|
+
// split it into its two paths so each is checked against the caller's planned set
|
|
56
|
+
.flatMap((l) => l.slice(3).replace(/^"|"$/g, '').split(' -> '))
|
|
57
|
+
.filter((p) => !LOCAL_STATE_FILES.has(p));
|
|
58
|
+
}
|
|
59
|
+
export const TRAILER = 'Witness-State: 1';
|
|
60
|
+
export function commitWithTrailer(root, files, subject) {
|
|
61
|
+
// tryGit, not git: a path already fully processed by a prior `git mv` (both sides
|
|
62
|
+
// of a rename staged atomically) is absent from working tree AND index, so `add`
|
|
63
|
+
// reports it unmatched — even though the rename itself is already staged. `commit
|
|
64
|
+
// --only` resolves its pathspecs against the index-vs-HEAD diff, where the rename
|
|
65
|
+
// is visible, so it still succeeds and is the real validation here.
|
|
66
|
+
tryGit(root, 'add', '--', ...files);
|
|
67
|
+
// --no-verify: state commits are machine-authored and scope-restricted to non-source
|
|
68
|
+
// paths (isStatePath), so a host lint/test hook has nothing to validate here — while
|
|
69
|
+
// its stash/restore step can destroy the human's unrelated dirty work, and a
|
|
70
|
+
// tree-mutating hook (formatters) can desync the index from the txn marker.
|
|
71
|
+
// stateCommit's own `unrelated-dirty` refusal is the real gate on these commits.
|
|
72
|
+
git(root, 'commit', '--no-verify', '--only', '-m', subject, '-m', TRAILER, '--', ...files);
|
|
73
|
+
return ok({ sha: git(root, 'rev-parse', 'HEAD') });
|
|
74
|
+
}
|
|
75
|
+
export function stateCommit(root, files, subject) {
|
|
76
|
+
const dirs = stateDirs(root);
|
|
77
|
+
const bad = files.filter((f) => !isStatePath(root, f));
|
|
78
|
+
if (bad.length) {
|
|
79
|
+
return refuse(bad.map((f) => v(f, 'out-of-scope', f, `state commits touch only ${dirs.map((d) => `${d}/`).join(', ')}`)));
|
|
80
|
+
}
|
|
81
|
+
const planned = new Set(files);
|
|
82
|
+
const unrelated = dirtyStatePaths(root).filter((p) => !planned.has(p));
|
|
83
|
+
if (unrelated.length) {
|
|
84
|
+
return refuse(unrelated.map((p) => v(p, 'unrelated-dirty', 'uncommitted change on a state path', 'revert it or re-apply via witness write, then re-run')));
|
|
85
|
+
}
|
|
86
|
+
return commitWithTrailer(root, files, subject);
|
|
87
|
+
}
|
|
88
|
+
export function auditStateCommits(root) {
|
|
89
|
+
const p = canonPaths(root);
|
|
90
|
+
const res = tryGit(root, 'log', '--format=%H%x1f%s%x1f%(trailers:key=Witness-State,valueonly=true)', '--', p.specs, p.plans, p.designs, '.witness/journal');
|
|
91
|
+
if (!res.ok || res.out === '')
|
|
92
|
+
return [];
|
|
93
|
+
return res.out.split('\n').filter(Boolean).map((line) => {
|
|
94
|
+
const [sha, subject, trailer] = line.split('\x1f');
|
|
95
|
+
return { sha: sha ?? '', subject: subject ?? '', trailered: (trailer ?? '').trim() === '1' };
|
|
96
|
+
});
|
|
97
|
+
}
|
package/dist/glob.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
export function globToRegExp(pattern) {
|
|
4
|
+
const segs = pattern.split('/');
|
|
5
|
+
const parts = segs.map((seg, i) => {
|
|
6
|
+
const last = i === segs.length - 1;
|
|
7
|
+
if (seg === '**')
|
|
8
|
+
return last ? '.*' : '(?:[^/]+/)*';
|
|
9
|
+
const body = seg
|
|
10
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
11
|
+
.replace(/\*/g, '[^/]*');
|
|
12
|
+
return last ? body : `${body}/`;
|
|
13
|
+
});
|
|
14
|
+
return new RegExp(`^${parts.join('')}$`);
|
|
15
|
+
}
|
|
16
|
+
export function walkFiles(root) {
|
|
17
|
+
const skip = new Set(['.git', 'node_modules']);
|
|
18
|
+
const out = [];
|
|
19
|
+
const rec = (rel) => {
|
|
20
|
+
for (const name of readdirSync(join(root, rel || '.'))) {
|
|
21
|
+
if (skip.has(name))
|
|
22
|
+
continue;
|
|
23
|
+
const r = rel ? `${rel}/${name}` : name;
|
|
24
|
+
if (statSync(join(root, r)).isDirectory())
|
|
25
|
+
rec(r);
|
|
26
|
+
else
|
|
27
|
+
out.push(r);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
rec('');
|
|
31
|
+
return out.sort();
|
|
32
|
+
}
|