@open-agent-toolkit/cli 0.2.73 → 0.2.74
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/bundle-metadata.json +1 -1
- package/assets/docs/cli-utilities/configuration.md +3 -56
- package/assets/docs/cli-utilities/tool-packs.md +4 -4
- package/assets/docs/contributing/code.md +2 -2
- package/assets/docs/contributing/index.md +0 -1
- package/assets/docs/reference/cli-reference.md +1 -1
- package/assets/docs/reference/troubleshooting.md +17 -40
- package/assets/docs/workflows/projects/artifacts.md +16 -34
- package/assets/docs/workflows/projects/lifecycle.md +20 -19
- package/assets/docs/workflows/skills/explainer-kit.md +180 -440
- package/assets/docs/workflows/skills/index.md +1 -2
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/explainer-kit/SKILL.md +96 -171
- package/assets/skills/explainer-kit/recipes/engineer-tour.json +1 -22
- package/assets/skills/explainer-kit/recipes/program-recap.json +1 -29
- package/assets/skills/explainer-kit/recipes/project-explainer.json +1 -22
- package/assets/skills/explainer-kit/recipes/project-recap.v2.json +1 -43
- package/assets/skills/explainer-kit/references/fact-base-contract.md +76 -101
- package/assets/skills/explainer-kit/references/recap-authoring.md +99 -0
- package/assets/skills/explainer-kit/schemas/manifest.schema.json +13 -135
- package/assets/skills/explainer-kit/scripts/bundle.mjs +712 -0
- package/assets/skills/explainer-kit/scripts/lib/claim-subject.mjs +40 -0
- package/assets/skills/explainer-kit/scripts/lib/contracts.mjs +113 -1504
- package/assets/skills/explainer-kit/scripts/lib/failure.mjs +13 -0
- package/assets/skills/explainer-kit/scripts/lib/package-coverage.mjs +57 -403
- package/assets/skills/explainer-kit/scripts/lib/qa-result.mjs +168 -0
- package/assets/skills/explainer-kit/scripts/lib/qa.mjs +90 -488
- package/assets/skills/explainer-kit/scripts/lib/recipes.mjs +28 -545
- package/assets/skills/explainer-kit/scripts/lib/run-package.mjs +120 -0
- package/assets/skills/explainer-kit/scripts/lib/sanitize.mjs +42 -0
- package/assets/skills/explainer-kit/scripts/record.mjs +335 -0
- package/assets/skills/explainer-kit/scripts/verify.mjs +648 -0
- package/assets/skills/oat-explainer-kit/SKILL.md +57 -117
- package/assets/skills/oat-explainer-kit/references/config-contract.md +20 -64
- package/assets/skills/oat-explainer-kit/references/lifecycle-contract.md +85 -237
- package/assets/skills/oat-explainer-kit/scripts/bind-project-sources.mjs +18 -10
- package/assets/skills/oat-explainer-kit/scripts/check-core.mjs +1 -0
- package/assets/skills/oat-explainer-kit/scripts/check-terminal-outcome.mjs +256 -21
- package/assets/skills/oat-explainer-kit/scripts/persist-intent.mjs +82 -3
- package/assets/skills/oat-explainer-kit/scripts/resolve-config.mjs +0 -162
- package/assets/skills/oat-explainer-kit/scripts/resolve-intent.mjs +57 -123
- package/assets/skills/oat-project-autonomous/SKILL.md +14 -13
- package/assets/skills/oat-project-autonomous/references/gate-inventory.md +165 -165
- package/assets/skills/oat-project-complete/SKILL.md +168 -349
- package/assets/skills/oat-project-complete/scripts/consume-persisted-recap-intent.mjs +125 -0
- package/assets/skills/oat-project-complete/scripts/execute-synced-archive-entry.mjs +0 -30
- package/assets/skills/oat-project-complete/scripts/parse-completion-retry-fields.mjs +4 -37
- package/assets/skills/oat-project-complete/scripts/parse-synced-archive-resume-fields.mjs +17 -16
- package/assets/skills/oat-project-complete/scripts/recover-completion-receipts.mjs +9 -174
- package/assets/skills/oat-project-complete/scripts/resolve-completion-retry.mjs +1 -2
- package/assets/skills/oat-project-document/SKILL.md +1 -1
- package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +165 -165
- package/assets/skills/oat-project-implement/SKILL.md +1 -1
- package/assets/skills/oat-project-implement/references/completion-and-closeout.md +44 -56
- package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +165 -165
- package/assets/skills/oat-project-lite/SKILL.md +1 -1
- package/assets/skills/oat-project-lite/references/docs/autonomy-contract.md +165 -165
- package/assets/skills/oat-project-plan/SKILL.md +5 -6
- package/assets/skills/oat-project-pr-final/SKILL.md +1 -1
- package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +165 -165
- package/assets/skills/oat-project-quick-start/SKILL.md +1 -1
- package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +165 -165
- package/assets/skills/oat-project-summary/SKILL.md +35 -25
- package/assets/skills/oat-wave-execute/SKILL.md +11 -36
- package/assets/skills/oat-wave-program/SKILL.md +13 -37
- package/dist/commands/config/index.d.ts.map +1 -1
- package/dist/commands/config/index.js +9 -113
- package/dist/commands/project/archive/archive-utils.d.ts.map +1 -1
- package/dist/commands/project/archive/archive-utils.js +32 -184
- package/dist/commands/project/archive/explainer-package-coverage.d.ts +3 -8
- package/dist/commands/project/archive/explainer-package-coverage.d.ts.map +1 -1
- package/dist/commands/project/archive/explainer-package-coverage.js +1 -1
- package/dist/config/oat-config.d.ts +0 -11
- package/dist/config/oat-config.d.ts.map +1 -1
- package/dist/config/oat-config.js +0 -34
- package/dist/config/resolve.d.ts.map +1 -1
- package/dist/config/resolve.js +0 -8
- package/dist/release/public-package-contract.d.ts.map +1 -1
- package/dist/release/public-package-contract.js +2 -10
- package/dist/validation/project-state.d.ts.map +1 -1
- package/dist/validation/project-state.js +42 -3
- package/package.json +2 -2
- package/assets/docs/contributing/explainer-kit-verification.md +0 -133
- package/assets/docs/workflows/skills/explainer-kit-providers.md +0 -155
- package/assets/skills/explainer-kit/briefs/deep-dive.md +0 -35
- package/assets/skills/explainer-kit/briefs/project-page.md +0 -38
- package/assets/skills/explainer-kit/briefs/supporting-diagram.md +0 -28
- package/assets/skills/explainer-kit/briefs/walkthrough-deck.md +0 -35
- package/assets/skills/explainer-kit/examples/project-explainer/content.md +0 -31
- package/assets/skills/explainer-kit/examples/project-explainer/fact-base.md +0 -22
- package/assets/skills/explainer-kit/examples/project-recap/content.md +0 -77
- package/assets/skills/explainer-kit/examples/project-recap/fact-base.json +0 -104
- package/assets/skills/explainer-kit/examples/project-recap/fact-base.md +0 -44
- package/assets/skills/explainer-kit/examples/theme-bundle.json +0 -87
- package/assets/skills/explainer-kit/recipes/project-recap.v1.json +0 -93
- package/assets/skills/explainer-kit/references/contracts.md +0 -324
- package/assets/skills/explainer-kit/references/destination-contract.md +0 -210
- package/assets/skills/explainer-kit/references/extension-contract.md +0 -80
- package/assets/skills/explainer-kit/references/golden-conformance.md +0 -80
- package/assets/skills/explainer-kit/references/visual-review.md +0 -71
- package/assets/skills/explainer-kit/schemas/author-request.v2.schema.json +0 -212
- package/assets/skills/explainer-kit/schemas/author-request.v3.schema.json +0 -241
- package/assets/skills/explainer-kit/schemas/author-result.v2.schema.json +0 -52
- package/assets/skills/explainer-kit/schemas/build-record.schema.json +0 -98
- package/assets/skills/explainer-kit/schemas/durability-evidence.schema.json +0 -53
- package/assets/skills/explainer-kit/schemas/publish-receipt.v1.schema.json +0 -97
- package/assets/skills/explainer-kit/schemas/publish-receipt.v2.schema.json +0 -215
- package/assets/skills/explainer-kit/schemas/publish-request.v1.schema.json +0 -32
- package/assets/skills/explainer-kit/schemas/publish-request.v2.schema.json +0 -34
- package/assets/skills/explainer-kit/schemas/run-request.schema.json +0 -147
- package/assets/skills/explainer-kit/schemas/set-plan.v1.schema.json +0 -149
- package/assets/skills/explainer-kit/schemas/terminal-evidence.v1.schema.json +0 -81
- package/assets/skills/explainer-kit/schemas/visual-review-evidence.v1.schema.json +0 -66
- package/assets/skills/explainer-kit/schemas/visual-review-request.v1.schema.json +0 -117
- package/assets/skills/explainer-kit/schemas/visual-review-result.v1.schema.json +0 -80
- package/assets/skills/explainer-kit/scripts/lib/catalog.mjs +0 -349
- package/assets/skills/explainer-kit/scripts/lib/content-approval.mjs +0 -418
- package/assets/skills/explainer-kit/scripts/lib/diagram.mjs +0 -514
- package/assets/skills/explainer-kit/scripts/lib/durability.mjs +0 -643
- package/assets/skills/explainer-kit/scripts/lib/fact-base.mjs +0 -664
- package/assets/skills/explainer-kit/scripts/lib/internal-references.mjs +0 -538
- package/assets/skills/explainer-kit/scripts/lib/markdown.mjs +0 -414
- package/assets/skills/explainer-kit/scripts/lib/publication-policy.mjs +0 -54
- package/assets/skills/explainer-kit/scripts/lib/records.mjs +0 -954
- package/assets/skills/explainer-kit/scripts/lib/render.mjs +0 -703
- package/assets/skills/explainer-kit/scripts/lib/s3-roots.mjs +0 -353
- package/assets/skills/explainer-kit/scripts/lib/s3-static.mjs +0 -759
- package/assets/skills/explainer-kit/scripts/lib/set-plan.mjs +0 -209
- package/assets/skills/explainer-kit/scripts/lib/source-backlinks.mjs +0 -218
- package/assets/skills/explainer-kit/scripts/lib/terminal-evidence.mjs +0 -157
- package/assets/skills/explainer-kit/scripts/lib/visual-review.mjs +0 -400
- package/assets/skills/explainer-kit/scripts/publish.mjs +0 -76
- package/assets/skills/explainer-kit/scripts/record-durability.mjs +0 -64
- package/assets/skills/explainer-kit/scripts/render-qa.mjs +0 -461
- package/assets/skills/explainer-kit/scripts/run.mjs +0 -3038
- package/assets/skills/explainer-kit/scripts/validate.mjs +0 -59
- package/assets/skills/oat-explainer-kit/references/author-callback.md +0 -53
- package/assets/skills/oat-explainer-kit/references/migration.md +0 -144
- package/assets/skills/oat-explainer-kit/references/visual-review-callback.md +0 -83
- package/assets/skills/oat-explainer-kit/scripts/derive-destination.mjs +0 -91
- package/assets/skills/oat-explainer-kit/scripts/finalize-tracked-run.mjs +0 -531
- package/assets/skills/oat-explainer-kit/scripts/probe-recap-seams.mjs +0 -371
- package/assets/skills/oat-explainer-kit/scripts/run.mjs +0 -905
- package/dist/commands/project/archive/explainer-source-backlinks.d.ts +0 -18
- package/dist/commands/project/archive/explainer-source-backlinks.d.ts.map +0 -1
- package/dist/commands/project/archive/explainer-source-backlinks.js +0 -27
- package/dist/commands/project/archive/explainer-terminal-evidence.d.ts +0 -29
- package/dist/commands/project/archive/explainer-terminal-evidence.d.ts.map +0 -1
- package/dist/commands/project/archive/explainer-terminal-evidence.js +0 -37
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
import { canonicalHash, validateContract } from './contracts.mjs';
|
|
2
|
-
import { recipeExpansion, recipeFloor } from './recipes.mjs';
|
|
3
|
-
|
|
4
|
-
export async function planExplainerSet({
|
|
5
|
-
recipe,
|
|
6
|
-
factBase,
|
|
7
|
-
discovery,
|
|
8
|
-
planSet,
|
|
9
|
-
}) {
|
|
10
|
-
const sourceIds = factBase.sources
|
|
11
|
-
.map(({ id }) => id)
|
|
12
|
-
.filter((id) => !id.startsWith('critic:'));
|
|
13
|
-
const request = deepFreeze({
|
|
14
|
-
schemaVersion: 'explainer-kit.set-plan-request/v1',
|
|
15
|
-
recipe: structuredClone(recipe),
|
|
16
|
-
factBase: structuredClone(factBase),
|
|
17
|
-
sourceIds: structuredClone(sourceIds),
|
|
18
|
-
discovery: structuredClone(discovery),
|
|
19
|
-
});
|
|
20
|
-
const callback =
|
|
21
|
-
typeof planSet === 'function' ? planSet : createBaselineSetPlan;
|
|
22
|
-
const candidate = await callback(structuredClone(request));
|
|
23
|
-
const validation = validateContract('set-plan', candidate);
|
|
24
|
-
if (!validation.valid) {
|
|
25
|
-
throw setPlanError(
|
|
26
|
-
validation.errors
|
|
27
|
-
.map(({ path, code, message }) => `${path} [${code}]: ${message}`)
|
|
28
|
-
.join('; '),
|
|
29
|
-
);
|
|
30
|
-
}
|
|
31
|
-
validateAgainstInputs(candidate, { recipe, sourceIds });
|
|
32
|
-
|
|
33
|
-
const plan = deepFreeze(structuredClone(candidate));
|
|
34
|
-
const retainedRequest = deepFreeze({
|
|
35
|
-
schemaVersion: request.schemaVersion,
|
|
36
|
-
recipe: { id: recipe.id, version: recipe.version },
|
|
37
|
-
factBaseHash: canonicalHash(factBase),
|
|
38
|
-
sourceIds,
|
|
39
|
-
discovery: structuredClone(discovery),
|
|
40
|
-
});
|
|
41
|
-
return { request: retainedRequest, plan };
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function plannedArtifacts(recipe, plan) {
|
|
45
|
-
const floor = new Map(
|
|
46
|
-
recipeFloor(recipe).map((artifact) => [artifact.id, artifact]),
|
|
47
|
-
);
|
|
48
|
-
const profiles = new Map(
|
|
49
|
-
recipeExpansion(recipe).profiles.map((profile) => [
|
|
50
|
-
profile.profileId,
|
|
51
|
-
profile,
|
|
52
|
-
]),
|
|
53
|
-
);
|
|
54
|
-
return plan.portfolio.map((planned) => {
|
|
55
|
-
const floorArtifact = floor.get(planned.artifactId);
|
|
56
|
-
if (floorArtifact) {
|
|
57
|
-
return {
|
|
58
|
-
...floorArtifact,
|
|
59
|
-
origin: 'floor',
|
|
60
|
-
shell:
|
|
61
|
-
floorArtifact.authoring === 'html'
|
|
62
|
-
? floorArtifact.template
|
|
63
|
-
: undefined,
|
|
64
|
-
plannedArtifact: planned,
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
const profile = profiles.get(planned.profileId);
|
|
68
|
-
return {
|
|
69
|
-
id: planned.artifactId,
|
|
70
|
-
type: profile.type,
|
|
71
|
-
authoring: profile.authoring,
|
|
72
|
-
briefRef: profile.briefRef,
|
|
73
|
-
shell: profile.shell,
|
|
74
|
-
template:
|
|
75
|
-
profile.authoring === 'markdown'
|
|
76
|
-
? templateForType(profile.type)
|
|
77
|
-
: profile.shell,
|
|
78
|
-
required: false,
|
|
79
|
-
origin: 'expansion',
|
|
80
|
-
profileId: profile.profileId,
|
|
81
|
-
plannedArtifact: planned,
|
|
82
|
-
};
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function createBaselineSetPlan({ recipe, factBase }) {
|
|
87
|
-
const sourceIds = factBase.sources
|
|
88
|
-
.map(({ id }) => id)
|
|
89
|
-
.filter((id) => !id.startsWith('critic:'));
|
|
90
|
-
return {
|
|
91
|
-
schemaVersion: 'explainer-kit.set-plan/v1',
|
|
92
|
-
planId: `${recipe.id}-set`,
|
|
93
|
-
recipe: { id: recipe.id, version: recipe.version },
|
|
94
|
-
sourceIds,
|
|
95
|
-
ledger: { terminology: [], statuses: [], numbers: [] },
|
|
96
|
-
portfolio: recipeFloor(recipe).map((artifact) => ({
|
|
97
|
-
artifactId: artifact.id,
|
|
98
|
-
artifactType: artifact.type,
|
|
99
|
-
profileId: 'recipe-floor',
|
|
100
|
-
required: true,
|
|
101
|
-
sourceIds,
|
|
102
|
-
draft: `Compose ${artifact.id} from the reconciled fact base.`,
|
|
103
|
-
visualIntent: 'Follow the bundled brief and selected medium.',
|
|
104
|
-
})),
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function validateAgainstInputs(plan, { recipe, sourceIds }) {
|
|
109
|
-
if (plan.recipe.id !== recipe.id || plan.recipe.version !== recipe.version) {
|
|
110
|
-
throw setPlanError('Set plan recipe does not match the active recipe.');
|
|
111
|
-
}
|
|
112
|
-
const availableSources = new Set(sourceIds);
|
|
113
|
-
for (const sourceId of plan.sourceIds) {
|
|
114
|
-
if (!availableSources.has(sourceId)) {
|
|
115
|
-
throw setPlanError(
|
|
116
|
-
`Set plan references unknown reconciled source ${sourceId}.`,
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
const plannedSources = new Set(plan.sourceIds);
|
|
121
|
-
const omittedSources = sourceIds.filter(
|
|
122
|
-
(sourceId) => !plannedSources.has(sourceId),
|
|
123
|
-
);
|
|
124
|
-
if (
|
|
125
|
-
omittedSources.length > 0 ||
|
|
126
|
-
plannedSources.size !== availableSources.size
|
|
127
|
-
) {
|
|
128
|
-
throw setPlanError(
|
|
129
|
-
`Set plan must declare every reconciled non-critic source; omitted ${omittedSources.join(', ') || 'none'}.`,
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
|
-
const coveredSources = new Set(
|
|
133
|
-
plan.portfolio.flatMap(({ sourceIds: artifactSourceIds }) =>
|
|
134
|
-
Array.isArray(artifactSourceIds) ? artifactSourceIds : [],
|
|
135
|
-
),
|
|
136
|
-
);
|
|
137
|
-
const uncoveredSources = plan.sourceIds.filter(
|
|
138
|
-
(sourceId) => !coveredSources.has(sourceId),
|
|
139
|
-
);
|
|
140
|
-
if (uncoveredSources.length > 0) {
|
|
141
|
-
throw setPlanError(
|
|
142
|
-
`Every declared source must cover at least one portfolio artifact; uncovered ${uncoveredSources.join(', ')}.`,
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const plannedById = new Map(
|
|
147
|
-
plan.portfolio.map((artifact) => [artifact.artifactId, artifact]),
|
|
148
|
-
);
|
|
149
|
-
for (const floorArtifact of recipeFloor(recipe)) {
|
|
150
|
-
const planned = plannedById.get(floorArtifact.id);
|
|
151
|
-
if (
|
|
152
|
-
!planned ||
|
|
153
|
-
planned.required !== true ||
|
|
154
|
-
planned.artifactType !== floorArtifact.type
|
|
155
|
-
) {
|
|
156
|
-
throw setPlanError(
|
|
157
|
-
`Set plan is missing required draft ${floorArtifact.id}.`,
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const floorIds = new Set(recipeFloor(recipe).map(({ id }) => id));
|
|
163
|
-
const profiles = new Map(
|
|
164
|
-
recipeExpansion(recipe).profiles.map((profile) => [
|
|
165
|
-
profile.profileId,
|
|
166
|
-
profile,
|
|
167
|
-
]),
|
|
168
|
-
);
|
|
169
|
-
for (const artifact of plan.portfolio) {
|
|
170
|
-
if (floorIds.has(artifact.artifactId)) continue;
|
|
171
|
-
const profile = profiles.get(artifact.profileId);
|
|
172
|
-
if (
|
|
173
|
-
!profile ||
|
|
174
|
-
artifact.required !== false ||
|
|
175
|
-
artifact.artifactType !== profile.type
|
|
176
|
-
) {
|
|
177
|
-
throw setPlanError(
|
|
178
|
-
`Optional artifact ${artifact.artifactId} does not match an allowed recipe profile.`,
|
|
179
|
-
);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function templateForType(type) {
|
|
185
|
-
return (
|
|
186
|
-
{
|
|
187
|
-
hub: 'house-style',
|
|
188
|
-
diagram: 'diagram-shell',
|
|
189
|
-
explainer: 'engineer-tour',
|
|
190
|
-
deck: 'deck-shell',
|
|
191
|
-
}[type] ?? 'house-style'
|
|
192
|
-
);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function deepFreeze(value) {
|
|
196
|
-
if (value && typeof value === 'object' && !Object.isFrozen(value)) {
|
|
197
|
-
Object.freeze(value);
|
|
198
|
-
for (const child of Object.values(value)) {
|
|
199
|
-
deepFreeze(child);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
return value;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function setPlanError(message) {
|
|
206
|
-
const error = new Error(`Invalid set plan: ${message}`);
|
|
207
|
-
error.code = 'E_SET_PLAN';
|
|
208
|
-
return error;
|
|
209
|
-
}
|
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
export const SOURCE_BACKLINK_CONTRACT_VERSION =
|
|
2
|
-
'explainer-kit.source-backlinks/v1';
|
|
3
|
-
|
|
4
|
-
const REPOSITORY_PATTERN = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
5
|
-
const REVISION_PATTERN = /^[a-f0-9]{40}$/;
|
|
6
|
-
const LINE_FRAGMENT_PATTERN = /^L([1-9][0-9]*)(?:-L([1-9][0-9]*))?$/;
|
|
7
|
-
|
|
8
|
-
export function canonicalGithubBlobBacklink(tuple) {
|
|
9
|
-
const normalized = normalizeBacklinkTuple(tuple);
|
|
10
|
-
const encodedPath = normalized.path
|
|
11
|
-
.split('/')
|
|
12
|
-
.map((segment) => encodeURIComponent(segment))
|
|
13
|
-
.join('/');
|
|
14
|
-
const fragment =
|
|
15
|
-
normalized.lineRange.start === normalized.lineRange.end
|
|
16
|
-
? `L${normalized.lineRange.start}`
|
|
17
|
-
: `L${normalized.lineRange.start}-L${normalized.lineRange.end}`;
|
|
18
|
-
return `https://github.com/${normalized.repository}/blob/${normalized.revision}/${encodedPath}#${fragment}`;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function validateCanonicalGithubBlobTuple(tuple) {
|
|
22
|
-
try {
|
|
23
|
-
const normalized = normalizeBacklinkTuple(tuple);
|
|
24
|
-
if (
|
|
25
|
-
typeof tuple?.url !== 'string' ||
|
|
26
|
-
tuple.url !== canonicalGithubBlobBacklink(normalized)
|
|
27
|
-
) {
|
|
28
|
-
return false;
|
|
29
|
-
}
|
|
30
|
-
return sameTuple(parseCanonicalGithubBlobUrl(tuple.url), {
|
|
31
|
-
...normalized,
|
|
32
|
-
url: tuple.url,
|
|
33
|
-
});
|
|
34
|
-
} catch {
|
|
35
|
-
return false;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function parseCanonicalGithubBlobUrl(value) {
|
|
40
|
-
if (typeof value !== 'string') {
|
|
41
|
-
throw backlinkError('GitHub backlink URL must be a string.');
|
|
42
|
-
}
|
|
43
|
-
if (
|
|
44
|
-
!value.startsWith('https://github.com/') ||
|
|
45
|
-
value.includes('?') ||
|
|
46
|
-
value.includes('\\')
|
|
47
|
-
) {
|
|
48
|
-
throw backlinkError(
|
|
49
|
-
'GitHub backlink must use canonical credential-free HTTPS without query.',
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
const hashIndex = value.indexOf('#');
|
|
53
|
-
if (hashIndex < 0 || value.indexOf('#', hashIndex + 1) >= 0) {
|
|
54
|
-
throw backlinkError(
|
|
55
|
-
'GitHub backlink requires one canonical line fragment.',
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
const rawPath = value.slice('https://github.com/'.length, hashIndex);
|
|
59
|
-
const fragment = value.slice(hashIndex + 1);
|
|
60
|
-
const rawSegments = rawPath.split('/');
|
|
61
|
-
if (
|
|
62
|
-
rawSegments.length < 5 ||
|
|
63
|
-
rawSegments.some((segment) => segment.length === 0)
|
|
64
|
-
) {
|
|
65
|
-
throw backlinkError('GitHub backlink path contains an empty segment.');
|
|
66
|
-
}
|
|
67
|
-
const [owner, repositoryName, blob, revision, ...rawFileSegments] =
|
|
68
|
-
rawSegments;
|
|
69
|
-
if (
|
|
70
|
-
blob !== 'blob' ||
|
|
71
|
-
!REPOSITORY_PATTERN.test(`${owner}/${repositoryName}`) ||
|
|
72
|
-
!REVISION_PATTERN.test(revision)
|
|
73
|
-
) {
|
|
74
|
-
throw backlinkError(
|
|
75
|
-
'GitHub backlink requires a canonical repository and full commit SHA.',
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
const fileSegments = rawFileSegments.map(decodeCanonicalSegment);
|
|
79
|
-
const lineMatch = fragment.match(LINE_FRAGMENT_PATTERN);
|
|
80
|
-
if (!lineMatch) {
|
|
81
|
-
throw backlinkError('GitHub backlink requires a canonical line range.');
|
|
82
|
-
}
|
|
83
|
-
const start = Number(lineMatch[1]);
|
|
84
|
-
const end = Number(lineMatch[2] ?? lineMatch[1]);
|
|
85
|
-
if (end < start) {
|
|
86
|
-
throw backlinkError('GitHub backlink line range is reversed.');
|
|
87
|
-
}
|
|
88
|
-
const tuple = {
|
|
89
|
-
repository: `${owner}/${repositoryName}`,
|
|
90
|
-
revision,
|
|
91
|
-
path: fileSegments.join('/'),
|
|
92
|
-
lineRange: { start, end },
|
|
93
|
-
};
|
|
94
|
-
const canonical = canonicalGithubBlobBacklink(tuple);
|
|
95
|
-
let parsed;
|
|
96
|
-
try {
|
|
97
|
-
parsed = new URL(value);
|
|
98
|
-
} catch {
|
|
99
|
-
throw backlinkError('GitHub backlink URL is malformed.');
|
|
100
|
-
}
|
|
101
|
-
if (
|
|
102
|
-
parsed.protocol !== 'https:' ||
|
|
103
|
-
parsed.hostname !== 'github.com' ||
|
|
104
|
-
parsed.port ||
|
|
105
|
-
parsed.username ||
|
|
106
|
-
parsed.password ||
|
|
107
|
-
parsed.search ||
|
|
108
|
-
parsed.href !== canonical ||
|
|
109
|
-
value !== canonical
|
|
110
|
-
) {
|
|
111
|
-
throw backlinkError('GitHub backlink URL is not canonical.');
|
|
112
|
-
}
|
|
113
|
-
return { ...tuple, url: canonical };
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function normalizeBacklinkTuple(tuple) {
|
|
117
|
-
if (
|
|
118
|
-
!tuple ||
|
|
119
|
-
typeof tuple !== 'object' ||
|
|
120
|
-
!REPOSITORY_PATTERN.test(tuple.repository ?? '') ||
|
|
121
|
-
!REVISION_PATTERN.test(tuple.revision ?? '')
|
|
122
|
-
) {
|
|
123
|
-
throw backlinkError(
|
|
124
|
-
'GitHub backlink tuple requires a canonical repository and full commit SHA.',
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
const path = normalizeRepositoryPath(tuple.path);
|
|
128
|
-
const lineRange = normalizeLineRange(tuple.lineRange);
|
|
129
|
-
return {
|
|
130
|
-
repository: tuple.repository,
|
|
131
|
-
revision: tuple.revision,
|
|
132
|
-
path,
|
|
133
|
-
lineRange,
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function normalizeRepositoryPath(value) {
|
|
138
|
-
if (
|
|
139
|
-
typeof value !== 'string' ||
|
|
140
|
-
value.length === 0 ||
|
|
141
|
-
value.startsWith('/') ||
|
|
142
|
-
value.includes('\\')
|
|
143
|
-
) {
|
|
144
|
-
throw backlinkError(
|
|
145
|
-
'GitHub backlink tuple requires a repository-relative path.',
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
const segments = value.split('/');
|
|
149
|
-
if (
|
|
150
|
-
segments.some(
|
|
151
|
-
(segment) =>
|
|
152
|
-
segment.length === 0 ||
|
|
153
|
-
segment === '.' ||
|
|
154
|
-
segment === '..' ||
|
|
155
|
-
segment.includes('/') ||
|
|
156
|
-
segment.includes('\\') ||
|
|
157
|
-
segment.includes('\0'),
|
|
158
|
-
)
|
|
159
|
-
) {
|
|
160
|
-
throw backlinkError('GitHub backlink path contains an unsafe segment.');
|
|
161
|
-
}
|
|
162
|
-
return segments.join('/');
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function normalizeLineRange(value) {
|
|
166
|
-
if (
|
|
167
|
-
!value ||
|
|
168
|
-
!Number.isInteger(value.start) ||
|
|
169
|
-
!Number.isInteger(value.end) ||
|
|
170
|
-
value.start < 1 ||
|
|
171
|
-
value.end < value.start
|
|
172
|
-
) {
|
|
173
|
-
throw backlinkError(
|
|
174
|
-
'GitHub backlink tuple requires a valid inclusive line range.',
|
|
175
|
-
);
|
|
176
|
-
}
|
|
177
|
-
return { start: value.start, end: value.end };
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function decodeCanonicalSegment(rawSegment) {
|
|
181
|
-
let decoded;
|
|
182
|
-
try {
|
|
183
|
-
decoded = decodeURIComponent(rawSegment);
|
|
184
|
-
} catch {
|
|
185
|
-
throw backlinkError('GitHub backlink path has malformed encoding.');
|
|
186
|
-
}
|
|
187
|
-
if (
|
|
188
|
-
decoded.length === 0 ||
|
|
189
|
-
decoded === '.' ||
|
|
190
|
-
decoded === '..' ||
|
|
191
|
-
decoded.includes('/') ||
|
|
192
|
-
decoded.includes('\\') ||
|
|
193
|
-
decoded.includes('\0') ||
|
|
194
|
-
encodeURIComponent(decoded) !== rawSegment
|
|
195
|
-
) {
|
|
196
|
-
throw backlinkError(
|
|
197
|
-
'GitHub backlink path has an unsafe or noncanonical encoded segment.',
|
|
198
|
-
);
|
|
199
|
-
}
|
|
200
|
-
return decoded;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function sameTuple(left, right) {
|
|
204
|
-
return (
|
|
205
|
-
left.repository === right.repository &&
|
|
206
|
-
left.revision === right.revision &&
|
|
207
|
-
left.path === right.path &&
|
|
208
|
-
left.lineRange.start === right.lineRange.start &&
|
|
209
|
-
left.lineRange.end === right.lineRange.end &&
|
|
210
|
-
left.url === right.url
|
|
211
|
-
);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function backlinkError(message) {
|
|
215
|
-
const error = new TypeError(message);
|
|
216
|
-
error.code = 'E_SOURCE_BACKLINK';
|
|
217
|
-
return error;
|
|
218
|
-
}
|
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
import { lstat, readFile, realpath } from 'node:fs/promises';
|
|
3
|
-
import { resolve } from 'node:path';
|
|
4
|
-
|
|
5
|
-
import { canonicalHash, validateContract } from './contracts.mjs';
|
|
6
|
-
|
|
7
|
-
export const TERMINAL_EVIDENCE_VERSION = 'explainer-kit.terminal-evidence/v1';
|
|
8
|
-
export const VISUAL_REVIEW_EVIDENCE_VERSION =
|
|
9
|
-
'explainer-kit.visual-review-evidence/v1';
|
|
10
|
-
export const EVIDENCE_REASON_MAX_COUNT = 50;
|
|
11
|
-
|
|
12
|
-
export function createTerminalEvidence({
|
|
13
|
-
runId,
|
|
14
|
-
outcome,
|
|
15
|
-
manifest,
|
|
16
|
-
reasons,
|
|
17
|
-
evidenceDisposition,
|
|
18
|
-
supersededBy,
|
|
19
|
-
} = {}) {
|
|
20
|
-
const evidence = {
|
|
21
|
-
schemaVersion: TERMINAL_EVIDENCE_VERSION,
|
|
22
|
-
runId,
|
|
23
|
-
outcome,
|
|
24
|
-
...(manifest && { manifestHash: canonicalHash(manifest) }),
|
|
25
|
-
reasons: structuredClone(reasons),
|
|
26
|
-
evidenceDisposition,
|
|
27
|
-
...(supersededBy && {
|
|
28
|
-
supersededBy: structuredClone(supersededBy),
|
|
29
|
-
}),
|
|
30
|
-
};
|
|
31
|
-
assertTerminalEvidence(evidence, { manifest });
|
|
32
|
-
return evidence;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function assertTerminalEvidence(evidence, { manifest } = {}) {
|
|
36
|
-
const validation = validateContract('terminal-evidence', evidence, {
|
|
37
|
-
manifest,
|
|
38
|
-
});
|
|
39
|
-
if (!validation.valid) {
|
|
40
|
-
throw new Error(
|
|
41
|
-
`Invalid terminal evidence: ${validation.errors
|
|
42
|
-
.map(({ path, code }) => `${path} [${code}]`)
|
|
43
|
-
.join('; ')}`,
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
return evidence;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function createVisualReviewEvidence({
|
|
50
|
-
request,
|
|
51
|
-
attempt,
|
|
52
|
-
result,
|
|
53
|
-
failureKind,
|
|
54
|
-
} = {}) {
|
|
55
|
-
const evidence = {
|
|
56
|
-
schemaVersion: VISUAL_REVIEW_EVIDENCE_VERSION,
|
|
57
|
-
requestHash: request?.requestHash,
|
|
58
|
-
attempt,
|
|
59
|
-
disposition: failureKind
|
|
60
|
-
? 'failed'
|
|
61
|
-
: result?.disposition === 'pass'
|
|
62
|
-
? 'pass'
|
|
63
|
-
: 'correct',
|
|
64
|
-
reasons: failureKind
|
|
65
|
-
? [{ stage: 'visual-review', kind: failureKind, count: 1 }]
|
|
66
|
-
: result?.disposition === 'pass'
|
|
67
|
-
? []
|
|
68
|
-
: findingReasons(result?.findings),
|
|
69
|
-
};
|
|
70
|
-
assertVisualReviewEvidence(evidence, { request, attempt });
|
|
71
|
-
return evidence;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function assertVisualReviewEvidence(
|
|
75
|
-
evidence,
|
|
76
|
-
{ request, attempt } = {},
|
|
77
|
-
) {
|
|
78
|
-
const validation = validateContract('visual-review-evidence', evidence, {
|
|
79
|
-
visualReviewRequest: request,
|
|
80
|
-
attempt,
|
|
81
|
-
});
|
|
82
|
-
if (!validation.valid) {
|
|
83
|
-
throw new Error(
|
|
84
|
-
`Invalid retained visual evidence: ${validation.errors
|
|
85
|
-
.map(({ path, code }) => `${path} [${code}]`)
|
|
86
|
-
.join('; ')}`,
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
return evidence;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export function evidenceReason(stage, kind, { artifactId, count = 1 } = {}) {
|
|
93
|
-
return {
|
|
94
|
-
stage,
|
|
95
|
-
kind,
|
|
96
|
-
...(artifactId !== undefined && { artifactId }),
|
|
97
|
-
count,
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export function projectThrownReason(stage, kind = 'pipeline-failure') {
|
|
102
|
-
return evidenceReason(stage, kind);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export async function readTerminalEvidenceFile(
|
|
106
|
-
runRoot,
|
|
107
|
-
{ manifest, expectedBytes, expectedHash } = {},
|
|
108
|
-
) {
|
|
109
|
-
if (typeof runRoot !== 'string' || runRoot.length === 0) {
|
|
110
|
-
throw new TypeError('Terminal evidence run root must be a path string.');
|
|
111
|
-
}
|
|
112
|
-
const canonicalRunRoot = await realpath(runRoot);
|
|
113
|
-
const evidencePath = resolve(canonicalRunRoot, 'terminal-evidence.json');
|
|
114
|
-
const stats = await lstat(evidencePath);
|
|
115
|
-
if (stats.isSymbolicLink() || !stats.isFile()) {
|
|
116
|
-
throw new Error(
|
|
117
|
-
'Terminal evidence must be a regular file, not a symbolic link.',
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
|
-
const canonicalEvidencePath = await realpath(evidencePath);
|
|
121
|
-
if (canonicalEvidencePath !== evidencePath) {
|
|
122
|
-
throw new Error('Terminal evidence must remain within the run root.');
|
|
123
|
-
}
|
|
124
|
-
const bytes = await readFile(canonicalEvidencePath);
|
|
125
|
-
const hash = `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
|
|
126
|
-
if (expectedHash !== undefined && hash !== expectedHash) {
|
|
127
|
-
throw new Error('Terminal evidence bytes changed while staging.');
|
|
128
|
-
}
|
|
129
|
-
if (
|
|
130
|
-
expectedBytes !== undefined &&
|
|
131
|
-
!bytes.equals(Buffer.from(expectedBytes))
|
|
132
|
-
) {
|
|
133
|
-
throw new Error('Terminal evidence bytes changed while staging.');
|
|
134
|
-
}
|
|
135
|
-
let evidence;
|
|
136
|
-
try {
|
|
137
|
-
evidence = JSON.parse(bytes.toString('utf8'));
|
|
138
|
-
} catch {
|
|
139
|
-
throw new Error('Terminal evidence must contain valid JSON.');
|
|
140
|
-
}
|
|
141
|
-
assertTerminalEvidence(evidence, { manifest });
|
|
142
|
-
return { evidence, bytes, hash };
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function findingReasons(findings) {
|
|
146
|
-
const counts = new Map();
|
|
147
|
-
let total = 0;
|
|
148
|
-
for (const finding of Array.isArray(findings) ? findings : []) {
|
|
149
|
-
if (total >= EVIDENCE_REASON_MAX_COUNT) break;
|
|
150
|
-
const artifactId = finding?.artifactId;
|
|
151
|
-
counts.set(artifactId, (counts.get(artifactId) ?? 0) + 1);
|
|
152
|
-
total += 1;
|
|
153
|
-
}
|
|
154
|
-
return [...counts].map(([artifactId, count]) =>
|
|
155
|
-
evidenceReason('visual-review', 'finding', { artifactId, count }),
|
|
156
|
-
);
|
|
157
|
-
}
|