@open-agent-toolkit/cli 0.2.26 → 0.2.27
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/NOTICES.md +156 -0
- package/assets/docs/contributing/explainer-kit-verification.md +125 -0
- package/assets/docs/contributing/index.md +1 -0
- package/assets/docs/reference/troubleshooting.md +47 -0
- package/assets/docs/workflows/projects/artifacts.md +24 -6
- package/assets/docs/workflows/skills/explainer-kit-providers.md +144 -0
- package/assets/docs/workflows/skills/explainer-kit.md +121 -69
- package/assets/docs/workflows/skills/index.md +1 -0
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/explainer-kit/SKILL.md +18 -3
- package/assets/skills/explainer-kit/recipes/project-recap.json +43 -16
- package/assets/skills/explainer-kit/references/contracts.md +167 -20
- package/assets/skills/explainer-kit/references/golden-conformance.md +80 -0
- package/assets/skills/explainer-kit/references/visual-authoring.md +92 -0
- package/assets/skills/explainer-kit/references/visual-review.md +57 -0
- package/assets/skills/explainer-kit/schemas/author-request.v2.schema.json +172 -1
- package/assets/skills/explainer-kit/schemas/build-record.schema.json +7 -1
- package/assets/skills/explainer-kit/schemas/fact-base.schema.json +38 -2
- package/assets/skills/explainer-kit/schemas/manifest.schema.json +25 -1
- package/assets/skills/explainer-kit/schemas/run-request.schema.json +4 -0
- package/assets/skills/explainer-kit/schemas/set-plan.v1.schema.json +149 -0
- package/assets/skills/explainer-kit/schemas/visual-review-request.v1.schema.json +117 -0
- package/assets/skills/explainer-kit/schemas/visual-review-result.v1.schema.json +80 -0
- package/assets/skills/explainer-kit/scripts/lib/browser-runtime.mjs +148 -4
- package/assets/skills/explainer-kit/scripts/lib/catalog.mjs +243 -0
- package/assets/skills/explainer-kit/scripts/lib/contracts.mjs +586 -8
- package/assets/skills/explainer-kit/scripts/lib/diagram.mjs +285 -8
- package/assets/skills/explainer-kit/scripts/lib/durability.mjs +35 -0
- package/assets/skills/explainer-kit/scripts/lib/fact-base.mjs +144 -8
- package/assets/skills/explainer-kit/scripts/lib/package-coverage.mjs +379 -0
- package/assets/skills/explainer-kit/scripts/lib/png.mjs +287 -0
- package/assets/skills/explainer-kit/scripts/lib/qa.mjs +280 -8
- package/assets/skills/explainer-kit/scripts/lib/recipes.mjs +132 -3
- package/assets/skills/explainer-kit/scripts/lib/records.mjs +513 -21
- package/assets/skills/explainer-kit/scripts/lib/render.mjs +67 -3
- package/assets/skills/explainer-kit/scripts/lib/s3-static.mjs +43 -1
- package/assets/skills/explainer-kit/scripts/lib/set-plan.mjs +208 -0
- package/assets/skills/explainer-kit/scripts/lib/source-backlinks.mjs +218 -0
- package/assets/skills/explainer-kit/scripts/lib/visual-review.mjs +380 -0
- package/assets/skills/explainer-kit/scripts/render-qa.mjs +48 -10
- package/assets/skills/explainer-kit/scripts/run.mjs +859 -134
- package/assets/skills/oat-explainer-kit/SKILL.md +40 -12
- package/assets/skills/oat-explainer-kit/references/author-callback.md +12 -10
- package/assets/skills/oat-explainer-kit/references/lifecycle-contract.md +40 -2
- package/assets/skills/oat-explainer-kit/references/visual-review-callback.md +72 -0
- package/assets/skills/oat-explainer-kit/scripts/bind-project-sources.mjs +167 -5
- package/assets/skills/oat-explainer-kit/scripts/finalize-tracked-run.mjs +92 -5
- package/assets/skills/oat-explainer-kit/scripts/run.mjs +324 -2
- package/dist/commands/project/archive/archive-utils.d.ts +1 -0
- package/dist/commands/project/archive/archive-utils.d.ts.map +1 -1
- package/dist/commands/project/archive/archive-utils.js +109 -42
- package/dist/commands/project/archive/explainer-package-coverage.d.ts +14 -0
- package/dist/commands/project/archive/explainer-package-coverage.d.ts.map +1 -0
- package/dist/commands/project/archive/explainer-package-coverage.js +27 -0
- package/dist/commands/project/archive/explainer-source-backlinks.d.ts +18 -0
- package/dist/commands/project/archive/explainer-source-backlinks.d.ts.map +1 -0
- package/dist/commands/project/archive/explainer-source-backlinks.js +27 -0
- package/dist/commands/project/archive/push-runner.d.ts +2 -1
- package/dist/commands/project/archive/push-runner.d.ts.map +1 -1
- package/dist/commands/project/archive/push-runner.js +5 -1
- package/dist/release/public-package-contract.d.ts +6 -0
- package/dist/release/public-package-contract.d.ts.map +1 -1
- package/dist/release/public-package-contract.js +75 -0
- package/package.json +2 -2
|
@@ -2,6 +2,10 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
3
|
|
|
4
4
|
import { validatePortablePath } from './safe-paths.mjs';
|
|
5
|
+
import {
|
|
6
|
+
parseCanonicalGithubBlobUrl,
|
|
7
|
+
validateCanonicalGithubBlobTuple,
|
|
8
|
+
} from './source-backlinks.mjs';
|
|
5
9
|
|
|
6
10
|
const SCHEMA_FILES = {
|
|
7
11
|
'run-request': 'run-request.schema.json',
|
|
@@ -14,6 +18,9 @@ const SCHEMA_FILES = {
|
|
|
14
18
|
'publish-receipt': 'publish-receipt.schema.json',
|
|
15
19
|
'author-request/v2': 'author-request.v2.schema.json',
|
|
16
20
|
'author-result/v2': 'author-result.v2.schema.json',
|
|
21
|
+
'set-plan': 'set-plan.v1.schema.json',
|
|
22
|
+
'visual-review-request': 'visual-review-request.v1.schema.json',
|
|
23
|
+
'visual-review-result': 'visual-review-result.v1.schema.json',
|
|
17
24
|
};
|
|
18
25
|
const DEFAULT_SCHEMA_KEYS = {
|
|
19
26
|
'author-request': 'author-request/v2',
|
|
@@ -47,6 +54,13 @@ const RAW_SECRET_KEYS = new Set([
|
|
|
47
54
|
]);
|
|
48
55
|
const DATE_TIME_PATTERN =
|
|
49
56
|
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
57
|
+
const SET_PLAN_RECORD_PATHS = [
|
|
58
|
+
'source/set-plan/request.json',
|
|
59
|
+
'source/set-plan/result.json',
|
|
60
|
+
'source/set-plan/ledger.json',
|
|
61
|
+
'source/set-plan/portfolio.json',
|
|
62
|
+
'source/set-plan/drafts.json',
|
|
63
|
+
];
|
|
50
64
|
|
|
51
65
|
export function validateContract(kind, value, context = {}) {
|
|
52
66
|
const schema = resolveContractSchema(kind, value);
|
|
@@ -68,9 +82,51 @@ export function validateContract(kind, value, context = {}) {
|
|
|
68
82
|
validateSchema(schema, value, '$', schema, errors);
|
|
69
83
|
validateContractPaths(kind, value, errors);
|
|
70
84
|
validateCrossRecord(kind, value, context, errors);
|
|
85
|
+
validateSourceBacklinks(kind, value, errors);
|
|
71
86
|
return { valid: errors.length === 0, errors };
|
|
72
87
|
}
|
|
73
88
|
|
|
89
|
+
function validateSourceBacklinks(kind, value, errors) {
|
|
90
|
+
if (kind === 'fact-base' || kind === 'explainer-kit.fact-base/v1') {
|
|
91
|
+
const tuples = [
|
|
92
|
+
...(value.sources ?? []),
|
|
93
|
+
...(value.claims ?? []).flatMap((claim) => claim.citations ?? []),
|
|
94
|
+
...(value.unresolvedClaims ?? []).flatMap(
|
|
95
|
+
(claim) => claim.citations ?? [],
|
|
96
|
+
),
|
|
97
|
+
];
|
|
98
|
+
tuples.forEach((tuple, index) => {
|
|
99
|
+
const declaresBacklink = ['repository', 'path', 'lineRange', 'url'].some(
|
|
100
|
+
(field) => tuple[field] !== undefined,
|
|
101
|
+
);
|
|
102
|
+
if (declaresBacklink && !validateCanonicalGithubBlobTuple(tuple)) {
|
|
103
|
+
add(
|
|
104
|
+
errors,
|
|
105
|
+
`$.sourceBacklinks[${index}]`,
|
|
106
|
+
'source-backlink',
|
|
107
|
+
'Must declare one complete canonical GitHub blob backlink tuple.',
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (kind !== 'manifest' && kind !== 'explainer-kit.manifest/v1') return;
|
|
114
|
+
(value.source?.backlinks ?? []).forEach((entry, index) => {
|
|
115
|
+
try {
|
|
116
|
+
parseCanonicalGithubBlobUrl(entry.url);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
add(
|
|
119
|
+
errors,
|
|
120
|
+
`$.source.backlinks[${index}].url`,
|
|
121
|
+
'source-backlink',
|
|
122
|
+
error instanceof Error
|
|
123
|
+
? error.message
|
|
124
|
+
: 'Must be a canonical GitHub blob backlink.',
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
74
130
|
function resolveContractSchema(kind, value) {
|
|
75
131
|
if (SCHEMAS[kind]) {
|
|
76
132
|
return SCHEMAS[kind];
|
|
@@ -475,6 +531,260 @@ function validateCrossRecord(kind, value, context, errors) {
|
|
|
475
531
|
'Retaining raw art direction requires theme.artDirection.',
|
|
476
532
|
);
|
|
477
533
|
}
|
|
534
|
+
|
|
535
|
+
if (value.recapMode !== undefined && value.recipe?.id !== 'project-recap') {
|
|
536
|
+
add(
|
|
537
|
+
errors,
|
|
538
|
+
'$.recapMode',
|
|
539
|
+
'recap-mode-recipe',
|
|
540
|
+
'recapMode is allowed only for the project-recap recipe.',
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (kind === 'set-plan') {
|
|
546
|
+
validateSetPlan(value, errors);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (
|
|
550
|
+
['author-request', 'author-request/v2'].includes(kind) ||
|
|
551
|
+
value.schemaVersion === 'explainer-kit.author-request/v2'
|
|
552
|
+
) {
|
|
553
|
+
validateAuthorSetContext(value, errors);
|
|
554
|
+
validateVisualAuthoringGuidance(value, errors);
|
|
555
|
+
validateAuthorGraphSemantics(value, errors);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (kind === 'visual-review-request') {
|
|
559
|
+
if (
|
|
560
|
+
typeof value.requestHash === 'string' &&
|
|
561
|
+
value.requestHash !== canonicalHash(visualReviewRequestPayload(value))
|
|
562
|
+
) {
|
|
563
|
+
add(
|
|
564
|
+
errors,
|
|
565
|
+
'$.requestHash',
|
|
566
|
+
'request-hash-mismatch',
|
|
567
|
+
'Visual review request hash does not match its canonical evidence payload.',
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
if (
|
|
571
|
+
typeof value.requestHash === 'string' &&
|
|
572
|
+
typeof value.requestId === 'string' &&
|
|
573
|
+
value.requestId !== visualReviewRequestId(value.requestHash)
|
|
574
|
+
) {
|
|
575
|
+
add(
|
|
576
|
+
errors,
|
|
577
|
+
'$.requestId',
|
|
578
|
+
'request-id-mismatch',
|
|
579
|
+
'Visual review request identity does not match its canonical request hash.',
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
const plannedArtifactIds = Array.isArray(value.plan?.portfolio)
|
|
583
|
+
? value.plan.portfolio.map(({ artifactId }) => artifactId)
|
|
584
|
+
: [];
|
|
585
|
+
const plannedIds = new Set(plannedArtifactIds);
|
|
586
|
+
const renderedIds = new Set();
|
|
587
|
+
const requiresObservedCohesion = value.plan?.recipe?.id === 'project-recap';
|
|
588
|
+
const expectedCohesion = expectedLedgerClaims(value.plan?.ledger);
|
|
589
|
+
const observedCohesion = new Set();
|
|
590
|
+
if (
|
|
591
|
+
requiresObservedCohesion &&
|
|
592
|
+
['terminology', 'statuses', 'numericClaims'].some(
|
|
593
|
+
(group) => expectedCohesion[group].size === 0,
|
|
594
|
+
)
|
|
595
|
+
) {
|
|
596
|
+
add(
|
|
597
|
+
errors,
|
|
598
|
+
'$.plan.ledger',
|
|
599
|
+
'cohesion-ledger-empty',
|
|
600
|
+
'Adaptive recap review requires non-empty terminology, status, and numeric ledger entries.',
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
for (const [index, artifact] of (Array.isArray(value.renderedArtifacts)
|
|
604
|
+
? value.renderedArtifacts
|
|
605
|
+
: []
|
|
606
|
+
).entries()) {
|
|
607
|
+
for (const [evidenceIndex, evidence] of (Array.isArray(artifact?.evidence)
|
|
608
|
+
? artifact.evidence
|
|
609
|
+
: []
|
|
610
|
+
).entries()) {
|
|
611
|
+
if (evidence?.captureIdentity !== value.captureIdentity) {
|
|
612
|
+
add(
|
|
613
|
+
errors,
|
|
614
|
+
`$.renderedArtifacts[${index}].evidence[${evidenceIndex}].captureIdentity`,
|
|
615
|
+
'browser-runtime-mismatch',
|
|
616
|
+
'Every visual-review evidence record must bind the request browser capture identity.',
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
if (!plannedIds.has(artifact?.artifactId)) {
|
|
621
|
+
add(
|
|
622
|
+
errors,
|
|
623
|
+
`$.renderedArtifacts[${index}].artifactId`,
|
|
624
|
+
'unknown-artifact',
|
|
625
|
+
'Rendered artifact is not present in the shared set plan.',
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
if (renderedIds.has(artifact?.artifactId)) {
|
|
629
|
+
add(
|
|
630
|
+
errors,
|
|
631
|
+
`$.renderedArtifacts[${index}].artifactId`,
|
|
632
|
+
'duplicate-artifact',
|
|
633
|
+
'Rendered artifact IDs must be unique.',
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
renderedIds.add(artifact?.artifactId);
|
|
637
|
+
const observations = Array.isArray(artifact?.cohesionObservations)
|
|
638
|
+
? artifact.cohesionObservations
|
|
639
|
+
: [];
|
|
640
|
+
if (requiresObservedCohesion && observations.length === 0) {
|
|
641
|
+
add(
|
|
642
|
+
errors,
|
|
643
|
+
`$.renderedArtifacts[${index}].cohesionObservations`,
|
|
644
|
+
'cohesion-observations-empty',
|
|
645
|
+
'Every adaptive recap artifact must expose observed shared-ledger evidence.',
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
for (const [observationIndex, observation] of observations.entries()) {
|
|
649
|
+
if (
|
|
650
|
+
observation?.artifactId !== artifact?.artifactId ||
|
|
651
|
+
observation?.contentHash !== artifact?.renderedHash
|
|
652
|
+
) {
|
|
653
|
+
add(
|
|
654
|
+
errors,
|
|
655
|
+
`$.renderedArtifacts[${index}].cohesionObservations[${observationIndex}]`,
|
|
656
|
+
'cohesion-binding-mismatch',
|
|
657
|
+
'Cohesion observations must bind to their artifact and exact rendered content hash.',
|
|
658
|
+
);
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
const expected = expectedCohesion[observation.group]?.get(
|
|
662
|
+
observation.claim,
|
|
663
|
+
);
|
|
664
|
+
if (
|
|
665
|
+
expected === undefined ||
|
|
666
|
+
normalizeComparable(expected) !==
|
|
667
|
+
normalizeComparable(observation.value)
|
|
668
|
+
) {
|
|
669
|
+
add(
|
|
670
|
+
errors,
|
|
671
|
+
`$.renderedArtifacts[${index}].cohesionObservations[${observationIndex}]`,
|
|
672
|
+
'cohesion-contradiction',
|
|
673
|
+
'Observed cohesion evidence must match an applicable shared-ledger value.',
|
|
674
|
+
);
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
observedCohesion.add(`${observation.group}:${observation.claim}`);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
for (const artifactId of plannedArtifactIds) {
|
|
681
|
+
if (!renderedIds.has(artifactId)) {
|
|
682
|
+
add(
|
|
683
|
+
errors,
|
|
684
|
+
'$.renderedArtifacts',
|
|
685
|
+
'missing-artifact',
|
|
686
|
+
`Rendered review set is missing planned artifact ${artifactId}.`,
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
if (requiresObservedCohesion) {
|
|
691
|
+
for (const [group, claims] of Object.entries(expectedCohesion)) {
|
|
692
|
+
for (const claim of claims.keys()) {
|
|
693
|
+
if (!observedCohesion.has(`${group}:${claim}`)) {
|
|
694
|
+
add(
|
|
695
|
+
errors,
|
|
696
|
+
'$.renderedArtifacts',
|
|
697
|
+
'cohesion-claim-unobserved',
|
|
698
|
+
`Shared-ledger claim ${group}.${claim} is not observable in the rendered set.`,
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
if (kind === 'visual-review-result') {
|
|
707
|
+
const reviewRequest = context.visualReviewRequest;
|
|
708
|
+
if (!isObject(reviewRequest)) {
|
|
709
|
+
add(
|
|
710
|
+
errors,
|
|
711
|
+
'$',
|
|
712
|
+
'review-request-required',
|
|
713
|
+
'Visual review results must be validated with their reviewed request.',
|
|
714
|
+
);
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const requestValidation = validateContract(
|
|
719
|
+
'visual-review-request',
|
|
720
|
+
reviewRequest,
|
|
721
|
+
);
|
|
722
|
+
if (!requestValidation.valid) {
|
|
723
|
+
add(
|
|
724
|
+
errors,
|
|
725
|
+
'$',
|
|
726
|
+
'invalid-review-request',
|
|
727
|
+
'Visual review result cannot bind to an invalid reviewed request.',
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
if (
|
|
731
|
+
value.requestId !== reviewRequest.requestId ||
|
|
732
|
+
value.requestHash !== reviewRequest.requestHash
|
|
733
|
+
) {
|
|
734
|
+
add(
|
|
735
|
+
errors,
|
|
736
|
+
'$.requestHash',
|
|
737
|
+
'review-binding-mismatch',
|
|
738
|
+
'Visual review result must echo the exact reviewed request identity and hash.',
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const reviewedArtifactIds = Array.isArray(reviewRequest.renderedArtifacts)
|
|
743
|
+
? reviewRequest.renderedArtifacts.map(({ artifactId }) => artifactId)
|
|
744
|
+
: [];
|
|
745
|
+
const reviewedIds = new Set(reviewedArtifactIds);
|
|
746
|
+
const resultArtifactIds = Array.isArray(value.artifactIds)
|
|
747
|
+
? value.artifactIds
|
|
748
|
+
: [];
|
|
749
|
+
const resultIds = new Set(resultArtifactIds);
|
|
750
|
+
if (
|
|
751
|
+
resultIds.size !== reviewedIds.size ||
|
|
752
|
+
reviewedArtifactIds.some((artifactId) => !resultIds.has(artifactId))
|
|
753
|
+
) {
|
|
754
|
+
add(
|
|
755
|
+
errors,
|
|
756
|
+
'$.artifactIds',
|
|
757
|
+
'review-set-mismatch',
|
|
758
|
+
'Visual review result artifact IDs must equal the complete reviewed request set.',
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
for (const [index, finding] of (Array.isArray(value.findings)
|
|
762
|
+
? value.findings
|
|
763
|
+
: []
|
|
764
|
+
).entries()) {
|
|
765
|
+
if (!reviewedIds.has(finding?.artifactId)) {
|
|
766
|
+
add(
|
|
767
|
+
errors,
|
|
768
|
+
`$.findings[${index}].artifactId`,
|
|
769
|
+
'detached-finding',
|
|
770
|
+
'Visual review findings must reference an artifact in the reviewed request.',
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
const findingCount = Array.isArray(value.findings)
|
|
775
|
+
? value.findings.length
|
|
776
|
+
: 0;
|
|
777
|
+
if (
|
|
778
|
+
(value.disposition === 'pass' && findingCount > 0) ||
|
|
779
|
+
(['correct', 'fail'].includes(value.disposition) && findingCount === 0)
|
|
780
|
+
) {
|
|
781
|
+
add(
|
|
782
|
+
errors,
|
|
783
|
+
'$.disposition',
|
|
784
|
+
'disposition-findings-mismatch',
|
|
785
|
+
'Pass requires no findings; correct and fail require at least one correction finding.',
|
|
786
|
+
);
|
|
787
|
+
}
|
|
478
788
|
}
|
|
479
789
|
|
|
480
790
|
if (kind === 'manifest') {
|
|
@@ -533,10 +843,17 @@ function validateCrossRecord(kind, value, context, errors) {
|
|
|
533
843
|
'run-request.json',
|
|
534
844
|
'source/content-approval.json',
|
|
535
845
|
];
|
|
846
|
+
const recordedImmutable = isObject(value.immutableHashes)
|
|
847
|
+
? new Set(Object.keys(value.immutableHashes))
|
|
848
|
+
: new Set();
|
|
849
|
+
const retainsSetPlan =
|
|
850
|
+
value.recipe?.id === 'project-recap' ||
|
|
851
|
+
SET_PLAN_RECORD_PATHS.some((path) => recordedImmutable.has(path));
|
|
536
852
|
const expectedImmutable = new Set([
|
|
537
853
|
...requiredProvenance,
|
|
538
854
|
value.source?.factBasePath,
|
|
539
855
|
'source/fact-base.md',
|
|
856
|
+
...(retainsSetPlan ? SET_PLAN_RECORD_PATHS : []),
|
|
540
857
|
...(Array.isArray(value.source?.authorResultPaths)
|
|
541
858
|
? value.source.authorResultPaths
|
|
542
859
|
: []),
|
|
@@ -552,9 +869,6 @@ function validateCrossRecord(kind, value, context, errors) {
|
|
|
552
869
|
: []),
|
|
553
870
|
]);
|
|
554
871
|
expectedImmutable.delete(undefined);
|
|
555
|
-
const recordedImmutable = isObject(value.immutableHashes)
|
|
556
|
-
? new Set(Object.keys(value.immutableHashes))
|
|
557
|
-
: new Set();
|
|
558
872
|
const missingLegacyPaths = requiredProvenance.filter(
|
|
559
873
|
(path) => !recordedImmutable.has(path),
|
|
560
874
|
);
|
|
@@ -566,17 +880,46 @@ function validateCrossRecord(kind, value, context, errors) {
|
|
|
566
880
|
`Legacy manifest is missing immutable coverage for ${missingLegacyPaths.join(', ')}; regenerate the recap package before archival.`,
|
|
567
881
|
);
|
|
568
882
|
}
|
|
883
|
+
if ([...expectedImmutable].some((path) => !recordedImmutable.has(path))) {
|
|
884
|
+
add(
|
|
885
|
+
errors,
|
|
886
|
+
'$.immutableHashes',
|
|
887
|
+
'immutable-package-incomplete',
|
|
888
|
+
'Manifest immutable hashes must cover the complete retained fact-base, set plan, content, theme, and required built artifact package.',
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
const hasVisualEvidence = [...recordedImmutable].some((path) =>
|
|
892
|
+
path.startsWith('qa/visual-review/'),
|
|
893
|
+
);
|
|
569
894
|
if (
|
|
570
|
-
|
|
571
|
-
[
|
|
895
|
+
hasVisualEvidence &&
|
|
896
|
+
[
|
|
897
|
+
'qa/visual-review/attempt-1/request.json',
|
|
898
|
+
'qa/visual-review/attempt-1/result.json',
|
|
899
|
+
].some((path) => !recordedImmutable.has(path))
|
|
572
900
|
) {
|
|
573
901
|
add(
|
|
574
902
|
errors,
|
|
575
903
|
'$.immutableHashes',
|
|
576
|
-
'
|
|
577
|
-
'
|
|
904
|
+
'visual-review-chain-incomplete',
|
|
905
|
+
'Retained visual review evidence requires its bound attempt request and result.',
|
|
578
906
|
);
|
|
579
907
|
}
|
|
908
|
+
for (const path of recordedImmutable) {
|
|
909
|
+
if (
|
|
910
|
+
(path.startsWith('qa/browser/') ||
|
|
911
|
+
path.includes('/visual-review/attempt-')) &&
|
|
912
|
+
path.endsWith('.png') &&
|
|
913
|
+
!recordedImmutable.has(path.replace(/\.png$/, '.json'))
|
|
914
|
+
) {
|
|
915
|
+
add(
|
|
916
|
+
errors,
|
|
917
|
+
'$.immutableHashes',
|
|
918
|
+
'visual-review-chain-incomplete',
|
|
919
|
+
`Screenshot evidence ${path} is missing its immutable metrics record.`,
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
580
923
|
|
|
581
924
|
const record = context.buildRecord;
|
|
582
925
|
if (isObject(record)) {
|
|
@@ -636,10 +979,24 @@ function validateCrossRecord(kind, value, context, errors) {
|
|
|
636
979
|
) {
|
|
637
980
|
const expected = new Map();
|
|
638
981
|
for (const artifact of context.manifest.artifacts ?? []) {
|
|
639
|
-
if (
|
|
982
|
+
if (
|
|
983
|
+
isObject(artifact) &&
|
|
984
|
+
artifact.status === 'built' &&
|
|
985
|
+
typeof artifact.renderedPath === 'string'
|
|
986
|
+
) {
|
|
640
987
|
expected.set(artifact.renderedPath, artifact.hash);
|
|
641
988
|
}
|
|
642
989
|
}
|
|
990
|
+
if (
|
|
991
|
+
isObject(context.catalogArtifact) &&
|
|
992
|
+
typeof context.catalogArtifact.relativePath === 'string'
|
|
993
|
+
) {
|
|
994
|
+
expected.set(
|
|
995
|
+
context.catalogArtifact.relativePath,
|
|
996
|
+
context.catalogArtifact.hash,
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
const received = new Set();
|
|
643
1000
|
for (const artifact of value.artifacts) {
|
|
644
1001
|
if (
|
|
645
1002
|
isObject(artifact) &&
|
|
@@ -652,6 +1009,227 @@ function validateCrossRecord(kind, value, context, errors) {
|
|
|
652
1009
|
'Publish receipt artifact does not match the manifest.',
|
|
653
1010
|
);
|
|
654
1011
|
}
|
|
1012
|
+
if (received.has(artifact?.relativePath)) {
|
|
1013
|
+
add(
|
|
1014
|
+
errors,
|
|
1015
|
+
'$.artifacts',
|
|
1016
|
+
'receipt-artifact-parity',
|
|
1017
|
+
'Publish receipt artifact paths must be unique.',
|
|
1018
|
+
);
|
|
1019
|
+
}
|
|
1020
|
+
received.add(artifact?.relativePath);
|
|
1021
|
+
}
|
|
1022
|
+
if (
|
|
1023
|
+
received.size !== expected.size ||
|
|
1024
|
+
[...expected.keys()].some((path) => !received.has(path))
|
|
1025
|
+
) {
|
|
1026
|
+
add(
|
|
1027
|
+
errors,
|
|
1028
|
+
'$.artifacts',
|
|
1029
|
+
'receipt-artifact-parity',
|
|
1030
|
+
'Publish receipt must exactly cover every manifest artifact and the generated catalog.',
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
export function visualReviewRequestPayload(request) {
|
|
1037
|
+
const {
|
|
1038
|
+
requestId: _requestId,
|
|
1039
|
+
requestHash: _requestHash,
|
|
1040
|
+
...payload
|
|
1041
|
+
} = request;
|
|
1042
|
+
return payload;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
export function visualReviewRequestId(requestHash) {
|
|
1046
|
+
return `visual-review-${String(requestHash).replace(/^sha256:/, '')}`;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
function expectedLedgerClaims(ledger) {
|
|
1050
|
+
return {
|
|
1051
|
+
terminology: new Map(
|
|
1052
|
+
(ledger?.terminology ?? []).map(({ term }) => [term, term]),
|
|
1053
|
+
),
|
|
1054
|
+
statuses: new Map(
|
|
1055
|
+
(ledger?.statuses ?? []).map(({ subject, value }) => [subject, value]),
|
|
1056
|
+
),
|
|
1057
|
+
numericClaims: new Map(
|
|
1058
|
+
(ledger?.numbers ?? []).map(({ subject, value }) => [subject, value]),
|
|
1059
|
+
),
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
function normalizeComparable(value) {
|
|
1064
|
+
return String(value).trim().toLocaleLowerCase();
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
function validateSetPlan(value, errors) {
|
|
1068
|
+
const sourceIds = new Set(
|
|
1069
|
+
Array.isArray(value.sourceIds) ? value.sourceIds : [],
|
|
1070
|
+
);
|
|
1071
|
+
const artifactIds = new Set();
|
|
1072
|
+
for (const [index, artifact] of (Array.isArray(value.portfolio)
|
|
1073
|
+
? value.portfolio
|
|
1074
|
+
: []
|
|
1075
|
+
).entries()) {
|
|
1076
|
+
if (artifactIds.has(artifact?.artifactId)) {
|
|
1077
|
+
add(
|
|
1078
|
+
errors,
|
|
1079
|
+
`$.portfolio[${index}].artifactId`,
|
|
1080
|
+
'duplicate-artifact',
|
|
1081
|
+
'Set-plan artifact IDs must be unique.',
|
|
1082
|
+
);
|
|
1083
|
+
}
|
|
1084
|
+
artifactIds.add(artifact?.artifactId);
|
|
1085
|
+
for (const sourceId of Array.isArray(artifact?.sourceIds)
|
|
1086
|
+
? artifact.sourceIds
|
|
1087
|
+
: []) {
|
|
1088
|
+
if (!sourceIds.has(sourceId)) {
|
|
1089
|
+
add(
|
|
1090
|
+
errors,
|
|
1091
|
+
`$.portfolio[${index}].sourceIds`,
|
|
1092
|
+
'unknown-source',
|
|
1093
|
+
`Artifact source ${sourceId} is not declared by the set plan.`,
|
|
1094
|
+
);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
if (artifact?.required === false && !isObject(artifact.justification)) {
|
|
1098
|
+
add(
|
|
1099
|
+
errors,
|
|
1100
|
+
`$.portfolio[${index}].justification`,
|
|
1101
|
+
'optional-justification-required',
|
|
1102
|
+
'Optional artifacts require a source-backed justification.',
|
|
1103
|
+
);
|
|
1104
|
+
}
|
|
1105
|
+
for (const sourceId of Array.isArray(artifact?.justification?.sourceIds)
|
|
1106
|
+
? artifact.justification.sourceIds
|
|
1107
|
+
: []) {
|
|
1108
|
+
if (!sourceIds.has(sourceId) || !artifact.sourceIds?.includes(sourceId)) {
|
|
1109
|
+
add(
|
|
1110
|
+
errors,
|
|
1111
|
+
`$.portfolio[${index}].justification.sourceIds`,
|
|
1112
|
+
'unknown-source',
|
|
1113
|
+
`Justification source ${sourceId} must be declared by the plan and artifact.`,
|
|
1114
|
+
);
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
for (const [field, identity] of [
|
|
1120
|
+
['terminology', (entry) => entry?.term],
|
|
1121
|
+
['statuses', (entry) => entry?.subject],
|
|
1122
|
+
['numbers', (entry) => entry?.subject],
|
|
1123
|
+
]) {
|
|
1124
|
+
const seen = new Map();
|
|
1125
|
+
for (const [index, entry] of (Array.isArray(value.ledger?.[field])
|
|
1126
|
+
? value.ledger[field]
|
|
1127
|
+
: []
|
|
1128
|
+
).entries()) {
|
|
1129
|
+
const key = identity(entry);
|
|
1130
|
+
if (
|
|
1131
|
+
seen.has(key) &&
|
|
1132
|
+
canonicalStringify(seen.get(key)) !== canonicalStringify(entry)
|
|
1133
|
+
) {
|
|
1134
|
+
add(
|
|
1135
|
+
errors,
|
|
1136
|
+
`$.ledger.${field}[${index}]`,
|
|
1137
|
+
'ledger-conflict',
|
|
1138
|
+
`Shared ledger contains conflicting values for ${key}.`,
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
seen.set(key, entry);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
function validateAuthorSetContext(value, errors) {
|
|
1147
|
+
if (!isObject(value.setContext) || !isObject(value.plannedArtifact)) {
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
const planned = Array.isArray(value.setContext.portfolio)
|
|
1151
|
+
? value.setContext.portfolio.find(
|
|
1152
|
+
({ artifactId }) => artifactId === value.artifactId,
|
|
1153
|
+
)
|
|
1154
|
+
: undefined;
|
|
1155
|
+
if (
|
|
1156
|
+
!planned ||
|
|
1157
|
+
value.plannedArtifact.artifactId !== value.artifactId ||
|
|
1158
|
+
value.plannedArtifact.artifactType !== value.artifactType
|
|
1159
|
+
) {
|
|
1160
|
+
add(
|
|
1161
|
+
errors,
|
|
1162
|
+
'$.plannedArtifact',
|
|
1163
|
+
'set-artifact-mismatch',
|
|
1164
|
+
'Author request identity must match one artifact in the shared set plan.',
|
|
1165
|
+
);
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
if (!deepEqual(planned, value.plannedArtifact)) {
|
|
1169
|
+
add(
|
|
1170
|
+
errors,
|
|
1171
|
+
'$.plannedArtifact',
|
|
1172
|
+
'set-plan-drift',
|
|
1173
|
+
'Author request planned artifact must be identical to the shared set plan entry.',
|
|
1174
|
+
);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
function validateVisualAuthoringGuidance(value, errors) {
|
|
1179
|
+
if (typeof value.visualAuthoringGuidance !== 'string') {
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1182
|
+
const guidance = value.visualAuthoringGuidance.toLowerCase();
|
|
1183
|
+
const missing = [
|
|
1184
|
+
'representation',
|
|
1185
|
+
'hierarchy',
|
|
1186
|
+
'responsive navigation',
|
|
1187
|
+
'table',
|
|
1188
|
+
'diagram',
|
|
1189
|
+
'deck',
|
|
1190
|
+
].filter((topic) => !guidance.includes(topic));
|
|
1191
|
+
if (missing.length > 0) {
|
|
1192
|
+
add(
|
|
1193
|
+
errors,
|
|
1194
|
+
'$.visualAuthoringGuidance',
|
|
1195
|
+
'malformed-authoring-guidance',
|
|
1196
|
+
`Visual authoring guidance is missing bundled topics: ${missing.join(', ')}.`,
|
|
1197
|
+
);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
function validateAuthorGraphSemantics(value, errors) {
|
|
1202
|
+
if (value.graphSemantics === undefined) {
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
if (value.authoring !== 'html' || !Array.isArray(value.graphSemantics)) {
|
|
1206
|
+
add(
|
|
1207
|
+
errors,
|
|
1208
|
+
'$.graphSemantics',
|
|
1209
|
+
'graph-semantics',
|
|
1210
|
+
'Planner-owned graph semantics are allowed only for artistic HTML authoring.',
|
|
1211
|
+
);
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
for (const [graphIndex, graph] of value.graphSemantics.entries()) {
|
|
1215
|
+
if (!isObject(graph)) continue;
|
|
1216
|
+
const nodes = Array.isArray(graph.nodes) ? graph.nodes : [];
|
|
1217
|
+
const edges = Array.isArray(graph.edges) ? graph.edges : [];
|
|
1218
|
+
const nodeIds = nodes.map(({ id }) => id);
|
|
1219
|
+
const edgeIds = edges.map(({ from, to }) => `${from}\0${to}`);
|
|
1220
|
+
if (
|
|
1221
|
+
new Set(nodeIds).size !== nodeIds.length ||
|
|
1222
|
+
new Set(edgeIds).size !== edgeIds.length ||
|
|
1223
|
+
edges.some(
|
|
1224
|
+
({ from, to }) => !nodeIds.includes(from) || !nodeIds.includes(to),
|
|
1225
|
+
)
|
|
1226
|
+
) {
|
|
1227
|
+
add(
|
|
1228
|
+
errors,
|
|
1229
|
+
`$.graphSemantics[${graphIndex}]`,
|
|
1230
|
+
'graph-semantics',
|
|
1231
|
+
'Graph semantics require unique nodes and edges with declared endpoints.',
|
|
1232
|
+
);
|
|
655
1233
|
}
|
|
656
1234
|
}
|
|
657
1235
|
}
|