@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.
- package/assets/agents/oat-reviewer.md +4 -3
- package/assets/agents/recon-worker.md +42 -8
- 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/docs/workflows/skills/recon.md +114 -23
- 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-review-receive/SKILL.md +14 -1
- 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/assets/skills/recon/SKILL.md +103 -40
- package/assets/skills/recon/references/packet-contract.md +150 -67
- package/assets/skills/recon/references/profiles.md +38 -16
- package/assets/skills/recon/references/worker-contract.md +39 -8
- package/assets/skills/recon/scripts/lib/contracts.mjs +703 -55
- package/assets/skills/recon/scripts/lib/routing.mjs +588 -0
- package/assets/skills/recon/scripts/lib/validated-run.mjs +2 -0
- package/assets/skills/recon/scripts/prepare-routing.mjs +88 -0
- package/assets/skills/recon/scripts/render-packet.mjs +69 -1
- package/assets/skills/recon/scripts/validate-packet.mjs +473 -56
- package/assets/skills/subagent-orchestration/SKILL.md +18 -9
- package/assets/skills/subagent-orchestration/references/model-selection-principles.md +17 -8
- 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,538 +0,0 @@
|
|
|
1
|
-
const MAX_HTML_BYTES = 1_000_000;
|
|
2
|
-
const MAX_ELEMENTS = 10_000;
|
|
3
|
-
const MAX_REFERENCES = 20_000;
|
|
4
|
-
const MAX_ERRORS = 100;
|
|
5
|
-
const RAW_TEXT_ELEMENTS = new Set(['script', 'style']);
|
|
6
|
-
const REFERENCE_ATTRIBUTES = new Set([
|
|
7
|
-
'href',
|
|
8
|
-
'poster',
|
|
9
|
-
'src',
|
|
10
|
-
'srcset',
|
|
11
|
-
'xlink:href',
|
|
12
|
-
]);
|
|
13
|
-
const SAFE_DATA_REFERENCE =
|
|
14
|
-
/^data:image\/(?:gif|jpeg|png|webp);base64,[a-z0-9+/]+={0,2}$/i;
|
|
15
|
-
|
|
16
|
-
export function validateInternalReferences({ artifacts, manifestPaths } = {}) {
|
|
17
|
-
const errors = [];
|
|
18
|
-
const add = (artifactId, code, message, detail = {}) => {
|
|
19
|
-
if (errors.length >= MAX_ERRORS) return;
|
|
20
|
-
errors.push({ artifactId, code, message, ...detail });
|
|
21
|
-
};
|
|
22
|
-
if (!Array.isArray(artifacts) || !Array.isArray(manifestPaths)) {
|
|
23
|
-
add(
|
|
24
|
-
null,
|
|
25
|
-
'invalid-site-tree',
|
|
26
|
-
'Internal-reference validation requires artifacts and manifest paths.',
|
|
27
|
-
);
|
|
28
|
-
return { valid: false, errors };
|
|
29
|
-
}
|
|
30
|
-
const declaredPaths = new Set(manifestPaths);
|
|
31
|
-
if (
|
|
32
|
-
declaredPaths.size !== manifestPaths.length ||
|
|
33
|
-
manifestPaths.some((path) => !isCanonicalSitePath(path))
|
|
34
|
-
) {
|
|
35
|
-
add(
|
|
36
|
-
null,
|
|
37
|
-
'invalid-site-tree',
|
|
38
|
-
'Manifest paths must be unique canonical site-relative files.',
|
|
39
|
-
);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const idsByPath = new Map();
|
|
43
|
-
const tokenized = [];
|
|
44
|
-
const boundPaths = new Set();
|
|
45
|
-
let referenceCount = 0;
|
|
46
|
-
for (const artifact of artifacts) {
|
|
47
|
-
if (
|
|
48
|
-
!isObject(artifact) ||
|
|
49
|
-
typeof artifact.artifactId !== 'string' ||
|
|
50
|
-
typeof artifact.renderedPath !== 'string' ||
|
|
51
|
-
typeof artifact.html !== 'string' ||
|
|
52
|
-
!declaredPaths.has(artifact.renderedPath)
|
|
53
|
-
) {
|
|
54
|
-
add(
|
|
55
|
-
artifact?.artifactId ?? null,
|
|
56
|
-
'invalid-site-tree',
|
|
57
|
-
'Every rendered artifact must bind to one manifest-declared site path.',
|
|
58
|
-
);
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
61
|
-
if (boundPaths.has(artifact.renderedPath)) {
|
|
62
|
-
add(
|
|
63
|
-
artifact.artifactId,
|
|
64
|
-
'invalid-site-tree',
|
|
65
|
-
'Rendered artifact paths must be unique.',
|
|
66
|
-
{ renderedPath: artifact.renderedPath },
|
|
67
|
-
);
|
|
68
|
-
continue;
|
|
69
|
-
}
|
|
70
|
-
boundPaths.add(artifact.renderedPath);
|
|
71
|
-
if (Buffer.byteLength(artifact.html, 'utf8') > MAX_HTML_BYTES) {
|
|
72
|
-
add(
|
|
73
|
-
artifact.artifactId,
|
|
74
|
-
'validation-bound',
|
|
75
|
-
`Rendered artifact exceeds the ${MAX_HTML_BYTES}-byte validation bound.`,
|
|
76
|
-
);
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
const parsed = tokenizeBoundedHtml(artifact.html);
|
|
80
|
-
if (!parsed.valid) {
|
|
81
|
-
add(artifact.artifactId, parsed.code, parsed.message, {
|
|
82
|
-
renderedPath: artifact.renderedPath,
|
|
83
|
-
});
|
|
84
|
-
continue;
|
|
85
|
-
}
|
|
86
|
-
const idCounts = new Map();
|
|
87
|
-
for (const element of parsed.elements) {
|
|
88
|
-
for (const attribute of element.attributes) {
|
|
89
|
-
if (attribute.name === 'id') {
|
|
90
|
-
idCounts.set(
|
|
91
|
-
attribute.value,
|
|
92
|
-
(idCounts.get(attribute.value) ?? 0) + 1,
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
if (REFERENCE_ATTRIBUTES.has(attribute.name)) {
|
|
96
|
-
referenceCount += 1;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
idsByPath.set(artifact.renderedPath, idCounts);
|
|
101
|
-
tokenized.push({ artifact, elements: parsed.elements });
|
|
102
|
-
}
|
|
103
|
-
if (referenceCount > MAX_REFERENCES) {
|
|
104
|
-
add(
|
|
105
|
-
null,
|
|
106
|
-
'validation-bound',
|
|
107
|
-
`Rendered site exceeds the ${MAX_REFERENCES}-reference validation bound.`,
|
|
108
|
-
);
|
|
109
|
-
return { valid: false, errors };
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
for (const { artifact, elements } of tokenized) {
|
|
113
|
-
for (const element of elements) {
|
|
114
|
-
for (const attribute of element.attributes) {
|
|
115
|
-
if (!REFERENCE_ATTRIBUTES.has(attribute.name)) continue;
|
|
116
|
-
const references =
|
|
117
|
-
attribute.name === 'srcset'
|
|
118
|
-
? parseSrcset(attribute.value)
|
|
119
|
-
: { valid: true, values: [attribute.value] };
|
|
120
|
-
if (!references.valid) {
|
|
121
|
-
add(
|
|
122
|
-
artifact.artifactId,
|
|
123
|
-
'malformed-srcset',
|
|
124
|
-
'srcset must contain bounded URL candidates with valid descriptors.',
|
|
125
|
-
referenceDetail(artifact, element, attribute, attribute.value),
|
|
126
|
-
);
|
|
127
|
-
continue;
|
|
128
|
-
}
|
|
129
|
-
for (const reference of references.values) {
|
|
130
|
-
validateReference({
|
|
131
|
-
artifact,
|
|
132
|
-
element,
|
|
133
|
-
attribute,
|
|
134
|
-
reference,
|
|
135
|
-
declaredPaths,
|
|
136
|
-
idsByPath,
|
|
137
|
-
add,
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
return { valid: errors.length === 0, errors };
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function validateReference({
|
|
147
|
-
artifact,
|
|
148
|
-
element,
|
|
149
|
-
attribute,
|
|
150
|
-
reference,
|
|
151
|
-
declaredPaths,
|
|
152
|
-
idsByPath,
|
|
153
|
-
add,
|
|
154
|
-
}) {
|
|
155
|
-
const detail = referenceDetail(artifact, element, attribute, reference);
|
|
156
|
-
if (
|
|
157
|
-
typeof reference !== 'string' ||
|
|
158
|
-
reference.length === 0 ||
|
|
159
|
-
hasUnsafeRawReferenceBytes(reference)
|
|
160
|
-
) {
|
|
161
|
-
add(
|
|
162
|
-
artifact.artifactId,
|
|
163
|
-
'unsafe-reference',
|
|
164
|
-
'References must use a non-empty canonical raw form.',
|
|
165
|
-
detail,
|
|
166
|
-
);
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
const scheme = reference.match(/^([a-z][a-z0-9+.-]*):/i)?.[1]?.toLowerCase();
|
|
170
|
-
if (scheme === 'data') {
|
|
171
|
-
if (
|
|
172
|
-
!isResourceAttribute(element.name, attribute.name) ||
|
|
173
|
-
!SAFE_DATA_REFERENCE.test(reference)
|
|
174
|
-
) {
|
|
175
|
-
add(
|
|
176
|
-
artifact.artifactId,
|
|
177
|
-
'unsafe-scheme',
|
|
178
|
-
'Only bounded base64 image data references are allowed inline.',
|
|
179
|
-
detail,
|
|
180
|
-
);
|
|
181
|
-
}
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
if (scheme) {
|
|
185
|
-
let external;
|
|
186
|
-
try {
|
|
187
|
-
external = new URL(reference);
|
|
188
|
-
} catch {
|
|
189
|
-
add(
|
|
190
|
-
artifact.artifactId,
|
|
191
|
-
'malformed-reference',
|
|
192
|
-
'Absolute reference is malformed.',
|
|
193
|
-
detail,
|
|
194
|
-
);
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
if (
|
|
198
|
-
scheme !== 'https' ||
|
|
199
|
-
external.username ||
|
|
200
|
-
external.password ||
|
|
201
|
-
(element.name !== 'a' && element.name !== 'area') ||
|
|
202
|
-
attribute.name !== 'href'
|
|
203
|
-
) {
|
|
204
|
-
add(
|
|
205
|
-
artifact.artifactId,
|
|
206
|
-
scheme === 'https' && isResourceAttribute(element.name, attribute.name)
|
|
207
|
-
? 'external-resource'
|
|
208
|
-
: 'unsafe-scheme',
|
|
209
|
-
'External resources and unsafe navigation schemes are not allowed.',
|
|
210
|
-
detail,
|
|
211
|
-
);
|
|
212
|
-
}
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
if (reference.startsWith('//')) {
|
|
216
|
-
add(
|
|
217
|
-
artifact.artifactId,
|
|
218
|
-
'unsafe-reference',
|
|
219
|
-
'Protocol-relative references are ambiguous.',
|
|
220
|
-
detail,
|
|
221
|
-
);
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
if (reference.includes('?')) {
|
|
225
|
-
add(
|
|
226
|
-
artifact.artifactId,
|
|
227
|
-
'query-reference',
|
|
228
|
-
'Internal references cannot contain queries.',
|
|
229
|
-
detail,
|
|
230
|
-
);
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
const [pathPart, rawFragment, ...extraFragments] = reference.split('#');
|
|
234
|
-
if (extraFragments.length > 0) {
|
|
235
|
-
add(
|
|
236
|
-
artifact.artifactId,
|
|
237
|
-
'malformed-reference',
|
|
238
|
-
'Internal references can contain at most one fragment delimiter.',
|
|
239
|
-
detail,
|
|
240
|
-
);
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
if (pathPart.endsWith('/')) {
|
|
244
|
-
add(
|
|
245
|
-
artifact.artifactId,
|
|
246
|
-
'directory-reference',
|
|
247
|
-
'Internal links must name an explicit file such as index.html.',
|
|
248
|
-
detail,
|
|
249
|
-
);
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
if (pathPart.startsWith('/')) {
|
|
253
|
-
add(
|
|
254
|
-
artifact.artifactId,
|
|
255
|
-
'site-root-escape',
|
|
256
|
-
'Internal references must be relative to the current artifact.',
|
|
257
|
-
detail,
|
|
258
|
-
);
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
const base = new URL(artifact.renderedPath, 'https://explainer.invalid/');
|
|
262
|
-
let resolved;
|
|
263
|
-
try {
|
|
264
|
-
resolved = new URL(reference, base);
|
|
265
|
-
} catch {
|
|
266
|
-
add(
|
|
267
|
-
artifact.artifactId,
|
|
268
|
-
'malformed-reference',
|
|
269
|
-
'Relative reference is malformed.',
|
|
270
|
-
detail,
|
|
271
|
-
);
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
if (
|
|
275
|
-
resolved.origin !== base.origin ||
|
|
276
|
-
resolved.protocol !== 'https:' ||
|
|
277
|
-
!resolved.pathname.startsWith('/site/')
|
|
278
|
-
) {
|
|
279
|
-
add(
|
|
280
|
-
artifact.artifactId,
|
|
281
|
-
'site-root-escape',
|
|
282
|
-
'Internal reference resolves outside the generated site root.',
|
|
283
|
-
detail,
|
|
284
|
-
);
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
const targetPath = resolved.pathname.slice(1);
|
|
288
|
-
if (!declaredPaths.has(targetPath)) {
|
|
289
|
-
add(
|
|
290
|
-
artifact.artifactId,
|
|
291
|
-
'missing-target',
|
|
292
|
-
'Internal reference does not resolve to a manifest-declared file.',
|
|
293
|
-
{ ...detail, targetPath },
|
|
294
|
-
);
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
if (rawFragment !== undefined) {
|
|
298
|
-
let fragment;
|
|
299
|
-
try {
|
|
300
|
-
fragment = decodeURIComponent(rawFragment);
|
|
301
|
-
} catch {
|
|
302
|
-
add(
|
|
303
|
-
artifact.artifactId,
|
|
304
|
-
'malformed-reference',
|
|
305
|
-
'Reference fragment is not valid percent-encoded text.',
|
|
306
|
-
detail,
|
|
307
|
-
);
|
|
308
|
-
return;
|
|
309
|
-
}
|
|
310
|
-
const targetCount = idsByPath.get(targetPath)?.get(fragment) ?? 0;
|
|
311
|
-
if (!fragment || targetCount === 0) {
|
|
312
|
-
add(
|
|
313
|
-
artifact.artifactId,
|
|
314
|
-
'missing-fragment',
|
|
315
|
-
'Internal reference fragment does not exist in the target document.',
|
|
316
|
-
{ ...detail, targetPath, fragment },
|
|
317
|
-
);
|
|
318
|
-
} else if (targetCount > 1) {
|
|
319
|
-
add(
|
|
320
|
-
artifact.artifactId,
|
|
321
|
-
'ambiguous-fragment',
|
|
322
|
-
'Internal reference fragment resolves to multiple targets in the target document.',
|
|
323
|
-
{ ...detail, targetPath, fragment },
|
|
324
|
-
);
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
function tokenizeBoundedHtml(html) {
|
|
330
|
-
const elements = [];
|
|
331
|
-
let cursor = 0;
|
|
332
|
-
while (cursor < html.length) {
|
|
333
|
-
const open = html.indexOf('<', cursor);
|
|
334
|
-
if (open < 0) break;
|
|
335
|
-
if (html.startsWith('<!--', open)) {
|
|
336
|
-
const close = html.indexOf('-->', open + 4);
|
|
337
|
-
if (close < 0) return malformedHtml();
|
|
338
|
-
cursor = close + 3;
|
|
339
|
-
continue;
|
|
340
|
-
}
|
|
341
|
-
const close = findTagEnd(html, open + 1);
|
|
342
|
-
if (close < 0) return malformedHtml();
|
|
343
|
-
const source = html.slice(open + 1, close);
|
|
344
|
-
if (/^\s*[!/]/.test(source)) {
|
|
345
|
-
cursor = close + 1;
|
|
346
|
-
continue;
|
|
347
|
-
}
|
|
348
|
-
const parsed = parseStartTag(source);
|
|
349
|
-
if (!parsed.valid) return malformedHtml();
|
|
350
|
-
elements.push(parsed.element);
|
|
351
|
-
if (elements.length > MAX_ELEMENTS) {
|
|
352
|
-
return {
|
|
353
|
-
valid: false,
|
|
354
|
-
code: 'validation-bound',
|
|
355
|
-
message: `Rendered artifact exceeds the ${MAX_ELEMENTS}-element validation bound.`,
|
|
356
|
-
};
|
|
357
|
-
}
|
|
358
|
-
cursor = close + 1;
|
|
359
|
-
if (RAW_TEXT_ELEMENTS.has(parsed.element.name)) {
|
|
360
|
-
const closing = indexOfCaseInsensitive(
|
|
361
|
-
html,
|
|
362
|
-
`</${parsed.element.name}`,
|
|
363
|
-
cursor,
|
|
364
|
-
);
|
|
365
|
-
if (closing < 0) return malformedHtml();
|
|
366
|
-
const closingEnd = findTagEnd(html, closing + 2);
|
|
367
|
-
if (closingEnd < 0) return malformedHtml();
|
|
368
|
-
cursor = closingEnd + 1;
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
return { valid: true, elements };
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
function findTagEnd(html, start) {
|
|
375
|
-
let quote = null;
|
|
376
|
-
for (let index = start; index < html.length; index += 1) {
|
|
377
|
-
const char = html[index];
|
|
378
|
-
if (quote) {
|
|
379
|
-
if (char === quote) quote = null;
|
|
380
|
-
} else if (char === '"' || char === "'") {
|
|
381
|
-
quote = char;
|
|
382
|
-
} else if (char === '>') {
|
|
383
|
-
return index;
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
return -1;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
function parseStartTag(source) {
|
|
390
|
-
const nameMatch = source.match(/^\s*([a-z][a-z0-9:-]*)/i);
|
|
391
|
-
if (!nameMatch) return { valid: false };
|
|
392
|
-
const name = nameMatch[1].toLowerCase();
|
|
393
|
-
const attributes = [];
|
|
394
|
-
let cursor = nameMatch[0].length;
|
|
395
|
-
while (cursor < source.length) {
|
|
396
|
-
while (/\s/.test(source[cursor] ?? '')) cursor += 1;
|
|
397
|
-
if (cursor >= source.length || source[cursor] === '/') break;
|
|
398
|
-
const attributeMatch = source
|
|
399
|
-
.slice(cursor)
|
|
400
|
-
.match(/^([^\s"'<>/=]+)(?:\s*=\s*)?/);
|
|
401
|
-
if (!attributeMatch) return { valid: false };
|
|
402
|
-
const attributeName = attributeMatch[1].toLowerCase();
|
|
403
|
-
cursor += attributeMatch[0].length;
|
|
404
|
-
const hasValue = attributeMatch[0].includes('=');
|
|
405
|
-
let value = '';
|
|
406
|
-
if (hasValue) {
|
|
407
|
-
const quote = source[cursor];
|
|
408
|
-
if (quote !== '"' && quote !== "'") return { valid: false };
|
|
409
|
-
const end = source.indexOf(quote, cursor + 1);
|
|
410
|
-
if (end < 0) return { valid: false };
|
|
411
|
-
value = decodeHtmlReferences(source.slice(cursor + 1, end));
|
|
412
|
-
cursor = end + 1;
|
|
413
|
-
}
|
|
414
|
-
attributes.push({ name: attributeName, value });
|
|
415
|
-
}
|
|
416
|
-
return { valid: true, element: { name, attributes } };
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
function parseSrcset(value) {
|
|
420
|
-
const values = [];
|
|
421
|
-
let cursor = 0;
|
|
422
|
-
while (cursor < value.length) {
|
|
423
|
-
while (/\s/.test(value[cursor] ?? '')) cursor += 1;
|
|
424
|
-
if (cursor >= value.length || value[cursor] === ',') {
|
|
425
|
-
return { valid: false, values: [] };
|
|
426
|
-
}
|
|
427
|
-
const dataReference =
|
|
428
|
-
value.slice(cursor, cursor + 'data:'.length).toLowerCase() === 'data:';
|
|
429
|
-
const urlStart = cursor;
|
|
430
|
-
if (dataReference) {
|
|
431
|
-
while (cursor < value.length && !/\s/.test(value[cursor])) {
|
|
432
|
-
cursor += 1;
|
|
433
|
-
}
|
|
434
|
-
} else {
|
|
435
|
-
while (cursor < value.length && !/[\s,]/.test(value[cursor])) {
|
|
436
|
-
cursor += 1;
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
const url = value.slice(urlStart, cursor);
|
|
440
|
-
if (!url) return { valid: false, values: [] };
|
|
441
|
-
values.push(url);
|
|
442
|
-
if (value[cursor] === ',') {
|
|
443
|
-
cursor += 1;
|
|
444
|
-
continue;
|
|
445
|
-
}
|
|
446
|
-
while (/\s/.test(value[cursor] ?? '')) cursor += 1;
|
|
447
|
-
const descriptorStart = cursor;
|
|
448
|
-
while (cursor < value.length && !/[\s,]/.test(value[cursor])) {
|
|
449
|
-
cursor += 1;
|
|
450
|
-
}
|
|
451
|
-
const descriptor = value.slice(descriptorStart, cursor);
|
|
452
|
-
if (descriptor && !/^(?:\d+w|(?:\d+(?:\.\d+)?|\.\d+)x)$/.test(descriptor)) {
|
|
453
|
-
return { valid: false, values: [] };
|
|
454
|
-
}
|
|
455
|
-
while (/\s/.test(value[cursor] ?? '')) cursor += 1;
|
|
456
|
-
if (cursor < value.length) {
|
|
457
|
-
if (value[cursor] !== ',') return { valid: false, values: [] };
|
|
458
|
-
cursor += 1;
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
return {
|
|
462
|
-
valid: values.length > 0,
|
|
463
|
-
values,
|
|
464
|
-
};
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
function hasUnsafeRawReferenceBytes(value) {
|
|
468
|
-
if (
|
|
469
|
-
value !== value.trim() ||
|
|
470
|
-
value.includes('\\') ||
|
|
471
|
-
value.includes('\0') ||
|
|
472
|
-
/%(?:2e|2f|5c)/i.test(value)
|
|
473
|
-
) {
|
|
474
|
-
return true;
|
|
475
|
-
}
|
|
476
|
-
for (const char of value) {
|
|
477
|
-
const code = char.codePointAt(0);
|
|
478
|
-
if (code < 0x20 || code === 0x7f) return true;
|
|
479
|
-
}
|
|
480
|
-
return false;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
function isResourceAttribute(elementName, attributeName) {
|
|
484
|
-
return (
|
|
485
|
-
['poster', 'src', 'srcset'].includes(attributeName) ||
|
|
486
|
-
(['href', 'xlink:href'].includes(attributeName) &&
|
|
487
|
-
!['a', 'area'].includes(elementName))
|
|
488
|
-
);
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
function isCanonicalSitePath(value) {
|
|
492
|
-
if (
|
|
493
|
-
typeof value !== 'string' ||
|
|
494
|
-
!/^site\/[a-z0-9._-]+(?:\/[a-z0-9._-]+)*$/i.test(value)
|
|
495
|
-
) {
|
|
496
|
-
return false;
|
|
497
|
-
}
|
|
498
|
-
return value.split('/').every((segment) => !['.', '..'].includes(segment));
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
function referenceDetail(artifact, element, attribute, reference) {
|
|
502
|
-
return {
|
|
503
|
-
renderedPath: artifact.renderedPath,
|
|
504
|
-
element: element.name,
|
|
505
|
-
attribute: attribute.name,
|
|
506
|
-
reference,
|
|
507
|
-
};
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
function malformedHtml() {
|
|
511
|
-
return {
|
|
512
|
-
valid: false,
|
|
513
|
-
code: 'malformed-html',
|
|
514
|
-
message: 'Rendered HTML cannot be tokenized safely.',
|
|
515
|
-
};
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
function indexOfCaseInsensitive(source, search, fromIndex) {
|
|
519
|
-
return source.toLowerCase().indexOf(search.toLowerCase(), fromIndex);
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
function decodeHtmlReferences(value) {
|
|
523
|
-
return value.replace(
|
|
524
|
-
/&(?:amp|quot|apos|#39|#x27);/gi,
|
|
525
|
-
(entity) =>
|
|
526
|
-
({
|
|
527
|
-
'&': '&',
|
|
528
|
-
'"': '"',
|
|
529
|
-
''': "'",
|
|
530
|
-
''': "'",
|
|
531
|
-
''': "'",
|
|
532
|
-
})[entity.toLowerCase()] ?? entity,
|
|
533
|
-
);
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
function isObject(value) {
|
|
537
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
538
|
-
}
|