@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,41 +1,12 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
composePublicationTarget,
|
|
6
|
-
normalizePublishRoots,
|
|
7
|
-
} from './s3-roots.mjs';
|
|
8
4
|
import { validatePortablePath } from './safe-paths.mjs';
|
|
9
|
-
import {
|
|
10
|
-
parseCanonicalGithubBlobUrl,
|
|
11
|
-
validateCanonicalGithubBlobTuple,
|
|
12
|
-
} from './source-backlinks.mjs';
|
|
13
5
|
|
|
14
6
|
const SCHEMA_FILES = {
|
|
15
|
-
'run-request': 'run-request.schema.json',
|
|
16
7
|
'fact-base': 'fact-base.schema.json',
|
|
17
|
-
theme: 'theme.schema.json',
|
|
18
8
|
manifest: 'manifest.schema.json',
|
|
19
|
-
|
|
20
|
-
'durability-evidence': 'durability-evidence.schema.json',
|
|
21
|
-
'publish-request/v1': 'publish-request.v1.schema.json',
|
|
22
|
-
'publish-request/v2': 'publish-request.v2.schema.json',
|
|
23
|
-
'publish-receipt/v1': 'publish-receipt.v1.schema.json',
|
|
24
|
-
'publish-receipt/v2': 'publish-receipt.v2.schema.json',
|
|
25
|
-
'author-request/v2': 'author-request.v2.schema.json',
|
|
26
|
-
'author-request/v3': 'author-request.v3.schema.json',
|
|
27
|
-
'author-result/v2': 'author-result.v2.schema.json',
|
|
28
|
-
'set-plan': 'set-plan.v1.schema.json',
|
|
29
|
-
'visual-review-request': 'visual-review-request.v1.schema.json',
|
|
30
|
-
'visual-review-result': 'visual-review-result.v1.schema.json',
|
|
31
|
-
'visual-review-evidence': 'visual-review-evidence.v1.schema.json',
|
|
32
|
-
'terminal-evidence': 'terminal-evidence.v1.schema.json',
|
|
33
|
-
};
|
|
34
|
-
const DEFAULT_SCHEMA_KEYS = {
|
|
35
|
-
'author-request': 'author-request/v3',
|
|
36
|
-
'author-result': 'author-result/v2',
|
|
37
|
-
'publish-request': 'publish-request/v2',
|
|
38
|
-
'publish-receipt': 'publish-receipt/v2',
|
|
9
|
+
theme: 'theme.schema.json',
|
|
39
10
|
};
|
|
40
11
|
|
|
41
12
|
const SCHEMAS = Object.fromEntries(
|
|
@@ -49,34 +20,8 @@ const SCHEMAS = Object.fromEntries(
|
|
|
49
20
|
const SCHEMAS_BY_ID = new Map(
|
|
50
21
|
Object.values(SCHEMAS).map((schema) => [schema.$id, schema]),
|
|
51
22
|
);
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
// does not start with `'publish-request'`, so an `$id`-form call skipped the
|
|
55
|
-
// default-deny publication-root gate entirely and answered `valid: true` for a
|
|
56
|
-
// credential-bearing root. Normalize once, here, so no individual gate has to
|
|
57
|
-
// remember to handle both spellings.
|
|
58
|
-
const KIND_BY_SCHEMA_ID = new Map(
|
|
59
|
-
Object.entries(SCHEMAS).map(([kind, schema]) => [schema.$id, kind]),
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
function canonicalContractKind(kind) {
|
|
63
|
-
return KIND_BY_SCHEMA_ID.get(kind) ?? kind;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const LEGACY_PUBLISH_RECEIPT = 'explainer-kit.publish-receipt/v1';
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* True for every publish-receipt shape except the retained v1 replay form.
|
|
70
|
-
*
|
|
71
|
-
* Stated as "not v1" rather than "=== v2" on purpose: keying verification to an
|
|
72
|
-
* exact version string means a future `publish-receipt/v3` silently skips
|
|
73
|
-
* sentinel verification and per-artifact validation, which is the same
|
|
74
|
-
* fail-open shape the publication-root gate was rewritten to eliminate. v1 is
|
|
75
|
-
* the only version that legitimately lacks these facts.
|
|
76
|
-
*/
|
|
77
|
-
export function isVerifiablePublishReceipt(value) {
|
|
78
|
-
return value?.schemaVersion !== LEGACY_PUBLISH_RECEIPT;
|
|
79
|
-
}
|
|
23
|
+
const DATE_TIME_PATTERN =
|
|
24
|
+
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
80
25
|
const RAW_SECRET_KEYS = new Set([
|
|
81
26
|
'accesskey',
|
|
82
27
|
'accesskeyid',
|
|
@@ -91,104 +36,24 @@ const RAW_SECRET_KEYS = new Set([
|
|
|
91
36
|
'sessiontoken',
|
|
92
37
|
'token',
|
|
93
38
|
]);
|
|
94
|
-
const DATE_TIME_PATTERN =
|
|
95
|
-
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
96
|
-
const SET_PLAN_RECORD_PATHS = [
|
|
97
|
-
'source/set-plan/request.json',
|
|
98
|
-
'source/set-plan/result.json',
|
|
99
|
-
'source/set-plan/ledger.json',
|
|
100
|
-
'source/set-plan/portfolio.json',
|
|
101
|
-
'source/set-plan/drafts.json',
|
|
102
|
-
];
|
|
103
39
|
|
|
104
|
-
export function validateContract(kind, value
|
|
105
|
-
const schema =
|
|
40
|
+
export function validateContract(kind, value) {
|
|
41
|
+
const schema = SCHEMAS[kind] ?? SCHEMAS_BY_ID.get(kind);
|
|
106
42
|
if (!schema) {
|
|
107
|
-
|
|
108
|
-
valid: false,
|
|
109
|
-
errors: [
|
|
110
|
-
{
|
|
111
|
-
path: '$',
|
|
112
|
-
code: 'unknown-kind',
|
|
113
|
-
message: `Unknown contract kind: ${kind}`,
|
|
114
|
-
},
|
|
115
|
-
],
|
|
116
|
-
};
|
|
43
|
+
throw new Error(`Unknown contract kind: ${kind}`);
|
|
117
44
|
}
|
|
118
45
|
|
|
119
46
|
const errors = [];
|
|
120
|
-
// Every gate below matches on the short registry form, so an `$id`-form kind
|
|
121
|
-
// is normalized before dispatch rather than in each gate.
|
|
122
|
-
const canonicalKind = canonicalContractKind(kind);
|
|
123
47
|
findRawSecrets(value, '$', errors);
|
|
124
48
|
validateSchema(schema, value, '$', schema, errors);
|
|
125
|
-
validateContractPaths(
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
validateSourceBacklinks(canonicalKind, value, errors);
|
|
129
|
-
return { valid: errors.length === 0, errors };
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function validateSourceBacklinks(kind, value, errors) {
|
|
133
|
-
if (kind === 'fact-base' || kind === 'explainer-kit.fact-base/v1') {
|
|
134
|
-
const tuples = [
|
|
135
|
-
...(value.sources ?? []),
|
|
136
|
-
...(value.claims ?? []).flatMap((claim) => claim.citations ?? []),
|
|
137
|
-
...(value.unresolvedClaims ?? []).flatMap(
|
|
138
|
-
(claim) => claim.citations ?? [],
|
|
139
|
-
),
|
|
140
|
-
];
|
|
141
|
-
tuples.forEach((tuple, index) => {
|
|
142
|
-
const declaresBacklink = ['repository', 'path', 'lineRange', 'url'].some(
|
|
143
|
-
(field) => tuple[field] !== undefined,
|
|
144
|
-
);
|
|
145
|
-
if (declaresBacklink && !validateCanonicalGithubBlobTuple(tuple)) {
|
|
146
|
-
add(
|
|
147
|
-
errors,
|
|
148
|
-
`$.sourceBacklinks[${index}]`,
|
|
149
|
-
'source-backlink',
|
|
150
|
-
'Must declare one complete canonical GitHub blob backlink tuple.',
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
});
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
if (kind !== 'manifest' && kind !== 'explainer-kit.manifest/v1') return;
|
|
157
|
-
(value.source?.backlinks ?? []).forEach((entry, index) => {
|
|
158
|
-
try {
|
|
159
|
-
parseCanonicalGithubBlobUrl(entry.url);
|
|
160
|
-
} catch (error) {
|
|
161
|
-
add(
|
|
162
|
-
errors,
|
|
163
|
-
`$.source.backlinks[${index}].url`,
|
|
164
|
-
'source-backlink',
|
|
165
|
-
error instanceof Error
|
|
166
|
-
? error.message
|
|
167
|
-
: 'Must be a canonical GitHub blob backlink.',
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function resolveContractSchema(kind, value) {
|
|
174
|
-
if (SCHEMAS[kind]) {
|
|
175
|
-
return SCHEMAS[kind];
|
|
49
|
+
validateContractPaths(kind, value, errors);
|
|
50
|
+
if (schema === SCHEMAS['fact-base']) {
|
|
51
|
+
rejectRetiredCitationKeys(value, errors);
|
|
176
52
|
}
|
|
177
|
-
if (
|
|
178
|
-
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const defaultKey = DEFAULT_SCHEMA_KEYS[kind];
|
|
182
|
-
if (!defaultKey) {
|
|
183
|
-
return null;
|
|
53
|
+
if (schema === SCHEMAS.manifest) {
|
|
54
|
+
validateManifest(value, errors);
|
|
184
55
|
}
|
|
185
|
-
|
|
186
|
-
const declared = isObject(value)
|
|
187
|
-
? SCHEMAS_BY_ID.get(value.schemaVersion)
|
|
188
|
-
: undefined;
|
|
189
|
-
return declared?.$id.startsWith(`explainer-kit.${kind}/`)
|
|
190
|
-
? declared
|
|
191
|
-
: SCHEMAS[defaultKey];
|
|
56
|
+
return { valid: errors.length === 0, errors };
|
|
192
57
|
}
|
|
193
58
|
|
|
194
59
|
export function canonicalHash(value) {
|
|
@@ -202,9 +67,7 @@ export function canonicalStringify(value) {
|
|
|
202
67
|
}
|
|
203
68
|
|
|
204
69
|
function canonicalize(value) {
|
|
205
|
-
if (Array.isArray(value))
|
|
206
|
-
return value.map(canonicalize);
|
|
207
|
-
}
|
|
70
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
208
71
|
if (isObject(value)) {
|
|
209
72
|
return Object.fromEntries(
|
|
210
73
|
Object.keys(value)
|
|
@@ -215,6 +78,88 @@ function canonicalize(value) {
|
|
|
215
78
|
return value;
|
|
216
79
|
}
|
|
217
80
|
|
|
81
|
+
function rejectRetiredCitationKeys(value, errors) {
|
|
82
|
+
for (const [group, claims] of [
|
|
83
|
+
['claims', value?.claims],
|
|
84
|
+
['unresolvedClaims', value?.unresolvedClaims],
|
|
85
|
+
]) {
|
|
86
|
+
for (const [claimIndex, claim] of (claims ?? []).entries()) {
|
|
87
|
+
for (const [citationIndex, citation] of (
|
|
88
|
+
claim?.citations ?? []
|
|
89
|
+
).entries()) {
|
|
90
|
+
for (const key of [
|
|
91
|
+
'repository',
|
|
92
|
+
'revision',
|
|
93
|
+
'path',
|
|
94
|
+
'lineRange',
|
|
95
|
+
'url',
|
|
96
|
+
]) {
|
|
97
|
+
if (citation?.[key] !== undefined) {
|
|
98
|
+
add(
|
|
99
|
+
errors,
|
|
100
|
+
`$.${group}[${claimIndex}].citations[${citationIndex}].${key}`,
|
|
101
|
+
'unknown-key',
|
|
102
|
+
`Unknown property ${key}.`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function validateManifest(value, errors) {
|
|
112
|
+
const immutableHashes = isObject(value?.immutableHashes)
|
|
113
|
+
? value.immutableHashes
|
|
114
|
+
: {};
|
|
115
|
+
if (
|
|
116
|
+
typeof value?.theme?.path === 'string' &&
|
|
117
|
+
!(value.theme.path in immutableHashes)
|
|
118
|
+
) {
|
|
119
|
+
add(
|
|
120
|
+
errors,
|
|
121
|
+
'$.theme.path',
|
|
122
|
+
'immutable-package-incomplete',
|
|
123
|
+
'Theme path must be covered by immutable hashes.',
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
if (
|
|
127
|
+
typeof value?.theme?.hash === 'string' &&
|
|
128
|
+
value.theme.hash !== immutableHashes['theme.resolved.json']
|
|
129
|
+
) {
|
|
130
|
+
add(
|
|
131
|
+
errors,
|
|
132
|
+
'$.theme.hash',
|
|
133
|
+
'hash-mismatch',
|
|
134
|
+
'Theme hash must match theme.resolved.json.',
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
for (const [index, artifact] of (value?.artifacts ?? []).entries()) {
|
|
138
|
+
if (
|
|
139
|
+
typeof artifact?.contentPath === 'string' &&
|
|
140
|
+
!(artifact.contentPath in immutableHashes)
|
|
141
|
+
) {
|
|
142
|
+
add(
|
|
143
|
+
errors,
|
|
144
|
+
`$.artifacts[${index}].contentPath`,
|
|
145
|
+
'immutable-package-incomplete',
|
|
146
|
+
'Artifact content path must be covered by immutable hashes.',
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
if (
|
|
150
|
+
typeof artifact?.hash === 'string' &&
|
|
151
|
+
artifact.hash !== immutableHashes[artifact.contentPath]
|
|
152
|
+
) {
|
|
153
|
+
add(
|
|
154
|
+
errors,
|
|
155
|
+
`$.artifacts[${index}].hash`,
|
|
156
|
+
'hash-mismatch',
|
|
157
|
+
'Artifact hash must match its immutable content hash.',
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
218
163
|
function validateSchema(schema, value, path, rootSchema, errors) {
|
|
219
164
|
if (schema.$ref) {
|
|
220
165
|
if (schema.$ref.endsWith('/safeRelativePath')) {
|
|
@@ -235,10 +180,8 @@ function validateSchema(schema, value, path, rootSchema, errors) {
|
|
|
235
180
|
validateSchema(resolved.schema, value, path, resolved.root, errors);
|
|
236
181
|
}
|
|
237
182
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
validateSchema(child, value, path, rootSchema, errors);
|
|
241
|
-
}
|
|
183
|
+
for (const child of schema.allOf ?? []) {
|
|
184
|
+
validateSchema(child, value, path, rootSchema, errors);
|
|
242
185
|
}
|
|
243
186
|
|
|
244
187
|
if (schema.oneOf) {
|
|
@@ -368,9 +311,7 @@ function validateSchema(schema, value, path, rootSchema, errors) {
|
|
|
368
311
|
}
|
|
369
312
|
}
|
|
370
313
|
for (const [key, childValue] of Object.entries(value)) {
|
|
371
|
-
if (key in properties)
|
|
372
|
-
continue;
|
|
373
|
-
}
|
|
314
|
+
if (key in properties) continue;
|
|
374
315
|
if (schema.additionalProperties === false) {
|
|
375
316
|
add(
|
|
376
317
|
errors,
|
|
@@ -403,193 +344,20 @@ function validateSchema(schema, value, path, rootSchema, errors) {
|
|
|
403
344
|
}
|
|
404
345
|
|
|
405
346
|
function validateContractPaths(kind, value, errors) {
|
|
406
|
-
if (
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
if (kind === 'run-request') {
|
|
411
|
-
addLexicalPathErrors(value.outputRoot, '$.outputRoot', errors, true);
|
|
412
|
-
if (isObject(value.factBase)) {
|
|
413
|
-
addLexicalPathErrors(
|
|
414
|
-
value.factBase.path,
|
|
415
|
-
'$.factBase.path',
|
|
416
|
-
errors,
|
|
417
|
-
true,
|
|
418
|
-
);
|
|
419
|
-
}
|
|
420
|
-
if (isObject(value.theme)) {
|
|
421
|
-
addLexicalPathErrors(
|
|
422
|
-
value.theme.suppliedBundlePath,
|
|
423
|
-
'$.theme.suppliedBundlePath',
|
|
424
|
-
errors,
|
|
425
|
-
true,
|
|
426
|
-
);
|
|
427
|
-
}
|
|
428
|
-
if (isObject(value.durability) && isObject(value.durability.publish)) {
|
|
429
|
-
validateContractPaths(
|
|
430
|
-
'publish-request',
|
|
431
|
-
value.durability.publish,
|
|
432
|
-
errors,
|
|
433
|
-
);
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
if (kind === 'publish-request') {
|
|
438
|
-
addLexicalPathErrors(value.siteRoot, '$.siteRoot', errors, true);
|
|
439
|
-
addLexicalPathErrors(value.manifestPath, '$.manifestPath', errors, true);
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
if (kind === 'durability-evidence') {
|
|
443
|
-
addLexicalPathErrors(value.manifestPath, '$.manifestPath', errors, true);
|
|
444
|
-
if (isObject(value.evidence)) {
|
|
445
|
-
addLexicalPathErrors(
|
|
446
|
-
value.evidence.repoRoot,
|
|
447
|
-
'$.evidence.repoRoot',
|
|
448
|
-
errors,
|
|
449
|
-
true,
|
|
450
|
-
);
|
|
451
|
-
addLexicalPathErrors(
|
|
452
|
-
value.evidence.receiptPath,
|
|
453
|
-
'$.evidence.receiptPath',
|
|
454
|
-
errors,
|
|
455
|
-
true,
|
|
456
|
-
);
|
|
457
|
-
if (Array.isArray(value.evidence.paths)) {
|
|
458
|
-
value.evidence.paths.forEach((candidate, index) =>
|
|
459
|
-
addLexicalPathErrors(
|
|
460
|
-
candidate,
|
|
461
|
-
`$.evidence.paths[${index}]`,
|
|
462
|
-
errors,
|
|
463
|
-
false,
|
|
464
|
-
),
|
|
465
|
-
);
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
if (kind === 'manifest' && Array.isArray(value.artifacts)) {
|
|
471
|
-
value.artifacts.forEach((artifact, index) => {
|
|
472
|
-
if (isObject(artifact) && isObject(artifact.rebuild)) {
|
|
473
|
-
addLexicalPathErrors(
|
|
474
|
-
artifact.rebuild.cwd,
|
|
475
|
-
`$.artifacts[${index}].rebuild.cwd`,
|
|
476
|
-
errors,
|
|
477
|
-
true,
|
|
478
|
-
);
|
|
479
|
-
}
|
|
480
|
-
});
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
function validatePublicationRoots(kind, value, errors, path = '$') {
|
|
485
|
-
if (!isObject(value)) return;
|
|
486
|
-
|
|
487
|
-
if (kind === 'run-request') {
|
|
488
|
-
const publish = value.durability?.publish;
|
|
489
|
-
if (isObject(publish)) {
|
|
490
|
-
validatePublicationRoots(
|
|
491
|
-
'publish-request',
|
|
492
|
-
publish,
|
|
493
|
-
errors,
|
|
494
|
-
`${path}.durability.publish`,
|
|
495
|
-
);
|
|
496
|
-
}
|
|
497
|
-
return;
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
// Default-deny: every publish-request shape is validated, regardless of its
|
|
501
|
-
// declared schemaVersion. Keying this gate to an exact version string is what
|
|
502
|
-
// let `publish-request/v1` reach `initializeRun` with credential-bearing roots,
|
|
503
|
-
// and would let any future version reintroduce the same bypass.
|
|
504
|
-
if (kind.startsWith('publish-request')) {
|
|
505
|
-
try {
|
|
506
|
-
normalizePublishRoots(value.s3Uri, value.publicBaseUrl);
|
|
507
|
-
} catch {
|
|
508
|
-
add(
|
|
509
|
-
errors,
|
|
510
|
-
path,
|
|
511
|
-
'publish-roots',
|
|
512
|
-
'Publish request roots must be credential-free and canonical.',
|
|
513
|
-
);
|
|
514
|
-
}
|
|
347
|
+
if (
|
|
348
|
+
!isObject(value) ||
|
|
349
|
+
(kind !== 'manifest' && kind !== 'explainer-kit.manifest/v1')
|
|
350
|
+
) {
|
|
515
351
|
return;
|
|
516
352
|
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
// has its roots screened, whatever version it declares. Pinning this to the
|
|
521
|
-
// exact v2 string let a `publish-receipt/v1` carry credential-bearing and
|
|
522
|
-
// internal-address roots straight through to `publish-receipt.json`, a
|
|
523
|
-
// retained hash-covered member of the run package, and on into the
|
|
524
|
-
// `publish-summary/v1` projection.
|
|
525
|
-
const receiptV2 = isVerifiablePublishReceipt(value);
|
|
526
|
-
let roots;
|
|
527
|
-
try {
|
|
528
|
-
roots = normalizePublishRoots(
|
|
529
|
-
value.roots?.s3Uri,
|
|
530
|
-
value.roots?.publicBaseUrl,
|
|
531
|
-
);
|
|
532
|
-
// The stricter *canonical-form* assertion stays v2-only: v1 is retained
|
|
533
|
-
// purely for replay and may legitimately carry non-canonical historical
|
|
534
|
-
// forms such as trailing slashes. Credential and address screening above
|
|
535
|
-
// applies to both, because no replay need justifies either.
|
|
536
|
-
if (
|
|
537
|
-
receiptV2 &&
|
|
538
|
-
(roots.s3Uri !== value.roots.s3Uri ||
|
|
539
|
-
roots.publicBaseUrl !== value.roots.publicBaseUrl)
|
|
540
|
-
) {
|
|
541
|
-
throw new Error('noncanonical');
|
|
542
|
-
}
|
|
543
|
-
} catch {
|
|
544
|
-
add(
|
|
545
|
-
errors,
|
|
546
|
-
`${path}.roots`,
|
|
547
|
-
'publish-roots',
|
|
548
|
-
'Publish receipt roots must be credential-free and canonical.',
|
|
549
|
-
);
|
|
550
|
-
return;
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
if (!receiptV2) return;
|
|
554
|
-
|
|
555
|
-
for (const [index, artifact] of (Array.isArray(value.artifacts)
|
|
556
|
-
? value.artifacts
|
|
557
|
-
: []
|
|
558
|
-
).entries()) {
|
|
559
|
-
if (!isObject(artifact) || typeof artifact.relativePath !== 'string') {
|
|
560
|
-
continue;
|
|
561
|
-
}
|
|
562
|
-
const publishPath = artifact.relativePath.startsWith('site/')
|
|
563
|
-
? artifact.relativePath.slice('site/'.length)
|
|
564
|
-
: artifact.relativePath;
|
|
565
|
-
try {
|
|
566
|
-
const expected = composePublicationTarget(publishPath, roots);
|
|
567
|
-
if (
|
|
568
|
-
artifact.s3Uri !== expected.s3Uri ||
|
|
569
|
-
artifact.publicUrl !== expected.publicUrl
|
|
570
|
-
) {
|
|
571
|
-
throw new Error('destination mismatch');
|
|
572
|
-
}
|
|
573
|
-
} catch {
|
|
574
|
-
add(
|
|
575
|
-
errors,
|
|
576
|
-
`${path}.artifacts[${index}]`,
|
|
577
|
-
'receipt-artifact-destination',
|
|
578
|
-
'Publish receipt artifact destinations must be canonical children of the validated roots.',
|
|
579
|
-
);
|
|
580
|
-
}
|
|
581
|
-
}
|
|
353
|
+
for (const [path, hash] of Object.entries(value.immutableHashes ?? {})) {
|
|
354
|
+
addLexicalPathErrors(path, `$.immutableHashes.${path}`, errors, false);
|
|
355
|
+
if (typeof hash !== 'string') continue;
|
|
582
356
|
}
|
|
583
357
|
}
|
|
584
358
|
|
|
585
359
|
function addLexicalPathErrors(value, path, errors, allowAbsolute) {
|
|
586
|
-
if (value === undefined)
|
|
587
|
-
return;
|
|
588
|
-
}
|
|
589
|
-
if (typeof value !== 'string') {
|
|
590
|
-
return;
|
|
591
|
-
}
|
|
592
|
-
|
|
360
|
+
if (value === undefined || typeof value !== 'string') return;
|
|
593
361
|
const result = validatePortablePath(value, { allowAbsolute });
|
|
594
362
|
if (!result.valid) {
|
|
595
363
|
add(errors, path, 'unsafe-path', result.errors[0].message);
|
|
@@ -603,1180 +371,21 @@ function resolveReference(reference, rootSchema) {
|
|
|
603
371
|
.split('/')
|
|
604
372
|
.map((part) => part.replaceAll('~1', '/').replaceAll('~0', '~'));
|
|
605
373
|
let schema = rootSchema;
|
|
606
|
-
for (const part of parts)
|
|
607
|
-
schema = schema?.[part];
|
|
608
|
-
}
|
|
374
|
+
for (const part of parts) schema = schema?.[part];
|
|
609
375
|
return schema ? { schema, root: rootSchema } : null;
|
|
610
376
|
}
|
|
611
377
|
const external = SCHEMAS_BY_ID.get(reference);
|
|
612
378
|
return external ? { schema: external, root: external } : null;
|
|
613
379
|
}
|
|
614
380
|
|
|
615
|
-
function
|
|
616
|
-
if (
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
if (kind === 'run-request') {
|
|
621
|
-
const factBase = value.factBase;
|
|
622
|
-
if (isObject(factBase)) {
|
|
623
|
-
const hasPath = typeof factBase.path === 'string';
|
|
624
|
-
const hasSources = Array.isArray(factBase.sources);
|
|
625
|
-
if (
|
|
626
|
-
(factBase.mode === 'supplied' && (!hasPath || hasSources)) ||
|
|
627
|
-
(factBase.mode === 'federated' && (!hasSources || hasPath))
|
|
628
|
-
) {
|
|
629
|
-
add(
|
|
630
|
-
errors,
|
|
631
|
-
'$.factBase',
|
|
632
|
-
'fact-base-fields',
|
|
633
|
-
'Supplied fact bases require only path; federated fact bases require only sources.',
|
|
634
|
-
);
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
const durability = value.durability;
|
|
639
|
-
if (
|
|
640
|
-
isObject(durability) &&
|
|
641
|
-
durability.strategy === 'publish' &&
|
|
642
|
-
!isObject(durability.publish)
|
|
643
|
-
) {
|
|
644
|
-
add(
|
|
645
|
-
errors,
|
|
646
|
-
'$.durability.publish',
|
|
647
|
-
'incomplete-publish',
|
|
648
|
-
'Publish durability requires a complete publish request.',
|
|
649
|
-
);
|
|
650
|
-
}
|
|
651
|
-
if (
|
|
652
|
-
isObject(durability) &&
|
|
653
|
-
durability.strategy !== 'publish' &&
|
|
654
|
-
'publish' in durability
|
|
655
|
-
) {
|
|
656
|
-
add(
|
|
657
|
-
errors,
|
|
658
|
-
'$.durability.publish',
|
|
659
|
-
'unexpected-publish',
|
|
660
|
-
'Publish settings are allowed only for publish durability.',
|
|
661
|
-
);
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
if (
|
|
665
|
-
isObject(value.privacy) &&
|
|
666
|
-
value.privacy.retainRawArtDirection === true &&
|
|
667
|
-
(!isObject(value.theme) ||
|
|
668
|
-
typeof value.theme.artDirection !== 'string' ||
|
|
669
|
-
value.theme.artDirection.length === 0)
|
|
670
|
-
) {
|
|
671
|
-
add(
|
|
672
|
-
errors,
|
|
673
|
-
'$.privacy.retainRawArtDirection',
|
|
674
|
-
'art-direction-required',
|
|
675
|
-
'Retaining raw art direction requires theme.artDirection.',
|
|
676
|
-
);
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
if (value.recapMode !== undefined && value.recipe?.id !== 'project-recap') {
|
|
680
|
-
add(
|
|
681
|
-
errors,
|
|
682
|
-
'$.recapMode',
|
|
683
|
-
'recap-mode-recipe',
|
|
684
|
-
'recapMode is allowed only for the project-recap recipe.',
|
|
685
|
-
);
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
if (kind === 'set-plan') {
|
|
690
|
-
validateSetPlan(value, errors);
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
if (
|
|
694
|
-
['author-request', 'author-request/v2', 'author-request/v3'].includes(
|
|
695
|
-
kind,
|
|
696
|
-
) ||
|
|
697
|
-
[
|
|
698
|
-
'explainer-kit.author-request/v2',
|
|
699
|
-
'explainer-kit.author-request/v3',
|
|
700
|
-
].includes(value.schemaVersion)
|
|
701
|
-
) {
|
|
702
|
-
validateAuthorSetContext(value, errors);
|
|
703
|
-
validateVisualAuthoringGuidance(value, errors);
|
|
704
|
-
validateAuthorGraphSemantics(value, errors);
|
|
705
|
-
if (value.schemaVersion === 'explainer-kit.author-request/v3') {
|
|
706
|
-
validateAuthorArtifactLinks(value, errors);
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
if (kind === 'visual-review-request') {
|
|
711
|
-
if (
|
|
712
|
-
typeof value.requestHash === 'string' &&
|
|
713
|
-
value.requestHash !== canonicalHash(visualReviewRequestPayload(value))
|
|
714
|
-
) {
|
|
715
|
-
add(
|
|
716
|
-
errors,
|
|
717
|
-
'$.requestHash',
|
|
718
|
-
'request-hash-mismatch',
|
|
719
|
-
'Visual review request hash does not match its canonical evidence payload.',
|
|
720
|
-
);
|
|
721
|
-
}
|
|
722
|
-
if (
|
|
723
|
-
typeof value.requestHash === 'string' &&
|
|
724
|
-
typeof value.requestId === 'string' &&
|
|
725
|
-
value.requestId !== visualReviewRequestId(value.requestHash)
|
|
726
|
-
) {
|
|
727
|
-
add(
|
|
728
|
-
errors,
|
|
729
|
-
'$.requestId',
|
|
730
|
-
'request-id-mismatch',
|
|
731
|
-
'Visual review request identity does not match its canonical request hash.',
|
|
732
|
-
);
|
|
733
|
-
}
|
|
734
|
-
const plannedArtifactIds = Array.isArray(value.plan?.portfolio)
|
|
735
|
-
? value.plan.portfolio.map(({ artifactId }) => artifactId)
|
|
736
|
-
: [];
|
|
737
|
-
const plannedIds = new Set(plannedArtifactIds);
|
|
738
|
-
const renderedIds = new Set();
|
|
739
|
-
const requiresObservedCohesion = value.plan?.recipe?.id === 'project-recap';
|
|
740
|
-
const expectedCohesion = expectedLedgerClaims(value.plan?.ledger);
|
|
741
|
-
const observedCohesion = new Set();
|
|
742
|
-
if (
|
|
743
|
-
requiresObservedCohesion &&
|
|
744
|
-
['terminology', 'statuses', 'numericClaims'].some(
|
|
745
|
-
(group) => expectedCohesion[group].size === 0,
|
|
746
|
-
)
|
|
747
|
-
) {
|
|
748
|
-
add(
|
|
749
|
-
errors,
|
|
750
|
-
'$.plan.ledger',
|
|
751
|
-
'cohesion-ledger-empty',
|
|
752
|
-
'Adaptive recap review requires non-empty terminology, status, and numeric ledger entries.',
|
|
753
|
-
);
|
|
754
|
-
}
|
|
755
|
-
for (const [index, artifact] of (Array.isArray(value.renderedArtifacts)
|
|
756
|
-
? value.renderedArtifacts
|
|
757
|
-
: []
|
|
758
|
-
).entries()) {
|
|
759
|
-
for (const [evidenceIndex, evidence] of (Array.isArray(artifact?.evidence)
|
|
760
|
-
? artifact.evidence
|
|
761
|
-
: []
|
|
762
|
-
).entries()) {
|
|
763
|
-
if (evidence?.captureIdentity !== value.captureIdentity) {
|
|
764
|
-
add(
|
|
765
|
-
errors,
|
|
766
|
-
`$.renderedArtifacts[${index}].evidence[${evidenceIndex}].captureIdentity`,
|
|
767
|
-
'browser-runtime-mismatch',
|
|
768
|
-
'Every visual-review evidence record must bind the request browser capture identity.',
|
|
769
|
-
);
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
if (!plannedIds.has(artifact?.artifactId)) {
|
|
773
|
-
add(
|
|
774
|
-
errors,
|
|
775
|
-
`$.renderedArtifacts[${index}].artifactId`,
|
|
776
|
-
'unknown-artifact',
|
|
777
|
-
'Rendered artifact is not present in the shared set plan.',
|
|
778
|
-
);
|
|
779
|
-
}
|
|
780
|
-
if (renderedIds.has(artifact?.artifactId)) {
|
|
781
|
-
add(
|
|
782
|
-
errors,
|
|
783
|
-
`$.renderedArtifacts[${index}].artifactId`,
|
|
784
|
-
'duplicate-artifact',
|
|
785
|
-
'Rendered artifact IDs must be unique.',
|
|
786
|
-
);
|
|
787
|
-
}
|
|
788
|
-
renderedIds.add(artifact?.artifactId);
|
|
789
|
-
const observations = Array.isArray(artifact?.cohesionObservations)
|
|
790
|
-
? artifact.cohesionObservations
|
|
791
|
-
: [];
|
|
792
|
-
if (requiresObservedCohesion && observations.length === 0) {
|
|
793
|
-
add(
|
|
794
|
-
errors,
|
|
795
|
-
`$.renderedArtifacts[${index}].cohesionObservations`,
|
|
796
|
-
'cohesion-observations-empty',
|
|
797
|
-
'Every adaptive recap artifact must expose observed shared-ledger evidence.',
|
|
798
|
-
);
|
|
799
|
-
}
|
|
800
|
-
for (const [observationIndex, observation] of observations.entries()) {
|
|
801
|
-
if (
|
|
802
|
-
observation?.artifactId !== artifact?.artifactId ||
|
|
803
|
-
observation?.contentHash !== artifact?.renderedHash
|
|
804
|
-
) {
|
|
805
|
-
add(
|
|
806
|
-
errors,
|
|
807
|
-
`$.renderedArtifacts[${index}].cohesionObservations[${observationIndex}]`,
|
|
808
|
-
'cohesion-binding-mismatch',
|
|
809
|
-
'Cohesion observations must bind to their artifact and exact rendered content hash.',
|
|
810
|
-
);
|
|
811
|
-
continue;
|
|
812
|
-
}
|
|
813
|
-
const expected = expectedCohesion[observation.group]?.get(
|
|
814
|
-
observation.claim,
|
|
815
|
-
);
|
|
816
|
-
if (
|
|
817
|
-
expected === undefined ||
|
|
818
|
-
normalizeComparable(expected) !==
|
|
819
|
-
normalizeComparable(observation.value)
|
|
820
|
-
) {
|
|
821
|
-
add(
|
|
822
|
-
errors,
|
|
823
|
-
`$.renderedArtifacts[${index}].cohesionObservations[${observationIndex}]`,
|
|
824
|
-
'cohesion-contradiction',
|
|
825
|
-
'Observed cohesion evidence must match an applicable shared-ledger value.',
|
|
826
|
-
);
|
|
827
|
-
continue;
|
|
828
|
-
}
|
|
829
|
-
observedCohesion.add(`${observation.group}:${observation.claim}`);
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
for (const artifactId of plannedArtifactIds) {
|
|
833
|
-
if (!renderedIds.has(artifactId)) {
|
|
834
|
-
add(
|
|
835
|
-
errors,
|
|
836
|
-
'$.renderedArtifacts',
|
|
837
|
-
'missing-artifact',
|
|
838
|
-
`Rendered review set is missing planned artifact ${artifactId}.`,
|
|
839
|
-
);
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
if (requiresObservedCohesion) {
|
|
843
|
-
for (const [group, claims] of Object.entries(expectedCohesion)) {
|
|
844
|
-
for (const claim of claims.keys()) {
|
|
845
|
-
if (!observedCohesion.has(`${group}:${claim}`)) {
|
|
846
|
-
add(
|
|
847
|
-
errors,
|
|
848
|
-
'$.renderedArtifacts',
|
|
849
|
-
'cohesion-claim-unobserved',
|
|
850
|
-
`Shared-ledger claim ${group}.${claim} is not observable in the rendered set.`,
|
|
851
|
-
);
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
if (kind === 'visual-review-result') {
|
|
859
|
-
const reviewRequest = context.visualReviewRequest;
|
|
860
|
-
if (!isObject(reviewRequest)) {
|
|
861
|
-
add(
|
|
862
|
-
errors,
|
|
863
|
-
'$',
|
|
864
|
-
'review-request-required',
|
|
865
|
-
'Visual review results must be validated with their reviewed request.',
|
|
866
|
-
);
|
|
867
|
-
return;
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
const requestValidation = validateContract(
|
|
871
|
-
'visual-review-request',
|
|
872
|
-
reviewRequest,
|
|
873
|
-
);
|
|
874
|
-
if (!requestValidation.valid) {
|
|
875
|
-
add(
|
|
876
|
-
errors,
|
|
877
|
-
'$',
|
|
878
|
-
'invalid-review-request',
|
|
879
|
-
'Visual review result cannot bind to an invalid reviewed request.',
|
|
880
|
-
);
|
|
881
|
-
}
|
|
882
|
-
if (
|
|
883
|
-
value.requestId !== reviewRequest.requestId ||
|
|
884
|
-
value.requestHash !== reviewRequest.requestHash
|
|
885
|
-
) {
|
|
886
|
-
add(
|
|
887
|
-
errors,
|
|
888
|
-
'$.requestHash',
|
|
889
|
-
'review-binding-mismatch',
|
|
890
|
-
'Visual review result must echo the exact reviewed request identity and hash.',
|
|
891
|
-
);
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
const reviewedArtifactIds = Array.isArray(reviewRequest.renderedArtifacts)
|
|
895
|
-
? reviewRequest.renderedArtifacts.map(({ artifactId }) => artifactId)
|
|
896
|
-
: [];
|
|
897
|
-
const reviewedIds = new Set(reviewedArtifactIds);
|
|
898
|
-
const resultArtifactIds = Array.isArray(value.artifactIds)
|
|
899
|
-
? value.artifactIds
|
|
900
|
-
: [];
|
|
901
|
-
const resultIds = new Set(resultArtifactIds);
|
|
902
|
-
if (
|
|
903
|
-
resultIds.size !== reviewedIds.size ||
|
|
904
|
-
reviewedArtifactIds.some((artifactId) => !resultIds.has(artifactId))
|
|
905
|
-
) {
|
|
906
|
-
add(
|
|
907
|
-
errors,
|
|
908
|
-
'$.artifactIds',
|
|
909
|
-
'review-set-mismatch',
|
|
910
|
-
'Visual review result artifact IDs must equal the complete reviewed request set.',
|
|
911
|
-
);
|
|
912
|
-
}
|
|
913
|
-
for (const [index, finding] of (Array.isArray(value.findings)
|
|
914
|
-
? value.findings
|
|
915
|
-
: []
|
|
916
|
-
).entries()) {
|
|
917
|
-
if (!reviewedIds.has(finding?.artifactId)) {
|
|
918
|
-
add(
|
|
919
|
-
errors,
|
|
920
|
-
`$.findings[${index}].artifactId`,
|
|
921
|
-
'detached-finding',
|
|
922
|
-
'Visual review findings must reference an artifact in the reviewed request.',
|
|
923
|
-
);
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
|
-
const findingCount = Array.isArray(value.findings)
|
|
927
|
-
? value.findings.length
|
|
928
|
-
: 0;
|
|
929
|
-
if (
|
|
930
|
-
(value.disposition === 'pass' && findingCount > 0) ||
|
|
931
|
-
(['correct', 'fail'].includes(value.disposition) && findingCount === 0)
|
|
932
|
-
) {
|
|
933
|
-
add(
|
|
934
|
-
errors,
|
|
935
|
-
'$.disposition',
|
|
936
|
-
'disposition-findings-mismatch',
|
|
937
|
-
'Pass requires no findings; correct and fail require at least one correction finding.',
|
|
938
|
-
);
|
|
939
|
-
}
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
if (kind === 'terminal-evidence') {
|
|
943
|
-
validateTerminalEvidence(value, context.manifest, errors);
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
if (kind === 'visual-review-evidence') {
|
|
947
|
-
validateVisualReviewEvidence(
|
|
948
|
-
value,
|
|
949
|
-
context.visualReviewRequest,
|
|
950
|
-
context.attempt,
|
|
951
|
-
errors,
|
|
952
|
-
);
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
if (kind === 'manifest') {
|
|
956
|
-
const paths = [];
|
|
957
|
-
for (const artifact of Array.isArray(value.artifacts)
|
|
958
|
-
? value.artifacts
|
|
959
|
-
: []) {
|
|
960
|
-
if (!isObject(artifact)) {
|
|
961
|
-
continue;
|
|
962
|
-
}
|
|
963
|
-
for (const field of ['contentPath', 'renderedPath']) {
|
|
964
|
-
if (typeof artifact[field] === 'string') {
|
|
965
|
-
paths.push(artifact[field]);
|
|
966
|
-
}
|
|
967
|
-
}
|
|
968
|
-
if (artifact.status === 'built' && typeof artifact.hash !== 'string') {
|
|
969
|
-
add(
|
|
970
|
-
errors,
|
|
971
|
-
'$.artifacts',
|
|
972
|
-
'built-artifact-hash-required',
|
|
973
|
-
'Built artifacts require a canonical hash.',
|
|
974
|
-
);
|
|
975
|
-
}
|
|
976
|
-
if (artifact.rebuildable === true && !isObject(artifact.rebuild)) {
|
|
977
|
-
add(
|
|
978
|
-
errors,
|
|
979
|
-
'$.artifacts',
|
|
980
|
-
'rebuild-metadata-required',
|
|
981
|
-
'Rebuildable artifacts require rebuild metadata.',
|
|
982
|
-
);
|
|
983
|
-
}
|
|
984
|
-
if (
|
|
985
|
-
value.outcome === 'built-durable' &&
|
|
986
|
-
artifact.status === 'built' &&
|
|
987
|
-
(!Array.isArray(artifact.durableEvidence) ||
|
|
988
|
-
artifact.durableEvidence.length === 0)
|
|
989
|
-
) {
|
|
990
|
-
add(
|
|
991
|
-
errors,
|
|
992
|
-
'$.artifacts',
|
|
993
|
-
'durability-evidence-required',
|
|
994
|
-
'Durable built artifacts require durability evidence.',
|
|
995
|
-
);
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
if (new Set(paths).size !== paths.length) {
|
|
999
|
-
add(
|
|
1000
|
-
errors,
|
|
1001
|
-
'$.artifacts',
|
|
1002
|
-
'duplicate-artifact-path',
|
|
1003
|
-
'Artifact content and rendered paths must be unique.',
|
|
1004
|
-
);
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
const requiredProvenance = [
|
|
1008
|
-
'run-request.json',
|
|
1009
|
-
'source/content-approval.json',
|
|
1010
|
-
];
|
|
1011
|
-
const recordedImmutable = isObject(value.immutableHashes)
|
|
1012
|
-
? new Set(Object.keys(value.immutableHashes))
|
|
1013
|
-
: new Set();
|
|
1014
|
-
const retainsSetPlan =
|
|
1015
|
-
value.recipe?.id === 'project-recap' ||
|
|
1016
|
-
SET_PLAN_RECORD_PATHS.some((path) => recordedImmutable.has(path));
|
|
1017
|
-
const expectedImmutable = new Set([
|
|
1018
|
-
...requiredProvenance,
|
|
1019
|
-
value.source?.factBasePath,
|
|
1020
|
-
'source/fact-base.md',
|
|
1021
|
-
...(retainsSetPlan ? SET_PLAN_RECORD_PATHS : []),
|
|
1022
|
-
...(Array.isArray(value.source?.authorResultPaths)
|
|
1023
|
-
? value.source.authorResultPaths
|
|
1024
|
-
: []),
|
|
1025
|
-
value.theme?.path,
|
|
1026
|
-
...(Array.isArray(value.artifacts)
|
|
1027
|
-
? value.artifacts.flatMap((artifact) => [
|
|
1028
|
-
artifact?.contentPath,
|
|
1029
|
-
...(artifact?.status === 'built' &&
|
|
1030
|
-
typeof artifact?.renderedPath === 'string'
|
|
1031
|
-
? [artifact.renderedPath]
|
|
1032
|
-
: []),
|
|
1033
|
-
])
|
|
1034
|
-
: []),
|
|
1035
|
-
]);
|
|
1036
|
-
expectedImmutable.delete(undefined);
|
|
1037
|
-
const missingLegacyPaths = requiredProvenance.filter(
|
|
1038
|
-
(path) => !recordedImmutable.has(path),
|
|
1039
|
-
);
|
|
1040
|
-
if (missingLegacyPaths.length > 0) {
|
|
1041
|
-
add(
|
|
1042
|
-
errors,
|
|
1043
|
-
'$.immutableHashes',
|
|
1044
|
-
'legacy-manifest-incomplete',
|
|
1045
|
-
`Legacy manifest is missing immutable coverage for ${missingLegacyPaths.join(', ')}; regenerate the recap package before archival.`,
|
|
1046
|
-
);
|
|
1047
|
-
}
|
|
1048
|
-
if ([...expectedImmutable].some((path) => !recordedImmutable.has(path))) {
|
|
1049
|
-
add(
|
|
1050
|
-
errors,
|
|
1051
|
-
'$.immutableHashes',
|
|
1052
|
-
'immutable-package-incomplete',
|
|
1053
|
-
'Manifest immutable hashes must cover the complete retained fact-base, set plan, content, theme, and required built artifact package.',
|
|
1054
|
-
);
|
|
1055
|
-
}
|
|
1056
|
-
const hasVisualEvidence = [...recordedImmutable].some((path) =>
|
|
1057
|
-
path.startsWith('qa/visual-review/'),
|
|
1058
|
-
);
|
|
1059
|
-
if (
|
|
1060
|
-
hasVisualEvidence &&
|
|
1061
|
-
[
|
|
1062
|
-
'qa/visual-review/attempt-1/request.json',
|
|
1063
|
-
'qa/visual-review/attempt-1/result.json',
|
|
1064
|
-
].some((path) => !recordedImmutable.has(path))
|
|
1065
|
-
) {
|
|
1066
|
-
add(
|
|
1067
|
-
errors,
|
|
1068
|
-
'$.immutableHashes',
|
|
1069
|
-
'visual-review-chain-incomplete',
|
|
1070
|
-
'Retained visual review evidence requires its bound attempt request and result.',
|
|
1071
|
-
);
|
|
1072
|
-
}
|
|
1073
|
-
for (const path of recordedImmutable) {
|
|
1074
|
-
if (
|
|
1075
|
-
(path.startsWith('qa/browser/') ||
|
|
1076
|
-
path.includes('/visual-review/attempt-')) &&
|
|
1077
|
-
path.endsWith('.png') &&
|
|
1078
|
-
!recordedImmutable.has(path.replace(/\.png$/, '.json'))
|
|
1079
|
-
) {
|
|
1080
|
-
add(
|
|
1081
|
-
errors,
|
|
1082
|
-
'$.immutableHashes',
|
|
1083
|
-
'visual-review-chain-incomplete',
|
|
1084
|
-
`Screenshot evidence ${path} is missing its immutable metrics record.`,
|
|
1085
|
-
);
|
|
1086
|
-
}
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
const record = context.buildRecord;
|
|
1090
|
-
if (isObject(record)) {
|
|
1091
|
-
if (value.runId !== record.runId || value.outcome !== record.outcome) {
|
|
1092
|
-
add(
|
|
1093
|
-
errors,
|
|
1094
|
-
'$',
|
|
1095
|
-
'cross-record-mismatch',
|
|
1096
|
-
'Manifest and build record identity or outcome do not match.',
|
|
1097
|
-
);
|
|
1098
|
-
}
|
|
1099
|
-
if (
|
|
1100
|
-
isObject(value.buildRecord) &&
|
|
1101
|
-
value.buildRecord.hash !== canonicalHash(record)
|
|
1102
|
-
) {
|
|
1103
|
-
add(
|
|
1104
|
-
errors,
|
|
1105
|
-
'$.buildRecord.hash',
|
|
1106
|
-
'hash-mismatch',
|
|
1107
|
-
'Build record hash does not match canonical content.',
|
|
1108
|
-
);
|
|
1109
|
-
}
|
|
1110
|
-
}
|
|
1111
|
-
if (
|
|
1112
|
-
isObject(context.theme) &&
|
|
1113
|
-
isObject(value.theme) &&
|
|
1114
|
-
value.theme.hash !== canonicalHash(context.theme)
|
|
1115
|
-
) {
|
|
1116
|
-
add(
|
|
1117
|
-
errors,
|
|
1118
|
-
'$.theme.hash',
|
|
1119
|
-
'hash-mismatch',
|
|
1120
|
-
'Theme hash does not match canonical content.',
|
|
1121
|
-
);
|
|
1122
|
-
}
|
|
1123
|
-
if (isObject(context.runRequest) && isObject(record)) {
|
|
1124
|
-
const expected =
|
|
1125
|
-
isObject(context.runRequest.theme) &&
|
|
1126
|
-
typeof context.runRequest.theme.renderStrategy === 'string'
|
|
1127
|
-
? context.runRequest.theme.renderStrategy
|
|
1128
|
-
: 'default-only';
|
|
1129
|
-
if (record.renderStrategy !== expected) {
|
|
1130
|
-
add(
|
|
1131
|
-
errors,
|
|
1132
|
-
'$.renderStrategy',
|
|
1133
|
-
'cross-record-mismatch',
|
|
1134
|
-
'Build render strategy does not match the run request.',
|
|
1135
|
-
);
|
|
1136
|
-
}
|
|
1137
|
-
}
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
if (kind.startsWith('publish-receipt') && isVerifiablePublishReceipt(value)) {
|
|
1141
|
-
const sentinel = value.sentinel;
|
|
1142
|
-
if (
|
|
1143
|
-
sentinel?.objectVerification?.status !== 'verified' ||
|
|
1144
|
-
sentinel.objectVerification.hash === undefined
|
|
1145
|
-
) {
|
|
1146
|
-
add(
|
|
1147
|
-
errors,
|
|
1148
|
-
'$.sentinel.objectVerification',
|
|
1149
|
-
'receipt-object-verification',
|
|
1150
|
-
'Publish receipt sentinel requires exact authenticated object verification.',
|
|
1151
|
-
);
|
|
1152
|
-
}
|
|
1153
|
-
const validPublic =
|
|
1154
|
-
value.publicAccess === 'public'
|
|
1155
|
-
? sentinel?.publicVerification?.status === 'verified' &&
|
|
1156
|
-
sentinel.publicVerification.hash === sentinel.objectVerification?.hash
|
|
1157
|
-
: sentinel?.publicVerification?.status === 'skipped-protected';
|
|
1158
|
-
if (!validPublic) {
|
|
1159
|
-
add(
|
|
1160
|
-
errors,
|
|
1161
|
-
'$.sentinel.publicVerification',
|
|
1162
|
-
'receipt-public-verification',
|
|
1163
|
-
'Publish receipt sentinel public verification must match the declared public-access mode.',
|
|
1164
|
-
);
|
|
1165
|
-
}
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
if (
|
|
1169
|
-
kind === 'publish-receipt' &&
|
|
1170
|
-
isObject(context.manifest) &&
|
|
1171
|
-
Array.isArray(value.artifacts)
|
|
1172
|
-
) {
|
|
1173
|
-
const expected = new Map();
|
|
1174
|
-
for (const artifact of context.manifest.artifacts ?? []) {
|
|
1175
|
-
if (
|
|
1176
|
-
isObject(artifact) &&
|
|
1177
|
-
artifact.status === 'built' &&
|
|
1178
|
-
typeof artifact.renderedPath === 'string'
|
|
1179
|
-
) {
|
|
1180
|
-
expected.set(artifact.renderedPath, {
|
|
1181
|
-
hash: artifact.hash,
|
|
1182
|
-
source: { kind: 'manifest', artifactId: artifact.id },
|
|
1183
|
-
});
|
|
1184
|
-
}
|
|
1185
|
-
}
|
|
1186
|
-
if (
|
|
1187
|
-
isObject(context.catalogArtifact) &&
|
|
1188
|
-
typeof context.catalogArtifact.relativePath === 'string'
|
|
1189
|
-
) {
|
|
1190
|
-
expected.set(context.catalogArtifact.relativePath, {
|
|
1191
|
-
hash: context.catalogArtifact.hash,
|
|
1192
|
-
source: { kind: 'auxiliary', name: 'catalog' },
|
|
1193
|
-
});
|
|
1194
|
-
}
|
|
1195
|
-
const received = new Set();
|
|
1196
|
-
for (const artifact of value.artifacts) {
|
|
1197
|
-
const expectedArtifact = expected.get(artifact?.relativePath);
|
|
1198
|
-
if (isObject(artifact) && expectedArtifact?.hash !== artifact.hash) {
|
|
1199
|
-
add(
|
|
1200
|
-
errors,
|
|
1201
|
-
'$.artifacts',
|
|
1202
|
-
'cross-record-mismatch',
|
|
1203
|
-
'Publish receipt artifact does not match the manifest.',
|
|
1204
|
-
);
|
|
1205
|
-
}
|
|
1206
|
-
if (isVerifiablePublishReceipt(value) && isObject(artifact)) {
|
|
1207
|
-
validatePublishReceiptV2Artifact(
|
|
1208
|
-
value,
|
|
1209
|
-
artifact,
|
|
1210
|
-
expectedArtifact,
|
|
1211
|
-
errors,
|
|
1212
|
-
);
|
|
1213
|
-
}
|
|
1214
|
-
if (received.has(artifact?.relativePath)) {
|
|
1215
|
-
add(
|
|
1216
|
-
errors,
|
|
1217
|
-
'$.artifacts',
|
|
1218
|
-
'receipt-artifact-parity',
|
|
1219
|
-
'Publish receipt artifact paths must be unique.',
|
|
1220
|
-
);
|
|
1221
|
-
}
|
|
1222
|
-
received.add(artifact?.relativePath);
|
|
1223
|
-
}
|
|
1224
|
-
if (
|
|
1225
|
-
received.size !== expected.size ||
|
|
1226
|
-
[...expected.keys()].some((path) => !received.has(path))
|
|
1227
|
-
) {
|
|
1228
|
-
add(
|
|
1229
|
-
errors,
|
|
1230
|
-
'$.artifacts',
|
|
1231
|
-
'receipt-artifact-parity',
|
|
1232
|
-
'Publish receipt must exactly cover every manifest artifact and the generated catalog.',
|
|
1233
|
-
);
|
|
1234
|
-
}
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
|
|
1238
|
-
function validateTerminalEvidence(value, manifest, errors) {
|
|
1239
|
-
const reasons = Array.isArray(value.reasons) ? value.reasons : [];
|
|
1240
|
-
const artifactIds = isObject(manifest)
|
|
1241
|
-
? new Set(
|
|
1242
|
-
(Array.isArray(manifest.artifacts) ? manifest.artifacts : []).map(
|
|
1243
|
-
({ id }) => id,
|
|
1244
|
-
),
|
|
1245
|
-
)
|
|
1246
|
-
: null;
|
|
1247
|
-
validateReasonSet(reasons, artifactIds, '$.reasons', errors);
|
|
1248
|
-
|
|
1249
|
-
if (isObject(manifest)) {
|
|
1250
|
-
if (value.runId !== manifest.runId) {
|
|
1251
|
-
add(
|
|
1252
|
-
errors,
|
|
1253
|
-
'$.runId',
|
|
1254
|
-
'terminal-run-mismatch',
|
|
1255
|
-
'Terminal evidence run identity must match its manifest.',
|
|
1256
|
-
);
|
|
1257
|
-
}
|
|
1258
|
-
if (value.outcome !== manifest.outcome) {
|
|
1259
|
-
add(
|
|
1260
|
-
errors,
|
|
1261
|
-
'$.outcome',
|
|
1262
|
-
'terminal-outcome-mismatch',
|
|
1263
|
-
'Terminal evidence outcome must match its manifest.',
|
|
1264
|
-
);
|
|
1265
|
-
}
|
|
1266
|
-
if (value.manifestHash !== canonicalHash(manifest)) {
|
|
1267
|
-
add(
|
|
1268
|
-
errors,
|
|
1269
|
-
'$.manifestHash',
|
|
1270
|
-
'terminal-manifest-mismatch',
|
|
1271
|
-
'Terminal evidence manifest hash must match its manifest.',
|
|
1272
|
-
);
|
|
1273
|
-
}
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
if (value.evidenceDisposition === 'superseded') {
|
|
1277
|
-
const reason = reasons[0];
|
|
1278
|
-
if (
|
|
1279
|
-
reasons.length !== 1 ||
|
|
1280
|
-
reason?.stage !== 'finalization' ||
|
|
1281
|
-
reason?.kind !== 'superseded' ||
|
|
1282
|
-
reason?.count !== 1 ||
|
|
1283
|
-
reason?.artifactId !== undefined
|
|
1284
|
-
) {
|
|
1285
|
-
add(
|
|
1286
|
-
errors,
|
|
1287
|
-
'$.reasons',
|
|
1288
|
-
'supersession-reason',
|
|
1289
|
-
'Superseded evidence requires exactly one count-1 finalization/superseded reason without an artifact ID.',
|
|
1290
|
-
);
|
|
1291
|
-
}
|
|
1292
|
-
if (
|
|
1293
|
-
!isObject(value.supersededBy) ||
|
|
1294
|
-
value.supersededBy.runId === value.runId
|
|
1295
|
-
) {
|
|
1296
|
-
add(
|
|
1297
|
-
errors,
|
|
1298
|
-
'$.supersededBy',
|
|
1299
|
-
'supersession-binding',
|
|
1300
|
-
'Superseded evidence requires a distinct replacement run identity.',
|
|
1301
|
-
);
|
|
1302
|
-
}
|
|
1303
|
-
if (typeof value.manifestHash !== 'string') {
|
|
1304
|
-
add(
|
|
1305
|
-
errors,
|
|
1306
|
-
'$.manifestHash',
|
|
1307
|
-
'supersession-manifest',
|
|
1308
|
-
'Superseded evidence requires the original manifest hash.',
|
|
1309
|
-
);
|
|
1310
|
-
}
|
|
1311
|
-
return;
|
|
1312
|
-
}
|
|
1313
|
-
|
|
1314
|
-
if (value.supersededBy !== undefined) {
|
|
1315
|
-
add(
|
|
1316
|
-
errors,
|
|
1317
|
-
'$.supersededBy',
|
|
1318
|
-
'unexpected-supersession',
|
|
1319
|
-
'Only superseded evidence may identify a replacement run.',
|
|
1320
|
-
);
|
|
1321
|
-
}
|
|
1322
|
-
if (reasons.some((reason) => reason?.kind === 'superseded')) {
|
|
1323
|
-
add(
|
|
1324
|
-
errors,
|
|
1325
|
-
'$.reasons',
|
|
1326
|
-
'unexpected-supersession',
|
|
1327
|
-
'Only superseded evidence may contain a superseded reason.',
|
|
1328
|
-
);
|
|
1329
|
-
}
|
|
1330
|
-
const allowedKinds =
|
|
1331
|
-
value.outcome === 'failed'
|
|
1332
|
-
? new Set(['provider-failure', 'pipeline-failure'])
|
|
1333
|
-
: new Set(['finding', 'provider-failure', 'pipeline-failure']);
|
|
1334
|
-
if (!reasons.some((reason) => allowedKinds.has(reason?.kind))) {
|
|
1335
|
-
add(
|
|
1336
|
-
errors,
|
|
1337
|
-
'$.reasons',
|
|
1338
|
-
'terminal-outcome-reasons',
|
|
1339
|
-
'Terminal evidence reasons do not satisfy the terminal outcome.',
|
|
1340
|
-
);
|
|
1341
|
-
}
|
|
1342
|
-
}
|
|
1343
|
-
|
|
1344
|
-
function validateVisualReviewEvidence(value, request, attempt, errors) {
|
|
1345
|
-
if (!isObject(request)) {
|
|
1346
|
-
add(
|
|
1347
|
-
errors,
|
|
1348
|
-
'$',
|
|
1349
|
-
'review-request-required',
|
|
1350
|
-
'Retained visual evidence requires its adjacent reviewed request.',
|
|
1351
|
-
);
|
|
1352
|
-
return;
|
|
1353
|
-
}
|
|
1354
|
-
const requestValidation = validateContract('visual-review-request', request);
|
|
1355
|
-
if (!requestValidation.valid) {
|
|
1356
|
-
add(
|
|
1357
|
-
errors,
|
|
1358
|
-
'$',
|
|
1359
|
-
'invalid-review-request',
|
|
1360
|
-
'Retained visual evidence cannot bind to an invalid reviewed request.',
|
|
1361
|
-
);
|
|
1362
|
-
}
|
|
1363
|
-
if (value.requestHash !== request.requestHash) {
|
|
1364
|
-
add(
|
|
1365
|
-
errors,
|
|
1366
|
-
'$.requestHash',
|
|
1367
|
-
'review-binding-mismatch',
|
|
1368
|
-
'Retained visual evidence must bind the adjacent request hash.',
|
|
1369
|
-
);
|
|
1370
|
-
}
|
|
1371
|
-
if (![1, 2].includes(attempt) || value.attempt !== attempt) {
|
|
1372
|
-
add(
|
|
1373
|
-
errors,
|
|
1374
|
-
'$.attempt',
|
|
1375
|
-
'review-attempt-mismatch',
|
|
1376
|
-
'Retained visual evidence attempt must match its attempt directory.',
|
|
1377
|
-
);
|
|
1378
|
-
}
|
|
1379
|
-
|
|
1380
|
-
const reasons = Array.isArray(value.reasons) ? value.reasons : [];
|
|
1381
|
-
const artifactIds = new Set(
|
|
1382
|
-
(Array.isArray(request.renderedArtifacts)
|
|
1383
|
-
? request.renderedArtifacts
|
|
1384
|
-
: []
|
|
1385
|
-
).map(({ artifactId }) => artifactId),
|
|
1386
|
-
);
|
|
1387
|
-
validateReasonSet(reasons, artifactIds, '$.reasons', errors);
|
|
1388
|
-
if (reasons.some((reason) => reason?.stage !== 'visual-review')) {
|
|
1389
|
-
add(
|
|
1390
|
-
errors,
|
|
1391
|
-
'$.reasons',
|
|
1392
|
-
'visual-reason-stage',
|
|
1393
|
-
'Retained visual evidence reasons must use the visual-review stage.',
|
|
1394
|
-
);
|
|
1395
|
-
}
|
|
1396
|
-
const validDisposition =
|
|
1397
|
-
(value.disposition === 'pass' && reasons.length === 0) ||
|
|
1398
|
-
(value.disposition === 'correct' &&
|
|
1399
|
-
reasons.length > 0 &&
|
|
1400
|
-
reasons.every((reason) => reason?.kind === 'finding')) ||
|
|
1401
|
-
(value.disposition === 'failed' &&
|
|
1402
|
-
reasons.length > 0 &&
|
|
1403
|
-
reasons.every((reason) =>
|
|
1404
|
-
['provider-failure', 'pipeline-failure'].includes(reason?.kind),
|
|
1405
|
-
));
|
|
1406
|
-
if (!validDisposition) {
|
|
1407
|
-
add(
|
|
1408
|
-
errors,
|
|
1409
|
-
'$.reasons',
|
|
1410
|
-
'visual-disposition-reasons',
|
|
1411
|
-
'Retained visual evidence reasons do not satisfy its disposition.',
|
|
1412
|
-
);
|
|
1413
|
-
}
|
|
1414
|
-
}
|
|
1415
|
-
|
|
1416
|
-
function validateReasonSet(reasons, artifactIds, path, errors) {
|
|
1417
|
-
let total = 0;
|
|
1418
|
-
const tuples = new Set();
|
|
1419
|
-
for (const [index, reason] of reasons.entries()) {
|
|
1420
|
-
if (!isObject(reason)) continue;
|
|
1421
|
-
if (Number.isInteger(reason.count)) total += reason.count;
|
|
1422
|
-
const tuple = `${reason.stage}\0${reason.kind}\0${reason.artifactId ?? ''}`;
|
|
1423
|
-
if (tuples.has(tuple)) {
|
|
1424
|
-
add(
|
|
1425
|
-
errors,
|
|
1426
|
-
`${path}[${index}]`,
|
|
1427
|
-
'duplicate-reason',
|
|
1428
|
-
'Retained reason tuples must be unique.',
|
|
1429
|
-
);
|
|
1430
|
-
}
|
|
1431
|
-
tuples.add(tuple);
|
|
1432
|
-
if (
|
|
1433
|
-
reason.artifactId !== undefined &&
|
|
1434
|
-
artifactIds &&
|
|
1435
|
-
!artifactIds.has(reason.artifactId)
|
|
1436
|
-
) {
|
|
1437
|
-
add(
|
|
1438
|
-
errors,
|
|
1439
|
-
`${path}[${index}].artifactId`,
|
|
1440
|
-
'foreign-artifact',
|
|
1441
|
-
'Retained reason artifact IDs must belong to the bound run.',
|
|
1442
|
-
);
|
|
1443
|
-
}
|
|
1444
|
-
}
|
|
1445
|
-
if (total > 50) {
|
|
1446
|
-
add(
|
|
1447
|
-
errors,
|
|
1448
|
-
path,
|
|
1449
|
-
'reason-total',
|
|
1450
|
-
'Retained reason counts must total at most 50.',
|
|
1451
|
-
);
|
|
1452
|
-
}
|
|
1453
|
-
}
|
|
1454
|
-
|
|
1455
|
-
function validatePublishReceiptV2Artifact(receipt, artifact, expected, errors) {
|
|
1456
|
-
if (!expected || !deepEqual(artifact.source, expected.source)) {
|
|
1457
|
-
add(
|
|
1458
|
-
errors,
|
|
1459
|
-
'$.artifacts',
|
|
1460
|
-
'receipt-artifact-source',
|
|
1461
|
-
'Publish receipt source identity does not match the manifest or generated auxiliary object.',
|
|
1462
|
-
);
|
|
1463
|
-
}
|
|
1464
|
-
if (
|
|
1465
|
-
artifact.objectVerification?.status !== 'verified' ||
|
|
1466
|
-
artifact.objectVerification?.hash !== artifact.hash
|
|
1467
|
-
) {
|
|
1468
|
-
add(
|
|
1469
|
-
errors,
|
|
1470
|
-
'$.artifacts',
|
|
1471
|
-
'receipt-object-verification',
|
|
1472
|
-
'Publish receipt object verification must prove the exact artifact hash.',
|
|
1473
|
-
);
|
|
1474
|
-
}
|
|
1475
|
-
const publicVerification = artifact.publicVerification;
|
|
1476
|
-
const validPublic =
|
|
1477
|
-
receipt.publicAccess === 'public'
|
|
1478
|
-
? publicVerification?.status === 'verified' &&
|
|
1479
|
-
publicVerification?.hash === artifact.hash
|
|
1480
|
-
: publicVerification?.status === 'skipped-protected';
|
|
1481
|
-
if (!validPublic) {
|
|
1482
|
-
add(
|
|
1483
|
-
errors,
|
|
1484
|
-
'$.artifacts',
|
|
1485
|
-
'receipt-public-verification',
|
|
1486
|
-
'Publish receipt public verification must match the declared public-access mode.',
|
|
1487
|
-
);
|
|
1488
|
-
}
|
|
1489
|
-
}
|
|
1490
|
-
|
|
1491
|
-
export function visualReviewRequestPayload(request) {
|
|
1492
|
-
const {
|
|
1493
|
-
requestId: _requestId,
|
|
1494
|
-
requestHash: _requestHash,
|
|
1495
|
-
...payload
|
|
1496
|
-
} = request;
|
|
1497
|
-
return payload;
|
|
1498
|
-
}
|
|
1499
|
-
|
|
1500
|
-
export function visualReviewRequestId(requestHash) {
|
|
1501
|
-
return `visual-review-${String(requestHash).replace(/^sha256:/, '')}`;
|
|
1502
|
-
}
|
|
1503
|
-
|
|
1504
|
-
function expectedLedgerClaims(ledger) {
|
|
1505
|
-
return {
|
|
1506
|
-
terminology: new Map(
|
|
1507
|
-
(ledger?.terminology ?? []).map(({ term }) => [term, term]),
|
|
1508
|
-
),
|
|
1509
|
-
statuses: new Map(
|
|
1510
|
-
(ledger?.statuses ?? []).map(({ subject, value }) => [subject, value]),
|
|
1511
|
-
),
|
|
1512
|
-
numericClaims: new Map(
|
|
1513
|
-
(ledger?.numbers ?? []).map(({ subject, value }) => [subject, value]),
|
|
1514
|
-
),
|
|
1515
|
-
};
|
|
1516
|
-
}
|
|
1517
|
-
|
|
1518
|
-
function normalizeComparable(value) {
|
|
1519
|
-
return String(value).trim().toLocaleLowerCase();
|
|
1520
|
-
}
|
|
1521
|
-
|
|
1522
|
-
function validateSetPlan(value, errors) {
|
|
1523
|
-
const sourceIds = new Set(
|
|
1524
|
-
Array.isArray(value.sourceIds) ? value.sourceIds : [],
|
|
1525
|
-
);
|
|
1526
|
-
const artifactIds = new Set();
|
|
1527
|
-
for (const [index, artifact] of (Array.isArray(value.portfolio)
|
|
1528
|
-
? value.portfolio
|
|
1529
|
-
: []
|
|
1530
|
-
).entries()) {
|
|
1531
|
-
if (artifactIds.has(artifact?.artifactId)) {
|
|
1532
|
-
add(
|
|
1533
|
-
errors,
|
|
1534
|
-
`$.portfolio[${index}].artifactId`,
|
|
1535
|
-
'duplicate-artifact',
|
|
1536
|
-
'Set-plan artifact IDs must be unique.',
|
|
1537
|
-
);
|
|
1538
|
-
}
|
|
1539
|
-
artifactIds.add(artifact?.artifactId);
|
|
1540
|
-
for (const sourceId of Array.isArray(artifact?.sourceIds)
|
|
1541
|
-
? artifact.sourceIds
|
|
1542
|
-
: []) {
|
|
1543
|
-
if (!sourceIds.has(sourceId)) {
|
|
1544
|
-
add(
|
|
1545
|
-
errors,
|
|
1546
|
-
`$.portfolio[${index}].sourceIds`,
|
|
1547
|
-
'unknown-source',
|
|
1548
|
-
`Artifact source ${sourceId} is not declared by the set plan.`,
|
|
1549
|
-
);
|
|
1550
|
-
}
|
|
1551
|
-
}
|
|
1552
|
-
if (artifact?.required === false && !isObject(artifact.justification)) {
|
|
1553
|
-
add(
|
|
1554
|
-
errors,
|
|
1555
|
-
`$.portfolio[${index}].justification`,
|
|
1556
|
-
'optional-justification-required',
|
|
1557
|
-
'Optional artifacts require a source-backed justification.',
|
|
1558
|
-
);
|
|
1559
|
-
}
|
|
1560
|
-
for (const sourceId of Array.isArray(artifact?.justification?.sourceIds)
|
|
1561
|
-
? artifact.justification.sourceIds
|
|
1562
|
-
: []) {
|
|
1563
|
-
if (!sourceIds.has(sourceId) || !artifact.sourceIds?.includes(sourceId)) {
|
|
1564
|
-
add(
|
|
1565
|
-
errors,
|
|
1566
|
-
`$.portfolio[${index}].justification.sourceIds`,
|
|
1567
|
-
'unknown-source',
|
|
1568
|
-
`Justification source ${sourceId} must be declared by the plan and artifact.`,
|
|
1569
|
-
);
|
|
1570
|
-
}
|
|
1571
|
-
}
|
|
1572
|
-
}
|
|
1573
|
-
|
|
1574
|
-
for (const [field, identity] of [
|
|
1575
|
-
['terminology', (entry) => entry?.term],
|
|
1576
|
-
['statuses', (entry) => entry?.subject],
|
|
1577
|
-
['numbers', (entry) => entry?.subject],
|
|
1578
|
-
]) {
|
|
1579
|
-
const seen = new Map();
|
|
1580
|
-
for (const [index, entry] of (Array.isArray(value.ledger?.[field])
|
|
1581
|
-
? value.ledger[field]
|
|
1582
|
-
: []
|
|
1583
|
-
).entries()) {
|
|
1584
|
-
const key = identity(entry);
|
|
1585
|
-
if (
|
|
1586
|
-
seen.has(key) &&
|
|
1587
|
-
canonicalStringify(seen.get(key)) !== canonicalStringify(entry)
|
|
1588
|
-
) {
|
|
1589
|
-
add(
|
|
1590
|
-
errors,
|
|
1591
|
-
`$.ledger.${field}[${index}]`,
|
|
1592
|
-
'ledger-conflict',
|
|
1593
|
-
`Shared ledger contains conflicting values for ${key}.`,
|
|
1594
|
-
);
|
|
1595
|
-
}
|
|
1596
|
-
seen.set(key, entry);
|
|
1597
|
-
}
|
|
1598
|
-
}
|
|
1599
|
-
}
|
|
1600
|
-
|
|
1601
|
-
function validateAuthorSetContext(value, errors) {
|
|
1602
|
-
if (!isObject(value.setContext) || !isObject(value.plannedArtifact)) {
|
|
1603
|
-
return;
|
|
1604
|
-
}
|
|
1605
|
-
const planned = Array.isArray(value.setContext.portfolio)
|
|
1606
|
-
? value.setContext.portfolio.find(
|
|
1607
|
-
({ artifactId }) => artifactId === value.artifactId,
|
|
1608
|
-
)
|
|
1609
|
-
: undefined;
|
|
1610
|
-
if (
|
|
1611
|
-
!planned ||
|
|
1612
|
-
value.plannedArtifact.artifactId !== value.artifactId ||
|
|
1613
|
-
value.plannedArtifact.artifactType !== value.artifactType
|
|
1614
|
-
) {
|
|
1615
|
-
add(
|
|
1616
|
-
errors,
|
|
1617
|
-
'$.plannedArtifact',
|
|
1618
|
-
'set-artifact-mismatch',
|
|
1619
|
-
'Author request identity must match one artifact in the shared set plan.',
|
|
1620
|
-
);
|
|
1621
|
-
return;
|
|
1622
|
-
}
|
|
1623
|
-
if (!deepEqual(planned, value.plannedArtifact)) {
|
|
1624
|
-
add(
|
|
1625
|
-
errors,
|
|
1626
|
-
'$.plannedArtifact',
|
|
1627
|
-
'set-plan-drift',
|
|
1628
|
-
'Author request planned artifact must be identical to the shared set plan entry.',
|
|
1629
|
-
);
|
|
1630
|
-
}
|
|
1631
|
-
}
|
|
1632
|
-
|
|
1633
|
-
function validateAuthorArtifactLinks(value, errors) {
|
|
1634
|
-
if (!Array.isArray(value.artifactLinks) || !isObject(value.setContext)) {
|
|
1635
|
-
return;
|
|
1636
|
-
}
|
|
1637
|
-
const planned = Array.isArray(value.setContext.portfolio)
|
|
1638
|
-
? value.setContext.portfolio
|
|
1639
|
-
: [];
|
|
1640
|
-
const linksById = new Map();
|
|
1641
|
-
for (const [index, link] of value.artifactLinks.entries()) {
|
|
1642
|
-
if (!isObject(link)) continue;
|
|
1643
|
-
if (linksById.has(link.artifactId)) {
|
|
1644
|
-
add(
|
|
1645
|
-
errors,
|
|
1646
|
-
`$.artifactLinks[${index}].artifactId`,
|
|
1647
|
-
'artifact-link-parity',
|
|
1648
|
-
'Canonical artifact link IDs must be unique.',
|
|
1649
|
-
);
|
|
1650
|
-
}
|
|
1651
|
-
linksById.set(link.artifactId, link);
|
|
1652
|
-
}
|
|
1653
|
-
if (
|
|
1654
|
-
linksById.size !== planned.length ||
|
|
1655
|
-
planned.some(
|
|
1656
|
-
({ artifactId, artifactType }) =>
|
|
1657
|
-
linksById.get(artifactId)?.artifactType !== artifactType,
|
|
1658
|
-
)
|
|
1659
|
-
) {
|
|
1660
|
-
add(
|
|
1661
|
-
errors,
|
|
1662
|
-
'$.artifactLinks',
|
|
1663
|
-
'artifact-link-parity',
|
|
1664
|
-
'Canonical artifact links must exactly cover the planned portfolio.',
|
|
1665
|
-
);
|
|
1666
|
-
return;
|
|
1667
|
-
}
|
|
1668
|
-
|
|
1669
|
-
const current = linksById.get(value.artifactId);
|
|
1670
|
-
if (!current || typeof current.sitePath !== 'string') return;
|
|
1671
|
-
const base = new URL(current.sitePath, 'https://explainer.invalid/');
|
|
1672
|
-
for (const [index, link] of value.artifactLinks.entries()) {
|
|
1673
|
-
if (
|
|
1674
|
-
!isObject(link) ||
|
|
1675
|
-
typeof link.href !== 'string' ||
|
|
1676
|
-
typeof link.sitePath !== 'string'
|
|
1677
|
-
) {
|
|
1678
|
-
continue;
|
|
1679
|
-
}
|
|
1680
|
-
let resolved;
|
|
1681
|
-
try {
|
|
1682
|
-
resolved = new URL(link.href, base);
|
|
1683
|
-
} catch {
|
|
1684
|
-
add(
|
|
1685
|
-
errors,
|
|
1686
|
-
`$.artifactLinks[${index}].href`,
|
|
1687
|
-
'artifact-link-resolution',
|
|
1688
|
-
'Canonical artifact href must be a valid relative reference.',
|
|
1689
|
-
);
|
|
1690
|
-
continue;
|
|
1691
|
-
}
|
|
1692
|
-
if (
|
|
1693
|
-
resolved.origin !== base.origin ||
|
|
1694
|
-
resolved.protocol !== 'https:' ||
|
|
1695
|
-
resolved.username ||
|
|
1696
|
-
resolved.password ||
|
|
1697
|
-
resolved.search ||
|
|
1698
|
-
resolved.hash ||
|
|
1699
|
-
resolved.pathname !== `/${link.sitePath}`
|
|
1700
|
-
) {
|
|
1701
|
-
add(
|
|
1702
|
-
errors,
|
|
1703
|
-
`$.artifactLinks[${index}].href`,
|
|
1704
|
-
'artifact-link-resolution',
|
|
1705
|
-
'Canonical artifact href must resolve exactly to its declared site path.',
|
|
1706
|
-
);
|
|
1707
|
-
}
|
|
1708
|
-
}
|
|
1709
|
-
}
|
|
1710
|
-
|
|
1711
|
-
function validateVisualAuthoringGuidance(value, errors) {
|
|
1712
|
-
if (typeof value.visualAuthoringGuidance !== 'string') {
|
|
1713
|
-
return;
|
|
1714
|
-
}
|
|
1715
|
-
const guidance = value.visualAuthoringGuidance.toLowerCase();
|
|
1716
|
-
const missing = [
|
|
1717
|
-
'representation',
|
|
1718
|
-
'hierarchy',
|
|
1719
|
-
'responsive navigation',
|
|
1720
|
-
'table',
|
|
1721
|
-
'diagram',
|
|
1722
|
-
'deck',
|
|
1723
|
-
].filter((topic) => !guidance.includes(topic));
|
|
1724
|
-
if (missing.length > 0) {
|
|
1725
|
-
add(
|
|
1726
|
-
errors,
|
|
1727
|
-
'$.visualAuthoringGuidance',
|
|
1728
|
-
'malformed-authoring-guidance',
|
|
1729
|
-
`Visual authoring guidance is missing bundled topics: ${missing.join(', ')}.`,
|
|
1730
|
-
);
|
|
1731
|
-
}
|
|
1732
|
-
}
|
|
1733
|
-
|
|
1734
|
-
function validateAuthorGraphSemantics(value, errors) {
|
|
1735
|
-
if (value.graphSemantics === undefined) {
|
|
1736
|
-
return;
|
|
1737
|
-
}
|
|
1738
|
-
if (value.authoring !== 'html' || !Array.isArray(value.graphSemantics)) {
|
|
1739
|
-
add(
|
|
1740
|
-
errors,
|
|
1741
|
-
'$.graphSemantics',
|
|
1742
|
-
'graph-semantics',
|
|
1743
|
-
'Planner-owned graph semantics are allowed only for artistic HTML authoring.',
|
|
1744
|
-
);
|
|
1745
|
-
return;
|
|
1746
|
-
}
|
|
1747
|
-
for (const [graphIndex, graph] of value.graphSemantics.entries()) {
|
|
1748
|
-
if (!isObject(graph)) continue;
|
|
1749
|
-
const nodes = Array.isArray(graph.nodes) ? graph.nodes : [];
|
|
1750
|
-
const edges = Array.isArray(graph.edges) ? graph.edges : [];
|
|
1751
|
-
const nodeIds = nodes.map(({ id }) => id);
|
|
1752
|
-
const edgeIds = edges.map(({ from, to }) => `${from}\0${to}`);
|
|
1753
|
-
if (
|
|
1754
|
-
new Set(nodeIds).size !== nodeIds.length ||
|
|
1755
|
-
new Set(edgeIds).size !== edgeIds.length ||
|
|
1756
|
-
edges.some(
|
|
1757
|
-
({ from, to }) => !nodeIds.includes(from) || !nodeIds.includes(to),
|
|
1758
|
-
)
|
|
1759
|
-
) {
|
|
1760
|
-
add(
|
|
1761
|
-
errors,
|
|
1762
|
-
`$.graphSemantics[${graphIndex}]`,
|
|
1763
|
-
'graph-semantics',
|
|
1764
|
-
'Graph semantics require unique nodes and edges with declared endpoints.',
|
|
1765
|
-
);
|
|
1766
|
-
}
|
|
1767
|
-
}
|
|
1768
|
-
}
|
|
1769
|
-
|
|
1770
|
-
function findRawSecrets(value, path, errors) {
|
|
1771
|
-
if (Array.isArray(value)) {
|
|
1772
|
-
value.forEach((item, index) =>
|
|
1773
|
-
findRawSecrets(item, `${path}[${index}]`, errors),
|
|
1774
|
-
);
|
|
1775
|
-
return;
|
|
1776
|
-
}
|
|
1777
|
-
if (!isObject(value)) {
|
|
381
|
+
function findRawSecrets(value, path, errors) {
|
|
382
|
+
if (Array.isArray(value)) {
|
|
383
|
+
value.forEach((item, index) =>
|
|
384
|
+
findRawSecrets(item, `${path}[${index}]`, errors),
|
|
385
|
+
);
|
|
1778
386
|
return;
|
|
1779
387
|
}
|
|
388
|
+
if (!isObject(value)) return;
|
|
1780
389
|
for (const [key, child] of Object.entries(value)) {
|
|
1781
390
|
const normalized = key.toLowerCase().replaceAll(/[^a-z0-9]/g, '');
|
|
1782
391
|
if (RAW_SECRET_KEYS.has(normalized)) {
|