@open-agent-toolkit/cli 0.2.26 → 0.2.27
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/assets/NOTICES.md +156 -0
- package/assets/docs/contributing/explainer-kit-verification.md +125 -0
- package/assets/docs/contributing/index.md +1 -0
- package/assets/docs/reference/troubleshooting.md +47 -0
- package/assets/docs/workflows/projects/artifacts.md +24 -6
- package/assets/docs/workflows/skills/explainer-kit-providers.md +144 -0
- package/assets/docs/workflows/skills/explainer-kit.md +121 -69
- package/assets/docs/workflows/skills/index.md +1 -0
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/explainer-kit/SKILL.md +18 -3
- package/assets/skills/explainer-kit/recipes/project-recap.json +43 -16
- package/assets/skills/explainer-kit/references/contracts.md +167 -20
- package/assets/skills/explainer-kit/references/golden-conformance.md +80 -0
- package/assets/skills/explainer-kit/references/visual-authoring.md +92 -0
- package/assets/skills/explainer-kit/references/visual-review.md +57 -0
- package/assets/skills/explainer-kit/schemas/author-request.v2.schema.json +172 -1
- package/assets/skills/explainer-kit/schemas/build-record.schema.json +7 -1
- package/assets/skills/explainer-kit/schemas/fact-base.schema.json +38 -2
- package/assets/skills/explainer-kit/schemas/manifest.schema.json +25 -1
- package/assets/skills/explainer-kit/schemas/run-request.schema.json +4 -0
- package/assets/skills/explainer-kit/schemas/set-plan.v1.schema.json +149 -0
- package/assets/skills/explainer-kit/schemas/visual-review-request.v1.schema.json +117 -0
- package/assets/skills/explainer-kit/schemas/visual-review-result.v1.schema.json +80 -0
- package/assets/skills/explainer-kit/scripts/lib/browser-runtime.mjs +148 -4
- package/assets/skills/explainer-kit/scripts/lib/catalog.mjs +243 -0
- package/assets/skills/explainer-kit/scripts/lib/contracts.mjs +586 -8
- package/assets/skills/explainer-kit/scripts/lib/diagram.mjs +285 -8
- package/assets/skills/explainer-kit/scripts/lib/durability.mjs +35 -0
- package/assets/skills/explainer-kit/scripts/lib/fact-base.mjs +144 -8
- package/assets/skills/explainer-kit/scripts/lib/package-coverage.mjs +379 -0
- package/assets/skills/explainer-kit/scripts/lib/png.mjs +287 -0
- package/assets/skills/explainer-kit/scripts/lib/qa.mjs +280 -8
- package/assets/skills/explainer-kit/scripts/lib/recipes.mjs +132 -3
- package/assets/skills/explainer-kit/scripts/lib/records.mjs +513 -21
- package/assets/skills/explainer-kit/scripts/lib/render.mjs +67 -3
- package/assets/skills/explainer-kit/scripts/lib/s3-static.mjs +43 -1
- package/assets/skills/explainer-kit/scripts/lib/set-plan.mjs +208 -0
- package/assets/skills/explainer-kit/scripts/lib/source-backlinks.mjs +218 -0
- package/assets/skills/explainer-kit/scripts/lib/visual-review.mjs +380 -0
- package/assets/skills/explainer-kit/scripts/render-qa.mjs +48 -10
- package/assets/skills/explainer-kit/scripts/run.mjs +859 -134
- package/assets/skills/oat-explainer-kit/SKILL.md +40 -12
- package/assets/skills/oat-explainer-kit/references/author-callback.md +12 -10
- package/assets/skills/oat-explainer-kit/references/lifecycle-contract.md +40 -2
- package/assets/skills/oat-explainer-kit/references/visual-review-callback.md +72 -0
- package/assets/skills/oat-explainer-kit/scripts/bind-project-sources.mjs +167 -5
- package/assets/skills/oat-explainer-kit/scripts/finalize-tracked-run.mjs +92 -5
- package/assets/skills/oat-explainer-kit/scripts/run.mjs +324 -2
- package/dist/commands/project/archive/archive-utils.d.ts +1 -0
- package/dist/commands/project/archive/archive-utils.d.ts.map +1 -1
- package/dist/commands/project/archive/archive-utils.js +109 -42
- package/dist/commands/project/archive/explainer-package-coverage.d.ts +14 -0
- package/dist/commands/project/archive/explainer-package-coverage.d.ts.map +1 -0
- package/dist/commands/project/archive/explainer-package-coverage.js +27 -0
- package/dist/commands/project/archive/explainer-source-backlinks.d.ts +18 -0
- package/dist/commands/project/archive/explainer-source-backlinks.d.ts.map +1 -0
- package/dist/commands/project/archive/explainer-source-backlinks.js +27 -0
- package/dist/commands/project/archive/push-runner.d.ts +2 -1
- package/dist/commands/project/archive/push-runner.d.ts.map +1 -1
- package/dist/commands/project/archive/push-runner.js +5 -1
- package/dist/release/public-package-contract.d.ts +6 -0
- package/dist/release/public-package-contract.d.ts.map +1 -1
- package/dist/release/public-package-contract.js +75 -0
- package/package.json +2 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: oat-explainer-kit
|
|
3
|
-
version: 1.0.
|
|
3
|
+
version: 1.0.5
|
|
4
4
|
description: Use when building project explainers or recaps from OAT configuration, state, and lifecycle artifacts.
|
|
5
5
|
disable-model-invocation: false
|
|
6
6
|
user-invocable: true
|
|
@@ -19,7 +19,10 @@ Adapt OAT project context into the versioned request consumed by the canonical
|
|
|
19
19
|
- Derive canonical project or repository output roots.
|
|
20
20
|
- Bind OAT lifecycle artifacts to generic recipe source roles.
|
|
21
21
|
- Resolve project explainer and recap intent before invoking the core.
|
|
22
|
+
- Resolve one provider-neutral set planner for unattended project recaps.
|
|
22
23
|
- Require lifecycle callers to construct a brief-aware author seam.
|
|
24
|
+
- Require first-class browser-evidence and whole-set visual-review providers
|
|
25
|
+
for unattended project recaps.
|
|
23
26
|
|
|
24
27
|
## Dependency Direction
|
|
25
28
|
|
|
@@ -34,7 +37,7 @@ rollback, and operator-owned real-wrapper gate, use `references/migration.md`.
|
|
|
34
37
|
|
|
35
38
|
Before reading OAT config or invoking the core, call
|
|
36
39
|
`scripts/check-core.mjs#checkCoreCompatibility` with this installed skill
|
|
37
|
-
directory and minimum core version `2.0.
|
|
40
|
+
directory and minimum core version `2.0.3`. Continue only when it returns
|
|
38
41
|
`ok: true`.
|
|
39
42
|
|
|
40
43
|
- Missing core: stop and show
|
|
@@ -55,7 +58,7 @@ Call `scripts/run.mjs#runOatExplainer` with the repository root, project
|
|
|
55
58
|
invocation, active project path, recipe, slug, lifecycle mode, and any explicit
|
|
56
59
|
runtime overrides. The adapter:
|
|
57
60
|
|
|
58
|
-
1. checks the user-scoped installed core at minimum version `2.0.
|
|
61
|
+
1. checks the user-scoped installed core at minimum version `2.0.3`;
|
|
59
62
|
2. resolves only the public `explainers.*` and `workflow.explainers.*` keys;
|
|
60
63
|
3. derives the canonical project output root;
|
|
61
64
|
4. binds approved OAT artifacts to the recipe's single `project` source set;
|
|
@@ -69,15 +72,40 @@ Missing optional artifacts are omitted, but at least one approved lifecycle
|
|
|
69
72
|
artifact is required. An explicit supplied fact-base path bypasses artifact
|
|
70
73
|
federation and is passed through as `factBase.mode: supplied`.
|
|
71
74
|
|
|
72
|
-
Before invocation, read `references/author-callback.md
|
|
73
|
-
|
|
74
|
-
`
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
75
|
+
Before invocation, read `references/author-callback.md`. Unattended
|
|
76
|
+
`project-recap` runs require exactly one provider-neutral set planner:
|
|
77
|
+
in-process callers supply `planSet`, while JSON/CLI callers supply
|
|
78
|
+
`planSetModulePath` naming a module with a `planSet` function export. The
|
|
79
|
+
adapter resolves this executable capability and passes only the callback to the
|
|
80
|
+
core. It never persists provider configuration or module paths.
|
|
81
|
+
|
|
82
|
+
Construct exactly one provider-neutral author seam in both modes: in-process
|
|
83
|
+
callers supply `author`, while JSON/CLI callers supply `authorModulePath` naming
|
|
84
|
+
a module with an `author` function export. The core invokes that callback once
|
|
85
|
+
per planned artifact. Every request carries the immutable shared `setContext`,
|
|
86
|
+
the matching `plannedArtifact`, the recipe brief, theme, and any
|
|
87
|
+
recipe-selected artistic shell. Those bundled inputs are the required
|
|
88
|
+
medium-specific guidance; an optional visual-explainer installation may enhance
|
|
89
|
+
provider execution but is never required. The adapter validates and resolves
|
|
90
|
+
executable inputs before passing them to `core.runExplainer`; callbacks and
|
|
91
|
+
module paths never enter the persisted run request.
|
|
92
|
+
|
|
93
|
+
Also read `references/visual-review-callback.md`. Unattended `project-recap`
|
|
94
|
+
runs require exactly one trusted launched-Chromium browser session and one
|
|
95
|
+
whole-set visual critic. In-process callers supply a branded `browserSession`
|
|
96
|
+
created by the compatible core and `visualCritic`; JSON/CLI callers supply
|
|
97
|
+
`browserSessionModulePath` and `visualCriticModulePath`, naming modules with
|
|
98
|
+
matching exports. Bare browser callbacks and caller-authored runtime metadata
|
|
99
|
+
are rejected. These are first-class adapter inputs; do not place either
|
|
100
|
+
provider in `coreOptions`.
|
|
101
|
+
|
|
102
|
+
The adapter resolves all executable providers before core invocation, enforces
|
|
103
|
+
direct-versus-module mutual exclusion, and requires distinct identities for
|
|
104
|
+
authoring, fact criticism, browser evidence, and visual criticism. A missing,
|
|
105
|
+
forged, or deterministic fixture session fails production validation before the
|
|
106
|
+
core runs. A runtime browser or visual-review failure is retained by the core
|
|
107
|
+
as `built-needs-review`, never as a successful durability or publication
|
|
108
|
+
outcome.
|
|
81
109
|
|
|
82
110
|
Unattended project runs pass `approved-oat-artifacts` provenance to the core's
|
|
83
111
|
content-approval seam and never prompt. Automated completion and
|
|
@@ -15,6 +15,10 @@ core constructs that request from recipe-owned policy:
|
|
|
15
15
|
|
|
16
16
|
- `brief` contains the prose loaded from the floor entry or expansion profile's
|
|
17
17
|
`briefRef`; the author must treat the inlined brief as its editorial contract.
|
|
18
|
+
- `visualAuthoringGuidance` contains the installed core's bundled
|
|
19
|
+
representation, hierarchy, responsive-navigation, table, diagram, and deck
|
|
20
|
+
rules. Treat it as the medium baseline; do not read ambient or home-directory
|
|
21
|
+
guidance.
|
|
18
22
|
- `factBase` contains the reconciled evidence for the run. Ground claims in that
|
|
19
23
|
fact base and do not replace it with ambient project context.
|
|
20
24
|
- `authoring` selects the output path. Return Markdown for `markdown` and a
|
|
@@ -24,23 +28,21 @@ core constructs that request from recipe-owned policy:
|
|
|
24
28
|
brief's license to elaborate.
|
|
25
29
|
- A floor request may include `floor.requiredNarrative`, which identifies the
|
|
26
30
|
narrative coverage checked later as non-blocking guidelines.
|
|
31
|
+
- `setContext` is the complete immutable planner-owned portfolio and shared
|
|
32
|
+
ledger. `plannedArtifact` is the exact entry assigned to this callback;
|
|
33
|
+
preserve its identity, sources, draft, and visual intent.
|
|
27
34
|
|
|
28
35
|
Return one matching `explainer-kit.author-result/v2`: preserve `artifactId`,
|
|
29
36
|
set exactly one of `content.markdown` or `content.html`, and include
|
|
30
37
|
`provenance`. Do not persist provider credentials or callback configuration in
|
|
31
38
|
the result.
|
|
32
39
|
|
|
33
|
-
##
|
|
40
|
+
## Planner-owned expansion
|
|
34
41
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
The core validates proposal IDs, profile membership, collisions, and finite
|
|
41
|
-
caps. It rejects malformed proposals, records over-limit proposals as warnings,
|
|
42
|
-
and issues a new author request for every accepted artifact. Handle each
|
|
43
|
-
follow-up request independently and do not recursively propose more artifacts.
|
|
42
|
+
The set planner finalizes the complete floor and expansion portfolio before any
|
|
43
|
+
author runs. Authors must not return `proposedArtifacts` or otherwise add,
|
|
44
|
+
remove, replace, or reorder portfolio entries. Handle only the supplied
|
|
45
|
+
`plannedArtifact`; the core rejects author-driven expansion after planning.
|
|
44
46
|
|
|
45
47
|
## Lifecycle invocation
|
|
46
48
|
|
|
@@ -86,13 +86,48 @@ The resolved callback is passed only as the `author` option to
|
|
|
86
86
|
`run-request.json`, or another retained data contract. Interactive runs use the
|
|
87
87
|
same author contract and differ only at the later approval gate.
|
|
88
88
|
|
|
89
|
+
## Browser and visual-review execution
|
|
90
|
+
|
|
91
|
+
Every unattended `project-recap` must provide exactly one browser-evidence
|
|
92
|
+
session and exactly one whole-set visual critic through the adapter's
|
|
93
|
+
first-class boundary. In-process callers pass `browserSession`, created by the
|
|
94
|
+
compatible core's `createBrowserProbeSession()`, and `visualCritic`. JSON-only
|
|
95
|
+
and official CLI callers pass `browserSessionModulePath` and
|
|
96
|
+
`visualCriticModulePath`, naming modules whose matching exports are
|
|
97
|
+
`browserSession` and `visualCritic`. Direct-plus-module conflicts, invalid
|
|
98
|
+
exports, bare `browserProbe` inputs, and the legacy
|
|
99
|
+
`coreOptions.browserProbe`, `coreOptions.browserSession`, or
|
|
100
|
+
`coreOptions.visualCritic` routes fail before core invocation.
|
|
101
|
+
|
|
102
|
+
The author, fact critic, branded session probe, and visual critic must have
|
|
103
|
+
distinct callback identities. The adapter asks the loaded core to validate the
|
|
104
|
+
session's private brand and launched-Chromium runtime before passing it to
|
|
105
|
+
`core.runExplainer`. Executable callbacks, descriptors, and module paths never
|
|
106
|
+
enter the retained run request. Deterministic fixture sessions are rejected for
|
|
107
|
+
unattended project recaps. See `visual-review-callback.md` for the session,
|
|
108
|
+
request/result, and byte-bound whole-set review contracts.
|
|
109
|
+
|
|
110
|
+
The core retains canonical 320, 768, and 1440 viewport screenshots, paired
|
|
111
|
+
`explainer-kit.browser-evidence/v2` metrics with launched Chromium name,
|
|
112
|
+
version, and capture identity, each review request/result, cohesion
|
|
113
|
+
observations, and any one-pass revision record. Missing, malformed, forged,
|
|
114
|
+
cross-record mismatched, stale, or failed review-chain evidence terminates as
|
|
115
|
+
`built-needs-review`; durability and publication remain blocked.
|
|
116
|
+
|
|
89
117
|
## Tracked-run finalization
|
|
90
118
|
|
|
91
119
|
`planTrackedRunFinalization(request, context)` is the shared command planner for
|
|
92
120
|
tracked project explainer and recap runs. The request contains `runRoot`,
|
|
93
121
|
`manifestPath`, `commitMode`, and optional `relocatedFrom`. Context supplies the
|
|
94
|
-
repository root, project name,
|
|
95
|
-
full artifact commit SHA.
|
|
122
|
+
repository root, project name, an explicit compatible `coreRoot`, and, for
|
|
123
|
+
`completion-bookkeeping`, the existing full artifact commit SHA. Finalization
|
|
124
|
+
dynamically loads the versioned package-coverage contract from that core root;
|
|
125
|
+
it does not maintain an adapter-local path list.
|
|
126
|
+
|
|
127
|
+
A core `built-needs-review` outcome is a terminal review gate, not a
|
|
128
|
+
non-durable success. The planner rejects it before producing artifact,
|
|
129
|
+
attestation, evidence-commit, or push commands. The recap must instead be
|
|
130
|
+
reviewed and rebuilt to a passing visual-review outcome.
|
|
96
131
|
|
|
97
132
|
The returned stages must run in order:
|
|
98
133
|
|
|
@@ -152,6 +187,9 @@ export remains committed, the mutable records retain the warning and
|
|
|
152
187
|
complete. A later attestation may recover durability without repeating the
|
|
153
188
|
archive.
|
|
154
189
|
|
|
190
|
+
This recovery path applies only to `built-not-durable`.
|
|
191
|
+
`built-needs-review` cannot be exported, attested, finalized, or pushed.
|
|
192
|
+
|
|
155
193
|
Post-archive summary and PR recap links target `projectRecapExport.exportRoot`
|
|
156
194
|
under `.oat/repo/reference/project-recaps/` on the current head branch. The
|
|
157
195
|
tracked summary export and the PR body may carry that link; the gitignored
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Browser and visual-review callbacks
|
|
2
|
+
|
|
3
|
+
Unattended project recaps require two provider-neutral executable seams. Supply
|
|
4
|
+
each seam either directly or through a module path, never both:
|
|
5
|
+
|
|
6
|
+
| Role | Direct input | Module-path input | Required export |
|
|
7
|
+
| ---------------- | ---------------- | -------------------------- | ---------------- |
|
|
8
|
+
| Browser evidence | `browserSession` | `browserSessionModulePath` | `browserSession` |
|
|
9
|
+
| Whole-set review | `visualCritic` | `visualCriticModulePath` | `visualCritic` |
|
|
10
|
+
|
|
11
|
+
Do not route these callbacks through `coreOptions`. The adapter resolves and
|
|
12
|
+
validates both providers before core invocation and keeps callbacks and module
|
|
13
|
+
paths out of retained requests.
|
|
14
|
+
|
|
15
|
+
## Browser evidence
|
|
16
|
+
|
|
17
|
+
Create the direct descriptor with the compatible core's
|
|
18
|
+
`createBrowserProbeSession()`. A module-path provider exports that branded
|
|
19
|
+
descriptor as `browserSession`; it does not export a bare callback or a
|
|
20
|
+
caller-authored metadata object. The core derives Chromium type and version
|
|
21
|
+
from the launched `Browser` instance and validates the private session brand.
|
|
22
|
+
Callers remain responsible for closing the session.
|
|
23
|
+
|
|
24
|
+
The descriptor's probe receives one provider-neutral request for each artifact,
|
|
25
|
+
viewport, and required interaction scenario. The request includes the rendered
|
|
26
|
+
artifact, exact viewport dimensions, scenario, interaction expectations, and,
|
|
27
|
+
for the default scenario, an absolute `screenshotPath`. The probe writes a PNG
|
|
28
|
+
to that path and returns measured browser facts:
|
|
29
|
+
|
|
30
|
+
- `pageOverflowX` as a boolean;
|
|
31
|
+
- `clippedX`, `viewportClipped`, and `unreadableHeadings` as arrays;
|
|
32
|
+
- `reducedMotion` as a boolean;
|
|
33
|
+
- `keyboard` as an object describing observed key behavior; and
|
|
34
|
+
- applicable deck, theme-toggle, animation, and layout measurements.
|
|
35
|
+
|
|
36
|
+
For unattended project recaps the core chooses the canonical 320, 768, and 1440
|
|
37
|
+
widths. The retained PNG signature and IHDR dimensions must match the requested
|
|
38
|
+
viewport. The core writes paired metrics JSON itself; callback assertions do
|
|
39
|
+
not substitute for retained evidence. Those
|
|
40
|
+
`explainer-kit.browser-evidence/v2` records retain the launched Chromium name,
|
|
41
|
+
version, capture settings, and stable capture identity. Deterministic fixture
|
|
42
|
+
sessions are explicit test-only descriptors and are rejected by unattended
|
|
43
|
+
project-recap production paths.
|
|
44
|
+
|
|
45
|
+
## Whole-set visual review
|
|
46
|
+
|
|
47
|
+
The visual critic receives one
|
|
48
|
+
`explainer-kit.visual-review-request/v1` plus a confined evidence reader. The
|
|
49
|
+
request contains every rendered artifact, exact rendered-content hashes,
|
|
50
|
+
viewport-matched screenshot and metrics hashes, observed cohesion claims, and
|
|
51
|
+
the exact browser runtime and capture identity shared by every evidence record.
|
|
52
|
+
Use the evidence reader to inspect the byte snapshot named by the request.
|
|
53
|
+
|
|
54
|
+
Return `explainer-kit.visual-review-result/v1` with:
|
|
55
|
+
|
|
56
|
+
- a safe `reviewId`;
|
|
57
|
+
- the request's exact `requestId` and `requestHash`;
|
|
58
|
+
- an ISO `reviewedAt`;
|
|
59
|
+
- `disposition` equal to `pass`, `correct`, or `fail`;
|
|
60
|
+
- the complete reviewed `artifactIds`; and
|
|
61
|
+
- structured `findings`.
|
|
62
|
+
|
|
63
|
+
The callback must not mutate rendered files, screenshots, or metrics. The core
|
|
64
|
+
revalidates all bound bytes after the callback returns.
|
|
65
|
+
|
|
66
|
+
## Correction and terminal behavior
|
|
67
|
+
|
|
68
|
+
`correct` permits one correction callback and exactly one final review. There
|
|
69
|
+
is never a second correction or third review. Missing evidence, invalid
|
|
70
|
+
contracts, thrown callbacks, byte mutation, unresolved correction, or a `fail`
|
|
71
|
+
disposition produces `built-needs-review`. Such a run retains available review
|
|
72
|
+
evidence but invokes neither durability nor publication.
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
import { execFile as execFileCallback } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
1
3
|
import { access, readFile, realpath } from 'node:fs/promises';
|
|
2
|
-
import { basename, join } from 'node:path';
|
|
4
|
+
import { basename, join, relative, resolve, sep } from 'node:path';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
|
|
7
|
+
const execFile = promisify(execFileCallback);
|
|
3
8
|
|
|
4
9
|
const RECIPE_ARTIFACTS = Object.freeze({
|
|
5
10
|
'project-explainer': ['plan', 'design', 'spec'],
|
|
@@ -8,8 +13,10 @@ const RECIPE_ARTIFACTS = Object.freeze({
|
|
|
8
13
|
|
|
9
14
|
export async function bindProjectSources({
|
|
10
15
|
projectRoot,
|
|
16
|
+
repoRoot,
|
|
11
17
|
recipe,
|
|
12
18
|
suppliedFactBasePath,
|
|
19
|
+
reviewedRepository,
|
|
13
20
|
}) {
|
|
14
21
|
if (!projectRoot) {
|
|
15
22
|
throw new TypeError('projectRoot is required to bind OAT artifacts.');
|
|
@@ -20,6 +27,9 @@ export async function bindProjectSources({
|
|
|
20
27
|
}
|
|
21
28
|
|
|
22
29
|
const canonicalProjectRoot = await realpath(projectRoot);
|
|
30
|
+
const repository =
|
|
31
|
+
reviewedRepository ??
|
|
32
|
+
(repoRoot ? await resolveReviewedRepository(repoRoot) : null);
|
|
23
33
|
if (suppliedFactBasePath) {
|
|
24
34
|
const path = await realpath(suppliedFactBasePath);
|
|
25
35
|
return {
|
|
@@ -30,7 +40,8 @@ export async function bindProjectSources({
|
|
|
30
40
|
},
|
|
31
41
|
reviewedSource: {
|
|
32
42
|
kind: 'approved-fact-base',
|
|
33
|
-
locator: path,
|
|
43
|
+
locator: repository?.repositoryUrl ?? path,
|
|
44
|
+
...(repository && repository),
|
|
34
45
|
},
|
|
35
46
|
};
|
|
36
47
|
}
|
|
@@ -59,6 +70,10 @@ export async function bindProjectSources({
|
|
|
59
70
|
`No approved OAT lifecycle artifacts were found for ${recipe}.`,
|
|
60
71
|
);
|
|
61
72
|
}
|
|
73
|
+
const reviewedSources =
|
|
74
|
+
repository && repoRoot
|
|
75
|
+
? await bindReviewedSources(sources, repoRoot, repository)
|
|
76
|
+
: undefined;
|
|
62
77
|
|
|
63
78
|
return {
|
|
64
79
|
factBase: {
|
|
@@ -68,25 +83,172 @@ export async function bindProjectSources({
|
|
|
68
83
|
},
|
|
69
84
|
reviewedSource: {
|
|
70
85
|
kind: 'approved-oat-artifacts',
|
|
71
|
-
locator: canonicalProjectRoot,
|
|
86
|
+
locator: repository?.repositoryUrl ?? canonicalProjectRoot,
|
|
87
|
+
...(repository && repository),
|
|
72
88
|
},
|
|
73
|
-
|
|
89
|
+
...(reviewedSources && {
|
|
90
|
+
sourceProvenance: reviewedSources.provenance,
|
|
91
|
+
}),
|
|
92
|
+
sourceLoader: reviewedSources
|
|
93
|
+
? (source) => loadReviewedOatArtifact(source, reviewedSources.documents)
|
|
94
|
+
: loadOatArtifact,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function resolveReviewedRepository(
|
|
99
|
+
repoRoot,
|
|
100
|
+
{ command = execFile } = {},
|
|
101
|
+
) {
|
|
102
|
+
const cwd = resolve(repoRoot);
|
|
103
|
+
const [{ stdout: revisionOutput }, { stdout: remoteOutput }] =
|
|
104
|
+
await Promise.all([
|
|
105
|
+
command('git', ['rev-parse', 'HEAD'], { cwd }),
|
|
106
|
+
command('git', ['config', '--get', 'remote.origin.url'], { cwd }),
|
|
107
|
+
]);
|
|
108
|
+
const revision = String(revisionOutput).trim();
|
|
109
|
+
if (!/^[a-f0-9]{40}$/.test(revision)) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
'Reviewed OAT sources require a full commit SHA, not a moving revision.',
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
const repository = githubRepositoryIdentity(String(remoteOutput).trim());
|
|
115
|
+
return {
|
|
116
|
+
repository,
|
|
117
|
+
repositoryUrl: `https://github.com/${repository}`,
|
|
118
|
+
revision,
|
|
74
119
|
};
|
|
75
120
|
}
|
|
76
121
|
|
|
77
122
|
async function loadOatArtifact(source) {
|
|
78
|
-
|
|
123
|
+
return oatArtifactDocument(
|
|
124
|
+
source,
|
|
125
|
+
(await readFile(source.locator, 'utf8')).trim(),
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function loadReviewedOatArtifact(source, documents) {
|
|
130
|
+
const document = documents.get(source.id);
|
|
131
|
+
if (!document) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
`No reviewed Git blob was bound for OAT source ${source.id}.`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
return structuredClone(document);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function oatArtifactDocument(source, text, sourceHash) {
|
|
79
140
|
return {
|
|
80
141
|
claims: [
|
|
81
142
|
{
|
|
82
143
|
id: source.id,
|
|
83
144
|
text,
|
|
84
145
|
locator: source.locator,
|
|
146
|
+
lineRange: lineRangeFor(text),
|
|
85
147
|
},
|
|
86
148
|
],
|
|
149
|
+
...(sourceHash && { sourceHash }),
|
|
87
150
|
};
|
|
88
151
|
}
|
|
89
152
|
|
|
153
|
+
async function bindReviewedSources(sources, repoRoot, repository) {
|
|
154
|
+
const canonicalRepoRoot = await realpath(repoRoot);
|
|
155
|
+
const entries = await Promise.all(
|
|
156
|
+
sources.map(async (source) => {
|
|
157
|
+
const repositoryPath = relative(canonicalRepoRoot, source.locator)
|
|
158
|
+
.split(sep)
|
|
159
|
+
.join('/');
|
|
160
|
+
if (
|
|
161
|
+
!repositoryPath ||
|
|
162
|
+
repositoryPath.startsWith('../') ||
|
|
163
|
+
repositoryPath === '..'
|
|
164
|
+
) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
`OAT source ${source.id} is outside the reviewed repository.`,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
await execFile(
|
|
171
|
+
'git',
|
|
172
|
+
['ls-files', '--error-unmatch', '--', repositoryPath],
|
|
173
|
+
{ cwd: canonicalRepoRoot },
|
|
174
|
+
);
|
|
175
|
+
} catch {
|
|
176
|
+
throw new Error(
|
|
177
|
+
`OAT source ${source.id} is not tracked in the reviewed repository.`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
let reviewedBytes;
|
|
181
|
+
try {
|
|
182
|
+
({ stdout: reviewedBytes } = await execFile(
|
|
183
|
+
'git',
|
|
184
|
+
['show', `${repository.revision}:${repositoryPath}`],
|
|
185
|
+
{
|
|
186
|
+
cwd: canonicalRepoRoot,
|
|
187
|
+
encoding: null,
|
|
188
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
189
|
+
},
|
|
190
|
+
));
|
|
191
|
+
} catch {
|
|
192
|
+
throw new Error(
|
|
193
|
+
`OAT source ${source.id} is absent from reviewed revision ${repository.revision}.`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
const workingBytes = await readFile(source.locator);
|
|
197
|
+
const canonicalReviewedBytes = Buffer.from(reviewedBytes);
|
|
198
|
+
if (!workingBytes.equals(canonicalReviewedBytes)) {
|
|
199
|
+
throw new Error(
|
|
200
|
+
`OAT source ${source.id} working tree bytes mismatch reviewed Git blob at revision ${repository.revision}.`,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
const text = canonicalReviewedBytes.toString('utf8').trim();
|
|
204
|
+
return {
|
|
205
|
+
id: source.id,
|
|
206
|
+
provenance: {
|
|
207
|
+
repository: repository.repository,
|
|
208
|
+
revision: repository.revision,
|
|
209
|
+
path: repositoryPath,
|
|
210
|
+
lineRange: lineRangeFor(text),
|
|
211
|
+
},
|
|
212
|
+
document: oatArtifactDocument(
|
|
213
|
+
source,
|
|
214
|
+
text,
|
|
215
|
+
`sha256:${createHash('sha256')
|
|
216
|
+
.update(canonicalReviewedBytes)
|
|
217
|
+
.digest('hex')}`,
|
|
218
|
+
),
|
|
219
|
+
};
|
|
220
|
+
}),
|
|
221
|
+
);
|
|
222
|
+
return {
|
|
223
|
+
provenance: Object.fromEntries(
|
|
224
|
+
entries.map((entry) => [entry.id, entry.provenance]),
|
|
225
|
+
),
|
|
226
|
+
documents: new Map(entries.map((entry) => [entry.id, entry.document])),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function lineRangeFor(text) {
|
|
231
|
+
return {
|
|
232
|
+
start: 1,
|
|
233
|
+
end: Math.max(1, text.split('\n').length),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function githubRepositoryIdentity(remote) {
|
|
238
|
+
const patterns = [
|
|
239
|
+
/^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?$/,
|
|
240
|
+
/^git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/,
|
|
241
|
+
/^ssh:\/\/git@github\.com\/([^/]+)\/([^/]+?)(?:\.git)?$/,
|
|
242
|
+
];
|
|
243
|
+
for (const pattern of patterns) {
|
|
244
|
+
const match = remote.match(pattern);
|
|
245
|
+
if (match) return `${match[1]}/${match[2]}`;
|
|
246
|
+
}
|
|
247
|
+
throw new Error(
|
|
248
|
+
'Reviewed OAT sources require a canonical GitHub origin repository.',
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
90
252
|
function authoritativeTopics(id) {
|
|
91
253
|
return {
|
|
92
254
|
plan: ['phases', 'validation-approach'],
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
1
2
|
import { readFile, realpath } from 'node:fs/promises';
|
|
2
|
-
import { relative, resolve, sep } from 'node:path';
|
|
3
|
+
import { join, relative, resolve, sep } from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
3
5
|
|
|
4
6
|
const MODES = new Set(['dedicated', 'completion-bookkeeping']);
|
|
5
7
|
const ARTIFACT_COMMIT_TOKEN = '$ARTIFACT_COMMIT';
|
|
6
8
|
const SHA_PATTERN = /^[a-f0-9]{40}$/;
|
|
9
|
+
const PACKAGE_COVERAGE_VERSION = 'explainer-kit.package-coverage/v2';
|
|
7
10
|
|
|
8
11
|
export async function planTrackedRunFinalization(request, context = {}) {
|
|
9
12
|
assertRequest(request);
|
|
@@ -21,10 +24,16 @@ export async function planTrackedRunFinalization(request, context = {}) {
|
|
|
21
24
|
'Finalization requires an explainer-kit.manifest/v1 record.',
|
|
22
25
|
);
|
|
23
26
|
}
|
|
27
|
+
if (manifest.outcome === 'built-needs-review') {
|
|
28
|
+
throw new Error(
|
|
29
|
+
'built-needs-review requires a passing visual review before finalization.',
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
const packageCoverage = await loadPackageCoverage(context.coreRoot);
|
|
24
33
|
|
|
25
|
-
const immutablePaths =
|
|
26
|
-
|
|
27
|
-
);
|
|
34
|
+
const immutablePaths = (
|
|
35
|
+
await immutablePackagePaths(manifest, runRoot, packageCoverage)
|
|
36
|
+
).map((path) => toRepoPath(repoRoot, resolveRunPath(runRoot, path)));
|
|
28
37
|
const mutablePaths = [
|
|
29
38
|
toRepoPath(repoRoot, manifestPath),
|
|
30
39
|
toRepoPath(repoRoot, resolveRunPath(runRoot, manifest.buildRecord?.path)),
|
|
@@ -245,7 +254,7 @@ function commitCommands(paths, subject) {
|
|
|
245
254
|
];
|
|
246
255
|
}
|
|
247
256
|
|
|
248
|
-
function immutablePackagePaths(manifest) {
|
|
257
|
+
async function immutablePackagePaths(manifest, runRoot, packageCoverage) {
|
|
249
258
|
if (
|
|
250
259
|
!manifest.immutableHashes ||
|
|
251
260
|
typeof manifest.immutableHashes !== 'object' ||
|
|
@@ -257,9 +266,87 @@ function immutablePackagePaths(manifest) {
|
|
|
257
266
|
if (paths.length === 0) {
|
|
258
267
|
throw new Error('Manifest does not identify a complete immutable package.');
|
|
259
268
|
}
|
|
269
|
+
const verifiedBytes = new Map();
|
|
270
|
+
for (const path of paths) {
|
|
271
|
+
const expected = manifest.immutableHashes[path];
|
|
272
|
+
if (!/^sha256:[a-f0-9]{64}$/.test(expected)) {
|
|
273
|
+
throw new Error(`Manifest has an invalid immutable hash for ${path}.`);
|
|
274
|
+
}
|
|
275
|
+
const bytes = await readFile(resolveRunPath(runRoot, path));
|
|
276
|
+
const actual = `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
|
|
277
|
+
if (actual !== expected) {
|
|
278
|
+
throw new Error(`Immutable package hash mismatch for ${path}.`);
|
|
279
|
+
}
|
|
280
|
+
verifiedBytes.set(path, bytes);
|
|
281
|
+
}
|
|
282
|
+
const runMode =
|
|
283
|
+
manifest.recipe?.id === 'project-recap'
|
|
284
|
+
? verifiedRunMode(verifiedBytes.get('run-request.json'))
|
|
285
|
+
: undefined;
|
|
286
|
+
const required = packageCoverage.requiredImmutablePackagePaths(manifest, {
|
|
287
|
+
runMode,
|
|
288
|
+
});
|
|
289
|
+
const missing = required.filter(
|
|
290
|
+
(path) => !(path in manifest.immutableHashes),
|
|
291
|
+
);
|
|
292
|
+
if (missing.length > 0) {
|
|
293
|
+
throw new Error(
|
|
294
|
+
`Manifest immutable hashes do not cover the canonical package: ${missing.join(', ')}.`,
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
await packageCoverage.validateImmutablePackageEvidence(manifest, {
|
|
298
|
+
runMode,
|
|
299
|
+
read: (path) => verifiedBytes.get(path),
|
|
300
|
+
});
|
|
260
301
|
return paths;
|
|
261
302
|
}
|
|
262
303
|
|
|
304
|
+
function verifiedRunMode(bytes) {
|
|
305
|
+
if (!Buffer.isBuffer(bytes)) {
|
|
306
|
+
throw new Error(
|
|
307
|
+
'Manifest must include hash-verified run-request.json before package coverage is evaluated.',
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
let request;
|
|
311
|
+
try {
|
|
312
|
+
request = JSON.parse(bytes.toString('utf8'));
|
|
313
|
+
} catch {
|
|
314
|
+
throw new Error(
|
|
315
|
+
'Hash-verified run-request.json must contain valid JSON before package coverage is evaluated.',
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
if (!['interactive', 'unattended'].includes(request?.mode)) {
|
|
319
|
+
throw new Error(
|
|
320
|
+
'Hash-verified run-request.json must declare interactive or unattended mode.',
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
return request.mode;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async function loadPackageCoverage(coreRoot) {
|
|
327
|
+
const root = await realpathRequired(coreRoot, 'coreRoot');
|
|
328
|
+
const modulePath = join(root, 'scripts', 'lib', 'package-coverage.mjs');
|
|
329
|
+
let loaded;
|
|
330
|
+
try {
|
|
331
|
+
loaded = await import(pathToFileURL(modulePath).href);
|
|
332
|
+
} catch (loadError) {
|
|
333
|
+
throw new Error(
|
|
334
|
+
`Compatible explainer package coverage could not be loaded from coreRoot: ${loadError.message}`,
|
|
335
|
+
{ cause: loadError },
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
if (
|
|
339
|
+
loaded.PACKAGE_COVERAGE_VERSION !== PACKAGE_COVERAGE_VERSION ||
|
|
340
|
+
typeof loaded.requiredImmutablePackagePaths !== 'function' ||
|
|
341
|
+
typeof loaded.validateImmutablePackageEvidence !== 'function'
|
|
342
|
+
) {
|
|
343
|
+
throw new Error(
|
|
344
|
+
`coreRoot must provide ${PACKAGE_COVERAGE_VERSION} package coverage.`,
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
return loaded;
|
|
348
|
+
}
|
|
349
|
+
|
|
263
350
|
function resolveRunPath(runRoot, path) {
|
|
264
351
|
const target = resolve(runRoot, path);
|
|
265
352
|
assertWithin(runRoot, target, `Manifest path ${path}`);
|