@awak-app/simy-cli 0.5.8 → 0.5.11

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.
Files changed (32) hide show
  1. package/README.md +64 -0
  2. package/package.json +7 -3
  3. package/src/agent.js +70 -9
  4. package/src/auto-update.js +312 -35
  5. package/src/browser.js +24 -7
  6. package/src/cli-contract.js +1 -1
  7. package/src/codex-plugins/simy/skills/agentic-delivery-loop/SKILL.md +227 -18
  8. package/src/codex-plugins/simy/skills/agentic-delivery-loop/references/acceptance-evidence.md +131 -0
  9. package/src/codex-plugins/simy/skills/agentic-delivery-loop/references/change-boundary-matrix.md +20 -1
  10. package/src/codex-plugins/simy/skills/agentic-delivery-loop/references/cross-repository-control.md +76 -0
  11. package/src/codex-plugins/simy/skills/agentic-delivery-loop/references/delivery-levels.md +80 -0
  12. package/src/codex-plugins/simy/skills/agentic-delivery-loop/references/minimum-completion.md +137 -0
  13. package/src/codex-plugins/simy/skills/agentic-delivery-loop/references/shared-system-specifications.md +138 -0
  14. package/src/codex-plugins/simy/skills/agentic-delivery-loop/references/state-and-receipts.md +18 -0
  15. package/src/codex-plugins/simy/skills/agentic-delivery-loop/scripts/check-acceptance.mjs +193 -0
  16. package/src/codex-plugins/simy/skills/agentic-delivery-loop/scripts/check-minimum-completion.mjs +221 -0
  17. package/src/console/app.js +21 -2
  18. package/src/console/commands.js +3 -0
  19. package/src/desktop-host.js +220 -0
  20. package/src/desktop-process-lock.js +231 -0
  21. package/src/desktop-status.js +263 -0
  22. package/src/direct-executor.js +6 -0
  23. package/src/index.js +37 -11
  24. package/src/install-update.js +494 -0
  25. package/src/orchestrator/audit.js +17 -2
  26. package/src/orchestrator/completion-contract.js +1 -1
  27. package/src/orchestrator/evidence.js +9 -1
  28. package/src/orchestrator/independent-audit.js +13 -1
  29. package/src/orchestrator/instruction.js +13 -1
  30. package/src/orchestrator/loop.js +34 -4
  31. package/src/orchestrator/minimum-completion.js +108 -0
  32. package/src/orchestrator/result.js +7 -0
@@ -0,0 +1,137 @@
1
+ # Minimum functional completion
2
+
3
+ Every level must prove that the requested work exists, actual operations reach
4
+ the intended result, and necessary persistence/integration works. Speed changes
5
+ adjacent verification breadth, never this floor. The CLI requires this matrix for
6
+ all coding-loop attempts, including legacy v2 resumes/rechecks. No opt-out field
7
+ in an executor result or assurance-level selection disables the gate.
8
+
9
+ ## Collect real observations
10
+
11
+ Before implementation, reconcile the original request, acceptance criteria and
12
+ planned operations. Preserve every requested outcome; a missing requirement must
13
+ be added through the controller's charter process, not omitted from the matrix.
14
+ The independent auditor compares original-request coverage and checks the oracle
15
+ against raw evidence. Machine mapping checks cannot discover an omitted idea in
16
+ natural language or authenticate what an evidence collector observed.
17
+
18
+ For each AC-N record implementation_refs (repository-relative file paths), then:
19
+
20
+ - user/operation: operate the actual UI or CLI/consumer. Expected is true; actual
21
+ is whether the intended operation really executed, not just whether a click
22
+ command succeeded. Include screenshots/traces where relevant.
23
+ - user/result: observe the requested visible outcome, including reload and session
24
+ return for a saving feature. Compare expected and actual values.
25
+ - system/postcondition: independently inspect the final state; for persistence,
26
+ include the saved values and ownership, not a toast or HTTP status.
27
+ - system/integration: inspect the necessary combined path through APIs, storage,
28
+ workers or consumers. Job acceptance is not job completion. Use the same input
29
+ and execution context across layers; cite trace/runtime identities in subject
30
+ and additional evidence.
31
+
32
+ For document-only work these mean opening the actual consumer and checking content,
33
+ links and packaged bytes. No DB/API is required if none participates. Both generic
34
+ layers remain required; there is no post-failure waiver. Tests, permissions or
35
+ runtime unavailable means pending/blocked. Never fabricate expected/actual equality.
36
+
37
+ ## Executable contract
38
+
39
+ `check-minimum-completion.mjs` and `check-acceptance.mjs` ship together under the
40
+ installed skill's scripts directory. The CLI instruction supplies controller input
41
+ JSON; save it as charter.json for the standalone command. It contains id,
42
+ requirement, repository, acceptance_criteria, completion_contract (version 2), and
43
+ thread_state.original_request. For standalone use the controller freezes the same
44
+ fields independently of implementation. The matrix otherwise follows
45
+ [acceptance evidence](acceptance-evidence.md).
46
+
47
+ To derive the frozen specification/context, import `minimumCharter(charter)` and
48
+ `minimumContext(charter, fullHeadSha)` from the minimum script; import `sha256`
49
+ and `contextDigest` from check-acceptance.mjs. The charter digest is
50
+ `sha256(Buffer.from(JSON.stringify(minimumCharter(charter))))`.
51
+ The context digest is `contextDigest(minimumContext(charter, fullHeadSha))`.
52
+ The CLI derives both itself; it never takes expected digests from executor output.
53
+
54
+ The current CLI context is one local checkout, its full HEAD, controller charter
55
+ ID and request digest. A clean checkout is required and checked again after
56
+ collection. This is not automatic multi-repository deployment identity discovery.
57
+ For companion services, verify the combined path and record the pinned service
58
+ identities in the evidence; the independent reviewer must validate their current
59
+ identity. Do not claim that this single-checkout gate automatically re-reads a
60
+ companion service's Git HEAD or production environment.
61
+
62
+ Every matrix row adds `implementation_refs: ["src/example.js"]`. Each required
63
+ user/system cell's **first** evidence file is JSON with this shape (user example):
64
+
65
+ ```json
66
+ {
67
+ "criterion_id": "AC-1",
68
+ "context_sha256": "<current context digest>",
69
+ "checks": {
70
+ "operation": {"subject": "Save clicked and request observed for fixture run", "expected": true, "actual": true},
71
+ "result": {"subject": "Note after reload and session return", "expected": "requested text", "actual": "requested text"}
72
+ }
73
+ }
74
+ ```
75
+
76
+ The system file uses `postcondition` and `integration` instead. Each has `subject`,
77
+ non-null `expected` and actual observed `actual`. Values may be objects/arrays to
78
+ compare saved values and ownership together. The checker compares values exactly;
79
+ missing fields or mismatches fail regardless of a cell's passed flag. Additional
80
+ cell evidence may contain screenshots or logs; all artifact hashes are validated.
81
+ Keep raw evidence local and use synthetic data, never credentials/personal data.
82
+
83
+ Store matrix and evidence under ui_evidence_root, or under repository `.artifacts`
84
+ when no root was specified. All artifact paths are relative to the matrix directory.
85
+ Return `delivery_evidence_path` in SIMY_RESULT_JSON, absolute under that root or
86
+ relative to it. Source mappings must resolve to files inside the current repository.
87
+ Evidence must be bounded regular files; path escapes/symlinks are rejected.
88
+
89
+ Standalone invocation, with controller-owned trusted digests:
90
+
91
+ ```text
92
+ node <installed-skill>/scripts/check-minimum-completion.mjs charter.json matrix.json context.json <trusted-charter-sha256> <trusted-context-sha256>
93
+ ```
94
+
95
+ Exit 0 is evidence_complete; 1 is evidence_incomplete; 2 means invalid/unavailable
96
+ inputs. JSON includes a readable checklist and floor failures. Check the top-level
97
+ status, not only individual base-matrix rows. The standalone checker validates
98
+ mapping structure; only the CLI additionally resolves mappings in its checkout.
99
+ Neither file hashes nor a passed report authenticate the collector or prove an
100
+ oracle matches the original requirement. Independent coverage/oracle review is
101
+ required by the CLI and must inspect the raw evidence. The standalone command
102
+ cannot stop Codex/Claude's final chat response and does not install global hooks.
103
+
104
+ ## State, review and recovery
105
+
106
+ The CLI's implementation gate includes the minimum report before independent
107
+ review. The review returns coverage_review with requirements_complete,
108
+ unmapped_requirements and a concrete observation. The controller binds that
109
+ review to the matrix digest, then recollects evidence. Changing the matrix after
110
+ review requires a new review; changing HEAD or dirtying the tree invalidates the
111
+ proof. A concurrent call using the same run ID is rejected, including cloned snapshots.
112
+ A repeated start rechecks evidence without executing the implementation again;
113
+ explicit resume remains the repair path. Cross-process
114
+ execution remains subject to existing runner/host ownership controls.
115
+
116
+ PR recheck clears prior completion flags and reruns the floor. Missing evidence
117
+ never promotes an old successful audit. The old history is retained; do not rewrite
118
+ it as verified under the new floor. Fix the named criterion, collect fresh evidence
119
+ and resume within the retry budget. Reconcile ambiguous external side effects
120
+ before retrying. The minimum pass is a prerequisite for existing CI/review/authority
121
+ gates, not merge, deployment or a complete production delivery.
122
+
123
+ Rollback: restoring the older CLI removes this additional enforcement. Retain the
124
+ versioned evidence/history and report that limitation; never relabel old evidence
125
+ as verified by this floor. Monitor minimum-completion findings and subsequent
126
+ reviewer-discovered gaps through existing local run records; no new telemetry or
127
+ external provider calls are introduced.
128
+
129
+ ## Shared specifications and independent review
130
+
131
+ The floor's AC-N rows trace back to stable requirement IDs in the team's shared
132
+ As-Is/To-Be specification. Follow [shared system specifications](shared-system-specifications.md)
133
+ for purpose, user experience, cross-view consistency, independent reviews and
134
+ pre/post-PR SQM. These are controller workflow obligations, not fields magically
135
+ validated by the minimum checker. The CLI gate verifies minimum structured evidence;
136
+ it does not launch every design review or SQM phase on its own. Standalone hosts
137
+ remain responsible for executing this skill and cannot claim an enforced host hook.
@@ -0,0 +1,138 @@
1
+ # Shared system specifications and delivery review
2
+
3
+ Read before planning a Delivery Loop change. The specification belongs to the
4
+ system and its team, not to a single run. Use existing team paths and conventions;
5
+ if none exist, start `docs/systems/<system-id>/` with an index and the applicable
6
+ views below. Repository paths here are a convention, not a demand to move existing
7
+ documents or to create a new service. Keep one canonical source for each concept.
8
+
9
+ ## Shared baseline and proposed change
10
+
11
+ The system index records its owner/maintainers, repository/document links, stable
12
+ system ID, As-Is source revision(s) and environment, verified date, and gaps.
13
+ Distinguish source-code behavior, locally observed behavior, and deployed behavior.
14
+ An old design or green PR is not proof of current production behavior.
15
+
16
+ At the start of each run:
17
+
18
+ 1. Read the existing index and current linked views. Reconcile relevant As-Is with
19
+ code and actual behavior; label unknowns, cite evidence, and correct documented
20
+ drift without claiming that the drift was part of the requested feature.
21
+ 2. State why the change matters: beneficiary, current difficulty, what they can
22
+ do after the change, observable success, and non-goals. Preserve the user's
23
+ original request as an independent acceptance source.
24
+ 3. Prepare a proposed To-Be on a branch/change proposal linked from the shared
25
+ system index. Record its baseline revision, affected stable IDs, before/after
26
+ behavior, dependencies and acceptance oracles. Reuse unchanged views by reference;
27
+ do not copy the whole specification into every run or generate empty diagrams.
28
+ 4. Link the requirement to workflow, screen/control, state/sequence, API/event,
29
+ data/relationship, permission and runtime boundaries as applicable. Map these
30
+ stable system IDs to the run's AC-N rows and implementation/evidence locations.
31
+ 5. Obtain independent design review before implementation. Supply the original
32
+ request, shared baseline, To-Be and linked unchanged views. Missing evidence or
33
+ contradictory views remains pending/failed, never silently accepted.
34
+
35
+ The twelve-view index must account for every category: present (with a canonical
36
+ link), unchanged (with a revision), not applicable (with a reason), or unknown
37
+ (with an owner/next action). Only create/update a diagram where it describes a
38
+ real system concept. For CLI/skill systems, chat/terminal interactions can be the
39
+ interface views; do not invent application screens, APIs, tables or infrastructure.
40
+
41
+ | View | Primary content | Consistency checks against other views |
42
+ | --- | --- | --- |
43
+ | art-01-screen-inventory-final | Screens or user interaction surfaces and stable IDs | Every referenced screen/control in workflows, transitions and wireframes exists |
44
+ | art-02-screen-transitions | Navigation, entry/exit, failure/recovery | Reachable inventory entries, correct permission guards and state conditions |
45
+ | art-03-wireframes | Controls, content, empty/loading/error/success states | Each action has a requirement, transition and downstream sequence; no dead control |
46
+ | art-04-functional-requirements | Purpose, beneficiary, outcomes, invariants, acceptance | Every request maps to implementation and evidence through the other affected views |
47
+ | art-05-state-machines | Legal states, guards, terminal outcomes, retry/cancel | Sequences/events trigger legal transitions; UI messages match actual final state |
48
+ | art-06-permissions | Actor/role, action, resource, authority boundary | UI guards, API checks, ownership and execution roles agree |
49
+ | art-07-user-workflows | User goal, steps, completion and recovery experience | Requirements covered end to end through screens, APIs, persistence and runtime |
50
+ | art-08-data-dictionary | Fields, meaning, type, requiredness, lifecycle and owner | API/event payloads, ER cardinality and state invariants use the same definitions |
51
+ | art-09-sequences | Calls/events through components, errors, asynchronous completion | API names/payloads, states, authorization and deployed component boundaries agree |
52
+ | art-10-api-design | Existing APIs, commands or file contracts, errors and compatibility | Sequence inputs/outputs, data meanings and permission checks agree |
53
+ | art-11-er-diagram | Entities, relationships, ownership and cardinality | Dictionary and API reads/writes agree; label logical artifact models as non-DB |
54
+ | art-12-infrastructure | Repositories, execution environments, services and trust boundaries | Every sequence participant has an actual host/owner; revision/rollout dependencies align |
55
+
56
+ ## Review evidence and neutral agents
57
+
58
+ All reviews use independent agents/sessions, including design, cross-view
59
+ consistency, implementation-versus-To-Be, verification changes and PR review.
60
+ An agent must not approve work it authored. A reviewer can re-review fixes to its
61
+ findings if it did not implement the fixes. Supply raw sources, not desired answers;
62
+ require inspected references, discrepancies, uncertainty and a reasoned verdict.
63
+ Use the repository-required model (gpt-5.5 / xhigh where mandated and available).
64
+ If independent review is unavailable, report that gate as blocked.
65
+
66
+ Persist a compact review receipt: reviewer/session identity, review kind, original
67
+ request and shared baseline/To-Be references, candidate revisions, inspected views
68
+ and evidence, findings, result, and re-review links. An independent reviewer checks:
69
+
70
+ - coverage of the original request, user benefit and acceptance oracles;
71
+ - consistency for every affected cross-view link, including unchanged neighbors;
72
+ - behavior after actual operation, reload/session return and final persistence or
73
+ integration outcome where applicable;
74
+ - implementation and tests against the proposed To-Be, not just against each other;
75
+ - missing/extra functionality, stale evidence and unresolved historical regressions.
76
+
77
+ A diagram syntax/link checker helps, but cannot replace semantic cross-view review.
78
+ Do not declare neutral review merely by renaming the implementer's role.
79
+
80
+ ## SQM before and after PR creation
81
+
82
+ Use the installed SQM loop skill. Respect explicit SQM opt-in, existing session and
83
+ service authority. Explain unavailable gates; do not install global hooks, weaken
84
+ rules, force an upload or switch organizations to obtain a pass.
85
+
86
+ For each phase use fresh, separate local paths outside the diff (for example an
87
+ ignored `.artifacts/delivery/<run>/sqm/pre-pr/` or `post-pr/` directory):
88
+
89
+ ```sh
90
+ simy sqm min-check --base <actual-pr-base> --output <phase>/min.json
91
+ simy sqm check --base <actual-pr-base> --min-result <phase>/min.json --proof <phase>/proof.json --signed-evidence <phase>/proof.json.signed.json
92
+ ```
93
+
94
+ Use supported no-upload options when required by the authorized sharing scope.
95
+ Read the proof and verify its repository, head, base, working-tree/diff identity,
96
+ knowledge bundle version/digest and `outcome.status == passed`; verify the bound
97
+ signature using SQM tooling. Exit zero alone is insufficient. Do not edit or commit
98
+ between min-check and check. Pin the commit before the pre-PR check; writing proof
99
+ into the committed diff would invalidate its identity.
100
+
101
+ Before the pre-PR phase, inventory relevant known incidents from the team's shared
102
+ incident records, SQM knowledge and repository regression history. Record incident
103
+ or source ID, failure boundary, applicable/unaffected/unknown classification,
104
+ regression command and result, SQM rule match/coverage, and residual gaps. Link this
105
+ ledger from the shared specification so the next task can reuse and improve it.
106
+ Separate confirmed incidents from synthetic risk scenarios; never invent history.
107
+ A zero-match/no-applicable-rules proof does not establish historical coverage.
108
+ Use concrete regressions for relevant uncovered incidents and have an independent
109
+ reviewer assess coverage; keep missing incident history or untested boundaries
110
+ visible. No finite test promises absence of all future bugs.
111
+
112
+ Pre-PR: implementation-to-To-Be review and relevant regressions pass, then run SQM;
113
+ repair findings within the bounded loop, re-review changed work, and rerun against
114
+ the final candidate before creation. On unavailable/failed proof, withhold creation
115
+ and report the exact blocker while continuing independent authorized preparation.
116
+
117
+ Post-PR: fetch the actual PR head/base and check out/verify that combination, run
118
+ both SQM phases again into new paths, inspect proof and current CI, and obtain
119
+ independent PR/evidence review. The pre-PR proof cannot stand in for this run.
120
+ If it fails, retain the PR as draft/not-ready with unmet gates recorded, repair and
121
+ rerun affected independent reviews and SQM. A changed head/base/spec or relevant
122
+ bundle invalidates affected approvals and proofs; preserve old receipts as history.
123
+ Promote Ready only with current required gates, including applicable combined-state
124
+ verification. Merge and deployment still require their own authority and evidence.
125
+
126
+ ## Team maintenance after verification
127
+
128
+ Merge reviewed specification changes with the corresponding implementation through
129
+ the normal team review process. Record proposed, implemented locally, merged and
130
+ verified-in-environment status separately. Advance As-Is only for the revision and
131
+ environment actually verified; a merged To-Be is not automatically production As-Is.
132
+ Preserve the prior baseline and decisions in version history. If another change
133
+ updates a shared view, reconcile its semantics and re-review affected links even
134
+ when Git reports no textual conflict. Do not overwrite concurrent edits.
135
+
136
+ Each run stores pointers, deltas and evidence in its receipt, not another competing
137
+ master specification. Record unresolved drift with an owner, source and next action.
138
+ Never lower the intended outcome merely to make an implementation match a diagram.
@@ -9,11 +9,18 @@ Persist one controller-owned snapshot with at least:
9
9
 
10
10
  - `run_id`, `state`, monotonic `revision`, and terminal disposition;
11
11
  - immutable explicit conditions and their stable digest;
12
+ - acceptance-source identities and digests, architecture boundaries, applicable
13
+ rule decisions, risk tier, requested/effective delivery levels, the safety
14
+ ceiling reason, and chat-confirmation receipt;
12
15
  - repository, base SHA, head SHA, companion SHAs, merge SHA, image digests, and
13
16
  deployed revision;
14
17
  - target environments, user journey, user/tenant fixture identities, and
15
18
  required artifacts;
16
19
  - applicable gates and their current status;
20
+ - frozen acceptance charter digest, current matrix/table paths and digests,
21
+ superseded matrix snapshots, and the acceptance validator result for each gate;
22
+ - verification-boundary changes, deferred-debt references, and applicable
23
+ outcome and guardrail measures;
17
24
  - attempt number, retry/time/cost budgets, and failure fingerprints;
18
25
  - append-only receipt IDs, evidence references, timestamps, and cleanup state.
19
26
 
@@ -29,6 +36,7 @@ Use the smallest applicable receipts rather than free-form completion claims:
29
36
  - `FailureReproductionReceipt`
30
37
  - `ImplementationReceipt`
31
38
  - `LocalVerificationReceipt`
39
+ - `AcceptanceEvidenceReceipt` (matrix completeness, not overall completion)
32
40
  - `IndependentReviewReceipt`
33
41
  - `PRCIReceipt`
34
42
  - `IntegrationVerificationReceipt`
@@ -91,6 +99,7 @@ Evidence is revision-bound:
91
99
  | Change | Invalidate |
92
100
  |---|---|
93
101
  | Charter or explicit success conditions | All affected downstream gates |
102
+ | Acceptance source, test oracle, assertion, or CI/security gate changes | The affected gate and all dependent downstream evidence |
94
103
  | Head SHA or generated source changes | Local, CI, integration, container, and live evidence |
95
104
  | Base SHA or companion revision changes | Integration and later evidence |
96
105
  | Container image digest changes | Container user journey and later runtime evidence |
@@ -125,6 +134,8 @@ For the full delivery mode:
125
134
  ```text
126
135
  done =
127
136
  reproduction_verified
137
+ && acceptance_evidence_complete
138
+ && minimum_functional_evidence_verified
128
139
  && locally_verified
129
140
  && pr_ci_green
130
141
  && integration_verified
@@ -140,3 +151,10 @@ done =
140
151
  Remove only genuinely non-applicable terms and record why. If merge or deploy
141
152
  is outside current authority, stop at the highest proven pre-authority state.
142
153
 
154
+ Acceptance evidence must cover every applicable criterion for the target gate,
155
+ with current context and the required user/system layers. After each evaluated
156
+ action, refresh the JSON matrix and its readable table; append a receipt for the
157
+ update. Keep the frozen charter distinct from mutable results. A charter patch
158
+ supersedes the old digest only after the controller verifies its authority.
159
+
160
+ The CLI records minimum evidence in observed_evidence.minimum_completion, independently of executor status. The independent audit is bound to its matrix digest. Legacy v2 histories remain readable, but missing minimum evidence cannot be reused as passing evidence on resume/recheck. Missing or unsupported charters require a refresh; they do not disable verification.
@@ -0,0 +1,193 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Offline record validation only. The controller owns acceptance and provenance.
4
+ import { createHash } from "node:crypto";
5
+ import { constants } from "node:fs";
6
+ import { lstat, open, readFile, realpath } from "node:fs/promises";
7
+ import path from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+
10
+ const layers = ["user", "system"];
11
+ const hashPattern = /^[a-f0-9]{64}$/;
12
+ const text = (value) => typeof value === "string" && value.trim().length > 0;
13
+ const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
14
+ export const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
15
+ export const contextDigest = (context) => sha256(JSON.stringify(canonical(context)));
16
+
17
+ function canonical(value) {
18
+ if (Array.isArray(value)) return value.map(canonical);
19
+ if (!object(value)) return value;
20
+ return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonical(value[key])]));
21
+ }
22
+
23
+ function requireValue(condition, message) {
24
+ if (!condition) throw new Error(message);
25
+ }
26
+
27
+ function validateCharter(charter) {
28
+ requireValue(object(charter) && charter.schema_version === 1 && text(charter.run_id), "Invalid charter header");
29
+ requireValue(Array.isArray(charter.criteria) && charter.criteria.length > 0, "Charter requires criteria");
30
+ const ids = new Set();
31
+ for (const criterion of charter.criteria) {
32
+ requireValue(object(criterion) && text(criterion.id) && !ids.has(criterion.id), "Duplicate or invalid criterion ID");
33
+ ids.add(criterion.id);
34
+ requireValue(text(criterion.outcome) && text(criterion.acceptance_source), "Criterion needs outcome and independent acceptance source");
35
+ requireValue(object(criterion.layers), "Criterion needs verification layers");
36
+ let required = 0;
37
+ for (const layer of layers) {
38
+ const spec = criterion.layers[layer];
39
+ requireValue(object(spec), `Missing ${layer} layer specification`);
40
+ if (spec.applicability === "required") {
41
+ requireValue(text(spec.method), "Required layer needs a verification method");
42
+ required += 1;
43
+ } else {
44
+ requireValue(spec.applicability === "not_applicable" && text(spec.reason) && text(spec.source), "Non-applicability needs a reason and source in the charter");
45
+ }
46
+ }
47
+ requireValue(required > 0, "A criterion cannot waive both verification layers");
48
+ }
49
+ }
50
+
51
+ function validateContext(context, runId) {
52
+ requireValue(object(context) && context.run_id === runId, "Context run ID mismatch");
53
+ for (const field of ["stage", "environment", "fixture_id"]) {
54
+ requireValue(text(context[field]), `Context needs ${field}`);
55
+ }
56
+ requireValue(hashPattern.test(context.input_sha256), "Context needs input SHA-256");
57
+ requireValue(object(context.source_revisions) && Object.keys(context.source_revisions).length > 0, "Context needs repository revisions");
58
+ for (const [repository, revision] of Object.entries(context.source_revisions)) {
59
+ requireValue(text(repository) && typeof revision === "string" && /^[a-f0-9]{40}$/.test(revision), "Invalid repository revision");
60
+ }
61
+ }
62
+
63
+ export async function readEvidenceArtifact(root, artifact) {
64
+ requireValue(object(artifact) && text(artifact.path) && hashPattern.test(artifact.sha256), "Invalid artifact reference");
65
+ // Explicit bundle-relative paths only; never fetch URLs or execute evidence.
66
+ const segments = artifact.path.split("/");
67
+ requireValue(!path.isAbsolute(artifact.path) && !artifact.path.includes("\\") &&
68
+ segments.every((part) => part && part !== "." && part !== ".." && !part.includes(":")), "Unsafe artifact path");
69
+ let target = root;
70
+ for (let index = 0; index < segments.length; index += 1) {
71
+ target = path.join(target, segments[index]);
72
+ const stat = await lstat(target);
73
+ requireValue(!stat.isSymbolicLink(), "Symbolic evidence paths are not allowed");
74
+ if (index < segments.length - 1) requireValue(stat.isDirectory(), "Invalid evidence directory");
75
+ }
76
+ const handle = await open(target, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
77
+ try {
78
+ const stat = await handle.stat();
79
+ requireValue(stat.isFile() && stat.size > 0 && stat.size <= 16 * 1024 * 1024, "Artifact must be a nonempty regular file of at most 16 MiB");
80
+ const bytes = await handle.readFile();
81
+ requireValue(sha256(bytes) === artifact.sha256, "Artifact digest mismatch");
82
+ return bytes;
83
+ } finally {
84
+ await handle.close();
85
+ }
86
+ }
87
+
88
+ const tableText = (value) => String(value ?? "").replaceAll("|", "\\|").replace(/[\r\n]+/g, " ");
89
+
90
+ export function renderChecklist(report) {
91
+ const header = [
92
+ "| Criterion / outcome | Layer / method | Status | Observation | Evidence | Unmet reason / next action |",
93
+ "| --- | --- | --- | --- | --- | --- |",
94
+ ];
95
+ const rows = report.checks.map((row) => "| " + [
96
+ `${row.id}: ${row.outcome}`, `${row.layer}: ${row.method}`, row.status,
97
+ row.observation, row.evidence.join(", "), [row.reason, row.next_action].filter(Boolean).join(" / "),
98
+ ].map(tableText).join(" | ") + " |");
99
+ return [...header, ...rows, "", `Validator: ${report.status}`,
100
+ ...report.errors.map((error) => `- ${tableText(error)}`)].join("\n") + "\n";
101
+ }
102
+
103
+ export async function checkAcceptance({ charterBytes, matrix, expectedCharterSha256, expectedContextSha256, evidenceRoot }) {
104
+ requireValue(hashPattern.test(expectedCharterSha256) && hashPattern.test(expectedContextSha256), "Trusted charter and context SHA-256 values are required");
105
+ requireValue(sha256(charterBytes) === expectedCharterSha256, "Frozen charter digest mismatch");
106
+ const charter = JSON.parse(charterBytes.toString());
107
+ validateCharter(charter);
108
+ requireValue(object(matrix) && matrix.schema_version === 1, "Invalid matrix schema version");
109
+ requireValue(matrix.charter_sha256 === expectedCharterSha256, "Matrix charter digest mismatch");
110
+ validateContext(matrix.context, charter.run_id);
111
+ requireValue(contextDigest(matrix.context) === expectedContextSha256, "Current context digest mismatch");
112
+ requireValue(Array.isArray(matrix.results), "Matrix requires results");
113
+ const root = await realpath(evidenceRoot);
114
+ const report = { status: "evidence_incomplete", checks: [], errors: [] };
115
+ const results = new Map();
116
+ const expectedIds = new Set(charter.criteria.map((criterion) => criterion.id));
117
+ for (const row of matrix.results) {
118
+ requireValue(object(row) && expectedIds.has(row.id) && !results.has(row.id), "Duplicate or unknown result criterion");
119
+ requireValue(object(row.layers) && Object.keys(row.layers).every((key) => layers.includes(key)), "Invalid result layers");
120
+ results.set(row.id, row);
121
+ }
122
+ for (const criterion of charter.criteria) {
123
+ for (const layer of layers) {
124
+ const spec = criterion.layers[layer];
125
+ const cell = results.get(criterion.id)?.layers[layer];
126
+ const row = { id: criterion.id, outcome: criterion.outcome, layer, method: spec.method ?? spec.reason,
127
+ status: "pending", observation: "", evidence: [], reason: "", next_action: "" };
128
+ report.checks.push(row);
129
+ if (spec.applicability === "not_applicable") {
130
+ row.status = "not_applicable";
131
+ row.reason = `${spec.reason} (${spec.source})`;
132
+ continue;
133
+ }
134
+ if (!object(cell)) {
135
+ row.reason = "Missing result";
136
+ row.next_action = "Run the specified check and record evidence";
137
+ } else {
138
+ row.observation = text(cell.observation) ? cell.observation : "";
139
+ row.next_action = text(cell.next_action) ? cell.next_action : "";
140
+ row.evidence = Array.isArray(cell.evidence) ? cell.evidence.map((entry) => object(entry) && text(entry.path) ? entry.path : "invalid reference") : [];
141
+ if (!["passed", "failed", "pending"].includes(cell.status)) {
142
+ row.reason = "Invalid status (results cannot waive a required layer)";
143
+ } else if (cell.context_sha256 !== expectedContextSha256 || cell.charter_sha256 !== expectedCharterSha256) {
144
+ row.reason = "Stale result; reverify for the current charter and context";
145
+ } else if (cell.status !== "passed") {
146
+ row.status = cell.status;
147
+ row.reason = text(cell.reason) ? cell.reason : "Missing unmet reason";
148
+ if (!row.next_action) row.reason += "; missing next action";
149
+ } else {
150
+ try {
151
+ requireValue(text(cell.action) && text(cell.observation), "Passed result needs action and observation");
152
+ requireValue(text(cell.observed_at) && /^\d{4}-\d{2}-\d{2}T.*(?:Z|[+-]\d{2}:\d{2})$/.test(cell.observed_at) &&
153
+ Number.isFinite(Date.parse(cell.observed_at)) && Date.parse(cell.observed_at) <= Date.now(), "Invalid or future observation timestamp");
154
+ requireValue(Array.isArray(cell.evidence) && cell.evidence.length > 0, "Passed result needs evidence");
155
+ for (const artifact of cell.evidence) await readEvidenceArtifact(root, artifact);
156
+ row.status = "passed";
157
+ } catch {
158
+ row.reason = "Invalid, missing, empty, unsafe, or changed evidence; check action, observation, timestamp and artifact hashes";
159
+ }
160
+ }
161
+ }
162
+ if (row.status !== "passed") report.errors.push(`${criterion.id}/${layer}: ${row.reason}`);
163
+ }
164
+ }
165
+ if (!report.errors.length) report.status = "evidence_complete";
166
+ report.markdown = renderChecklist(report);
167
+ return report;
168
+ }
169
+
170
+ async function main(args) {
171
+ requireValue(args.length === 4 || (args.length === 5 && args[4] === "--markdown"),
172
+ "Usage: node check-acceptance.mjs <charter.json> <matrix.json> <trusted-charter-sha256> <current-context-sha256> [--markdown]");
173
+ const [charterPath, matrixPath, expectedCharterSha256, expectedContextSha256] = args;
174
+ const load = async (target) => {
175
+ const stat = await lstat(target);
176
+ requireValue(stat.isFile() && stat.size <= 1024 * 1024, "Input must be a regular JSON file of at most 1 MiB");
177
+ return readFile(target);
178
+ };
179
+ const report = await checkAcceptance({ charterBytes: await load(charterPath),
180
+ matrix: JSON.parse((await load(matrixPath)).toString()), expectedCharterSha256,
181
+ expectedContextSha256, evidenceRoot: path.dirname(path.resolve(matrixPath)) });
182
+ process.stdout.write(args[4] === "--markdown" ? report.markdown : JSON.stringify(report, null, 2) + "\n");
183
+ process.exitCode = report.status === "evidence_complete" ? 0 : 1;
184
+ }
185
+
186
+ const entryPath = process.argv[1] ? await realpath(process.argv[1]).catch(() => null) : null;
187
+ if (entryPath === fileURLToPath(import.meta.url)) {
188
+ main(process.argv.slice(2)).catch(() => {
189
+ // Do not echo arbitrary JSON, filesystem contents, or sensitive input values.
190
+ process.stderr.write("Acceptance input invalid. Check JSON schema, paths, frozen charter and current context digests. Usage: node check-acceptance.mjs <charter.json> <matrix.json> <trusted-charter-sha256> <current-context-sha256> [--markdown]\n");
191
+ process.exitCode = 2;
192
+ });
193
+ }