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