@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.
Files changed (149) hide show
  1. package/assets/bundle-metadata.json +1 -1
  2. package/assets/docs/cli-utilities/configuration.md +3 -56
  3. package/assets/docs/cli-utilities/tool-packs.md +4 -4
  4. package/assets/docs/contributing/code.md +2 -2
  5. package/assets/docs/contributing/index.md +0 -1
  6. package/assets/docs/reference/cli-reference.md +1 -1
  7. package/assets/docs/reference/troubleshooting.md +17 -40
  8. package/assets/docs/workflows/projects/artifacts.md +16 -34
  9. package/assets/docs/workflows/projects/lifecycle.md +20 -19
  10. package/assets/docs/workflows/skills/explainer-kit.md +180 -440
  11. package/assets/docs/workflows/skills/index.md +1 -2
  12. package/assets/public-package-versions.json +4 -4
  13. package/assets/skills/explainer-kit/SKILL.md +96 -171
  14. package/assets/skills/explainer-kit/recipes/engineer-tour.json +1 -22
  15. package/assets/skills/explainer-kit/recipes/program-recap.json +1 -29
  16. package/assets/skills/explainer-kit/recipes/project-explainer.json +1 -22
  17. package/assets/skills/explainer-kit/recipes/project-recap.v2.json +1 -43
  18. package/assets/skills/explainer-kit/references/fact-base-contract.md +76 -101
  19. package/assets/skills/explainer-kit/references/recap-authoring.md +99 -0
  20. package/assets/skills/explainer-kit/schemas/manifest.schema.json +13 -135
  21. package/assets/skills/explainer-kit/scripts/bundle.mjs +712 -0
  22. package/assets/skills/explainer-kit/scripts/lib/claim-subject.mjs +40 -0
  23. package/assets/skills/explainer-kit/scripts/lib/contracts.mjs +113 -1504
  24. package/assets/skills/explainer-kit/scripts/lib/failure.mjs +13 -0
  25. package/assets/skills/explainer-kit/scripts/lib/package-coverage.mjs +57 -403
  26. package/assets/skills/explainer-kit/scripts/lib/qa-result.mjs +168 -0
  27. package/assets/skills/explainer-kit/scripts/lib/qa.mjs +90 -488
  28. package/assets/skills/explainer-kit/scripts/lib/recipes.mjs +28 -545
  29. package/assets/skills/explainer-kit/scripts/lib/run-package.mjs +120 -0
  30. package/assets/skills/explainer-kit/scripts/lib/sanitize.mjs +42 -0
  31. package/assets/skills/explainer-kit/scripts/record.mjs +335 -0
  32. package/assets/skills/explainer-kit/scripts/verify.mjs +648 -0
  33. package/assets/skills/oat-explainer-kit/SKILL.md +57 -117
  34. package/assets/skills/oat-explainer-kit/references/config-contract.md +20 -64
  35. package/assets/skills/oat-explainer-kit/references/lifecycle-contract.md +85 -237
  36. package/assets/skills/oat-explainer-kit/scripts/bind-project-sources.mjs +18 -10
  37. package/assets/skills/oat-explainer-kit/scripts/check-core.mjs +1 -0
  38. package/assets/skills/oat-explainer-kit/scripts/check-terminal-outcome.mjs +256 -21
  39. package/assets/skills/oat-explainer-kit/scripts/persist-intent.mjs +82 -3
  40. package/assets/skills/oat-explainer-kit/scripts/resolve-config.mjs +0 -162
  41. package/assets/skills/oat-explainer-kit/scripts/resolve-intent.mjs +57 -123
  42. package/assets/skills/oat-project-autonomous/SKILL.md +14 -13
  43. package/assets/skills/oat-project-autonomous/references/gate-inventory.md +165 -165
  44. package/assets/skills/oat-project-complete/SKILL.md +168 -349
  45. package/assets/skills/oat-project-complete/scripts/consume-persisted-recap-intent.mjs +125 -0
  46. package/assets/skills/oat-project-complete/scripts/execute-synced-archive-entry.mjs +0 -30
  47. package/assets/skills/oat-project-complete/scripts/parse-completion-retry-fields.mjs +4 -37
  48. package/assets/skills/oat-project-complete/scripts/parse-synced-archive-resume-fields.mjs +17 -16
  49. package/assets/skills/oat-project-complete/scripts/recover-completion-receipts.mjs +9 -174
  50. package/assets/skills/oat-project-complete/scripts/resolve-completion-retry.mjs +1 -2
  51. package/assets/skills/oat-project-document/SKILL.md +1 -1
  52. package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +165 -165
  53. package/assets/skills/oat-project-implement/SKILL.md +1 -1
  54. package/assets/skills/oat-project-implement/references/completion-and-closeout.md +44 -56
  55. package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +165 -165
  56. package/assets/skills/oat-project-lite/SKILL.md +1 -1
  57. package/assets/skills/oat-project-lite/references/docs/autonomy-contract.md +165 -165
  58. package/assets/skills/oat-project-plan/SKILL.md +5 -6
  59. package/assets/skills/oat-project-pr-final/SKILL.md +1 -1
  60. package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +165 -165
  61. package/assets/skills/oat-project-quick-start/SKILL.md +1 -1
  62. package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +165 -165
  63. package/assets/skills/oat-project-summary/SKILL.md +35 -25
  64. package/assets/skills/oat-wave-execute/SKILL.md +11 -36
  65. package/assets/skills/oat-wave-program/SKILL.md +13 -37
  66. package/dist/commands/config/index.d.ts.map +1 -1
  67. package/dist/commands/config/index.js +9 -113
  68. package/dist/commands/project/archive/archive-utils.d.ts.map +1 -1
  69. package/dist/commands/project/archive/archive-utils.js +32 -184
  70. package/dist/commands/project/archive/explainer-package-coverage.d.ts +3 -8
  71. package/dist/commands/project/archive/explainer-package-coverage.d.ts.map +1 -1
  72. package/dist/commands/project/archive/explainer-package-coverage.js +1 -1
  73. package/dist/config/oat-config.d.ts +0 -11
  74. package/dist/config/oat-config.d.ts.map +1 -1
  75. package/dist/config/oat-config.js +0 -34
  76. package/dist/config/resolve.d.ts.map +1 -1
  77. package/dist/config/resolve.js +0 -8
  78. package/dist/release/public-package-contract.d.ts.map +1 -1
  79. package/dist/release/public-package-contract.js +2 -10
  80. package/dist/validation/project-state.d.ts.map +1 -1
  81. package/dist/validation/project-state.js +42 -3
  82. package/package.json +2 -2
  83. package/assets/docs/contributing/explainer-kit-verification.md +0 -133
  84. package/assets/docs/workflows/skills/explainer-kit-providers.md +0 -155
  85. package/assets/skills/explainer-kit/briefs/deep-dive.md +0 -35
  86. package/assets/skills/explainer-kit/briefs/project-page.md +0 -38
  87. package/assets/skills/explainer-kit/briefs/supporting-diagram.md +0 -28
  88. package/assets/skills/explainer-kit/briefs/walkthrough-deck.md +0 -35
  89. package/assets/skills/explainer-kit/examples/project-explainer/content.md +0 -31
  90. package/assets/skills/explainer-kit/examples/project-explainer/fact-base.md +0 -22
  91. package/assets/skills/explainer-kit/examples/project-recap/content.md +0 -77
  92. package/assets/skills/explainer-kit/examples/project-recap/fact-base.json +0 -104
  93. package/assets/skills/explainer-kit/examples/project-recap/fact-base.md +0 -44
  94. package/assets/skills/explainer-kit/examples/theme-bundle.json +0 -87
  95. package/assets/skills/explainer-kit/recipes/project-recap.v1.json +0 -93
  96. package/assets/skills/explainer-kit/references/contracts.md +0 -324
  97. package/assets/skills/explainer-kit/references/destination-contract.md +0 -210
  98. package/assets/skills/explainer-kit/references/extension-contract.md +0 -80
  99. package/assets/skills/explainer-kit/references/golden-conformance.md +0 -80
  100. package/assets/skills/explainer-kit/references/visual-review.md +0 -71
  101. package/assets/skills/explainer-kit/schemas/author-request.v2.schema.json +0 -212
  102. package/assets/skills/explainer-kit/schemas/author-request.v3.schema.json +0 -241
  103. package/assets/skills/explainer-kit/schemas/author-result.v2.schema.json +0 -52
  104. package/assets/skills/explainer-kit/schemas/build-record.schema.json +0 -98
  105. package/assets/skills/explainer-kit/schemas/durability-evidence.schema.json +0 -53
  106. package/assets/skills/explainer-kit/schemas/publish-receipt.v1.schema.json +0 -97
  107. package/assets/skills/explainer-kit/schemas/publish-receipt.v2.schema.json +0 -215
  108. package/assets/skills/explainer-kit/schemas/publish-request.v1.schema.json +0 -32
  109. package/assets/skills/explainer-kit/schemas/publish-request.v2.schema.json +0 -34
  110. package/assets/skills/explainer-kit/schemas/run-request.schema.json +0 -147
  111. package/assets/skills/explainer-kit/schemas/set-plan.v1.schema.json +0 -149
  112. package/assets/skills/explainer-kit/schemas/terminal-evidence.v1.schema.json +0 -81
  113. package/assets/skills/explainer-kit/schemas/visual-review-evidence.v1.schema.json +0 -66
  114. package/assets/skills/explainer-kit/schemas/visual-review-request.v1.schema.json +0 -117
  115. package/assets/skills/explainer-kit/schemas/visual-review-result.v1.schema.json +0 -80
  116. package/assets/skills/explainer-kit/scripts/lib/catalog.mjs +0 -349
  117. package/assets/skills/explainer-kit/scripts/lib/content-approval.mjs +0 -418
  118. package/assets/skills/explainer-kit/scripts/lib/diagram.mjs +0 -514
  119. package/assets/skills/explainer-kit/scripts/lib/durability.mjs +0 -643
  120. package/assets/skills/explainer-kit/scripts/lib/fact-base.mjs +0 -664
  121. package/assets/skills/explainer-kit/scripts/lib/internal-references.mjs +0 -538
  122. package/assets/skills/explainer-kit/scripts/lib/markdown.mjs +0 -414
  123. package/assets/skills/explainer-kit/scripts/lib/publication-policy.mjs +0 -54
  124. package/assets/skills/explainer-kit/scripts/lib/records.mjs +0 -954
  125. package/assets/skills/explainer-kit/scripts/lib/render.mjs +0 -703
  126. package/assets/skills/explainer-kit/scripts/lib/s3-roots.mjs +0 -353
  127. package/assets/skills/explainer-kit/scripts/lib/s3-static.mjs +0 -759
  128. package/assets/skills/explainer-kit/scripts/lib/set-plan.mjs +0 -209
  129. package/assets/skills/explainer-kit/scripts/lib/source-backlinks.mjs +0 -218
  130. package/assets/skills/explainer-kit/scripts/lib/terminal-evidence.mjs +0 -157
  131. package/assets/skills/explainer-kit/scripts/lib/visual-review.mjs +0 -400
  132. package/assets/skills/explainer-kit/scripts/publish.mjs +0 -76
  133. package/assets/skills/explainer-kit/scripts/record-durability.mjs +0 -64
  134. package/assets/skills/explainer-kit/scripts/render-qa.mjs +0 -461
  135. package/assets/skills/explainer-kit/scripts/run.mjs +0 -3038
  136. package/assets/skills/explainer-kit/scripts/validate.mjs +0 -59
  137. package/assets/skills/oat-explainer-kit/references/author-callback.md +0 -53
  138. package/assets/skills/oat-explainer-kit/references/migration.md +0 -144
  139. package/assets/skills/oat-explainer-kit/references/visual-review-callback.md +0 -83
  140. package/assets/skills/oat-explainer-kit/scripts/derive-destination.mjs +0 -91
  141. package/assets/skills/oat-explainer-kit/scripts/finalize-tracked-run.mjs +0 -531
  142. package/assets/skills/oat-explainer-kit/scripts/probe-recap-seams.mjs +0 -371
  143. package/assets/skills/oat-explainer-kit/scripts/run.mjs +0 -905
  144. package/dist/commands/project/archive/explainer-source-backlinks.d.ts +0 -18
  145. package/dist/commands/project/archive/explainer-source-backlinks.d.ts.map +0 -1
  146. package/dist/commands/project/archive/explainer-source-backlinks.js +0 -27
  147. package/dist/commands/project/archive/explainer-terminal-evidence.d.ts +0 -29
  148. package/dist/commands/project/archive/explainer-terminal-evidence.d.ts.map +0 -1
  149. 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
- }