project-tiny-context-harness 0.8.8 → 0.8.10
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/README.md +149 -123
- package/assets/README.md +279 -251
- package/assets/README.zh-CN.md +172 -144
- package/assets/skills/design-resource-authoring/SKILL.md +12 -12
- package/assets/skills/design-resource-authoring/references/downstream-handoff.md +57 -48
- package/dist/commands/design-resource.js +62 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lib/design-resource-fact-manifest-validation.d.ts +3 -2
- package/dist/lib/design-resource-fact-manifest-validation.js +23 -18
- package/dist/lib/design-resource-fact-universe-catalog.js +18 -8
- package/dist/lib/design-resource-fact-universe-helpers.js +9 -5
- package/dist/lib/design-resource-handoff-bundle.d.ts +31 -0
- package/dist/lib/design-resource-handoff-bundle.js +171 -0
- package/dist/lib/design-resource-handoff-file-validation.d.ts +1 -1
- package/dist/lib/design-resource-handoff-file-validation.js +1 -9
- package/dist/lib/design-resource-handoff-manifest-projection.d.ts +3 -0
- package/dist/lib/design-resource-handoff-manifest-projection.js +37 -0
- package/dist/lib/design-resource-handoff-parser.d.ts +7 -2
- package/dist/lib/design-resource-handoff-parser.js +32 -12
- package/dist/lib/design-resource-handoff-set-integrity.d.ts +8 -0
- package/dist/lib/design-resource-handoff-set-integrity.js +93 -0
- package/dist/lib/design-resource-handoff-shape.d.ts +2 -1
- package/dist/lib/design-resource-handoff-shape.js +62 -0
- package/dist/lib/design-resource-handoff-snapshot.d.ts +6 -0
- package/dist/lib/design-resource-handoff-snapshot.js +21 -0
- package/dist/lib/design-resource-handoff-types.d.ts +23 -1
- package/dist/lib/design-resource-handoff-validation.d.ts +2 -1
- package/dist/lib/design-resource-handoff-validation.js +45 -22
- package/dist/lib/long-task-design-resource-handoff.d.ts +15 -0
- package/dist/lib/long-task-design-resource-handoff.js +63 -173
- package/dist/lib/long-task-design-resource-method-binding.d.ts +8 -0
- package/dist/lib/long-task-design-resource-method-binding.js +135 -0
- package/dist/lib/long-task-source-item-parser.d.ts +8 -0
- package/dist/lib/long-task-source-item-parser.js +29 -107
- package/dist/lib/long-task-source-owned-sections.d.ts +29 -0
- package/dist/lib/long-task-source-owned-sections.js +90 -0
- package/dist/lib/source-line-scanner.d.ts +3 -0
- package/dist/lib/source-line-scanner.js +42 -0
- package/package.json +84 -84
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { lstat, mkdtemp, readFile, readdir, realpath, rename, rm, writeFile, } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { parseDesignResourceHandoffMarkdown } from "./design-resource-handoff-parser.js";
|
|
4
|
+
import { createDesignResourceHandoffSetIntegrity } from "./design-resource-handoff-set-integrity.js";
|
|
5
|
+
import { preflightParsedDesignResourceHandoff } from "./design-resource-handoff-validation.js";
|
|
6
|
+
import { normalizeRepositoryCwd, normalizeRepositoryFile, } from "./long-task-paths.js";
|
|
7
|
+
import { repoRelative, resolveInsideRepository, } from "./long-task-workspace.js";
|
|
8
|
+
import { sha256Hex } from "./strict-codec.js";
|
|
9
|
+
export async function publishDesignResourceHandoffBundle(options) {
|
|
10
|
+
const repository = path.resolve(options.repository);
|
|
11
|
+
const draftDirectory = normalizeBundleDirectory(options.draft_directory, "design_resource_bundle_draft_directory");
|
|
12
|
+
const outputDirectory = normalizeBundleDirectory(options.output_directory, "design_resource_bundle_output_directory");
|
|
13
|
+
if (!Number.isSafeInteger(options.max_handoff_bytes) ||
|
|
14
|
+
options.max_handoff_bytes <= 0)
|
|
15
|
+
invalid("max_handoff_bytes", String(options.max_handoff_bytes));
|
|
16
|
+
const manifestPaths = normalizeManifestPaths(options.manifest_paths);
|
|
17
|
+
const declaredManifestPaths = new Set(manifestPaths);
|
|
18
|
+
const draftAbsolute = await assertSafeDirectory(repository, draftDirectory, "draft_directory");
|
|
19
|
+
const outputAbsolute = resolveInsideRepository(repository, outputDirectory, "design_resource_bundle_output_directory");
|
|
20
|
+
if (await lstat(outputAbsolute).catch(() => null))
|
|
21
|
+
invalid("output_directory_exists", outputDirectory);
|
|
22
|
+
const outputParentRelative = path.posix.dirname(outputDirectory);
|
|
23
|
+
const outputParent = await assertSafeDirectory(repository, outputParentRelative === "." ? "." : outputParentRelative, "output_parent");
|
|
24
|
+
const temporary = await mkdtemp(path.join(outputParent, `.${path.basename(outputAbsolute)}.tmp-`));
|
|
25
|
+
try {
|
|
26
|
+
const draftEntries = (await readdir(draftAbsolute, {
|
|
27
|
+
withFileTypes: true,
|
|
28
|
+
})).sort((left, right) => compareText(left.name, right.name));
|
|
29
|
+
if (!draftEntries.length)
|
|
30
|
+
invalid("draft_directory_empty", draftDirectory);
|
|
31
|
+
for (const entry of draftEntries)
|
|
32
|
+
if (!entry.isFile() ||
|
|
33
|
+
entry.isSymbolicLink() ||
|
|
34
|
+
path.extname(entry.name).toLowerCase() !== ".md")
|
|
35
|
+
invalid("draft_entry_not_markdown_file", entry.name);
|
|
36
|
+
const handoffs = [];
|
|
37
|
+
const seenTargets = new Set();
|
|
38
|
+
const seenManifestPaths = new Set();
|
|
39
|
+
const manifests = new Map();
|
|
40
|
+
const handoffSetIntegrity = createDesignResourceHandoffSetIntegrity((code, detail) => invalid(code, detail));
|
|
41
|
+
for (const entry of draftEntries) {
|
|
42
|
+
const draftFile = path.join(draftAbsolute, entry.name);
|
|
43
|
+
const info = await lstat(draftFile);
|
|
44
|
+
if (typeof info.nlink === "number" && info.nlink > 1)
|
|
45
|
+
invalid("draft_hardlink_not_allowed", entry.name);
|
|
46
|
+
if (info.size > options.max_handoff_bytes)
|
|
47
|
+
invalid("handoff_byte_limit_exceeded", `${entry.name}:${info.size}:${options.max_handoff_bytes}`);
|
|
48
|
+
const bytes = await readFile(draftFile);
|
|
49
|
+
if (bytes.length > options.max_handoff_bytes)
|
|
50
|
+
invalid("handoff_byte_limit_exceeded", `${entry.name}:${bytes.length}:${options.max_handoff_bytes}`);
|
|
51
|
+
const content = bytes.toString("utf8");
|
|
52
|
+
if (!Buffer.from(content, "utf8").equals(bytes))
|
|
53
|
+
invalid("handoff_utf8_invalid", entry.name);
|
|
54
|
+
const stagedFile = path.join(temporary, entry.name);
|
|
55
|
+
await writeFile(stagedFile, bytes, { flag: "wx" });
|
|
56
|
+
const stagedPath = repoRelative(repository, stagedFile);
|
|
57
|
+
const parsed = parseDesignResourceHandoffMarkdown(stagedPath, content);
|
|
58
|
+
if (!("representation" in parsed.handoff))
|
|
59
|
+
invalid("manifest_backed_representation_required", entry.name);
|
|
60
|
+
if (parsed.handoff.targets.length !== 1)
|
|
61
|
+
invalid("one_target_per_handoff_required", `${entry.name}:${parsed.handoff.targets.length}`);
|
|
62
|
+
const declaredTarget = parsed.handoff.targets[0];
|
|
63
|
+
const manifestResource = parsed.handoff.resources.find((resource) => resource.key ===
|
|
64
|
+
declaredTarget.source_profile.fact_manifest_resource_ref);
|
|
65
|
+
if (!manifestResource)
|
|
66
|
+
invalid("target_manifest_resource_missing", declaredTarget.key);
|
|
67
|
+
if (!declaredManifestPaths.has(manifestResource.path))
|
|
68
|
+
invalid("target_manifest_not_declared", `${declaredTarget.key}:${manifestResource.path}`);
|
|
69
|
+
const preflight = await preflightParsedDesignResourceHandoff(repository, parsed);
|
|
70
|
+
handoffSetIntegrity.consume(preflight);
|
|
71
|
+
const targetKey = preflight.handoff.targets[0].key;
|
|
72
|
+
if (seenTargets.has(targetKey))
|
|
73
|
+
invalid("target_duplicate", targetKey);
|
|
74
|
+
seenTargets.add(targetKey);
|
|
75
|
+
const manifestIdentity = preflight.manifest_identities[0];
|
|
76
|
+
if (manifestIdentity.target_key !== targetKey ||
|
|
77
|
+
manifestIdentity.path !== manifestResource.path ||
|
|
78
|
+
manifestIdentity.sha256 !== manifestResource.sha256)
|
|
79
|
+
invalid("target_manifest_identity_mismatch", targetKey);
|
|
80
|
+
seenManifestPaths.add(manifestIdentity.path);
|
|
81
|
+
manifests.set(targetKey, {
|
|
82
|
+
path: manifestIdentity.path,
|
|
83
|
+
sha256: manifestIdentity.sha256,
|
|
84
|
+
scope_key: manifestIdentity.scope_key,
|
|
85
|
+
target_key: manifestIdentity.target_key,
|
|
86
|
+
collections: manifestIdentity.collections.map((collection) => ({
|
|
87
|
+
...collection,
|
|
88
|
+
})),
|
|
89
|
+
});
|
|
90
|
+
handoffs.push({
|
|
91
|
+
path: `${outputDirectory}/${entry.name}`,
|
|
92
|
+
sha256: sha256Hex(bytes),
|
|
93
|
+
bytes: bytes.length,
|
|
94
|
+
scope_key: preflight.handoff.scope.key,
|
|
95
|
+
target_key: targetKey,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
handoffSetIntegrity.finish();
|
|
99
|
+
assertSameSet(seenManifestPaths, declaredManifestPaths, "manifest_path_set_mismatch");
|
|
100
|
+
const result = {
|
|
101
|
+
status: "published",
|
|
102
|
+
output_directory: outputDirectory,
|
|
103
|
+
handoffs,
|
|
104
|
+
manifests: [...manifests.values()]
|
|
105
|
+
.map((baseline) => ({
|
|
106
|
+
path: baseline.path,
|
|
107
|
+
sha256: baseline.sha256,
|
|
108
|
+
scope_key: baseline.scope_key,
|
|
109
|
+
target_key: baseline.target_key,
|
|
110
|
+
collections: baseline.collections.map((collection) => ({
|
|
111
|
+
...collection,
|
|
112
|
+
})),
|
|
113
|
+
}))
|
|
114
|
+
.sort((left, right) => compareText(left.target_key, right.target_key)),
|
|
115
|
+
};
|
|
116
|
+
await rename(temporary, outputAbsolute);
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
try {
|
|
121
|
+
await rm(temporary, { recursive: true, force: true });
|
|
122
|
+
}
|
|
123
|
+
catch (cleanupError) {
|
|
124
|
+
throw new AggregateError([error, cleanupError], "design_resource_handoff_bundle_cleanup_failed");
|
|
125
|
+
}
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function normalizeManifestPaths(values) {
|
|
130
|
+
if (!values.length)
|
|
131
|
+
invalid("manifest_paths_required", "");
|
|
132
|
+
const normalized = values.map((value) => normalizeRepositoryFile(value, "design_resource_bundle_manifest"));
|
|
133
|
+
if (new Set(normalized).size !== normalized.length)
|
|
134
|
+
invalid("manifest_path_duplicate", normalized.join(","));
|
|
135
|
+
return normalized.sort(compareText);
|
|
136
|
+
}
|
|
137
|
+
async function assertSafeDirectory(repository, relative, label) {
|
|
138
|
+
const absolute = resolveInsideRepository(repository, relative, label);
|
|
139
|
+
const info = await lstat(absolute).catch(() => null);
|
|
140
|
+
if (!info)
|
|
141
|
+
invalid(`${label}_not_found`, relative);
|
|
142
|
+
if (info.isSymbolicLink() || !info.isDirectory())
|
|
143
|
+
invalid(`${label}_not_directory`, relative);
|
|
144
|
+
const root = await realpath(repository);
|
|
145
|
+
const resolved = await realpath(absolute);
|
|
146
|
+
const outside = path.relative(root, resolved);
|
|
147
|
+
if (outside === ".." ||
|
|
148
|
+
outside.startsWith(`..${path.sep}`) ||
|
|
149
|
+
path.isAbsolute(outside))
|
|
150
|
+
invalid(`${label}_outside_repository`, relative);
|
|
151
|
+
return resolved;
|
|
152
|
+
}
|
|
153
|
+
function normalizeBundleDirectory(value, label) {
|
|
154
|
+
const normalized = normalizeRepositoryCwd(value, label);
|
|
155
|
+
if (normalized === ".")
|
|
156
|
+
invalid("repository_root_directory_forbidden", label);
|
|
157
|
+
return normalized;
|
|
158
|
+
}
|
|
159
|
+
function assertSameSet(actual, expected, code) {
|
|
160
|
+
const left = [...actual].sort(compareText);
|
|
161
|
+
const right = [...expected].sort(compareText);
|
|
162
|
+
if (left.length !== right.length ||
|
|
163
|
+
left.some((value, index) => value !== right[index]))
|
|
164
|
+
invalid(code, `${left.join(",")}:${right.join(",")}`);
|
|
165
|
+
}
|
|
166
|
+
function invalid(code, detail) {
|
|
167
|
+
throw new Error(`design_resource_handoff_bundle_invalid:${code}${detail ? `:${detail}` : ""}`);
|
|
168
|
+
}
|
|
169
|
+
function compareText(left, right) {
|
|
170
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
171
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { ParsedDesignResourceHandoffV1 } from "./design-resource-handoff-types.js";
|
|
2
|
-
export declare function validateDesignResourceFiles(
|
|
2
|
+
export declare function validateDesignResourceFiles(parsed: ParsedDesignResourceHandoffV1, contents: Map<string, Buffer>): void;
|
|
@@ -1,17 +1,9 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
1
|
import { isTextResource, } from "./design-resource-handoff-file-primitives.js";
|
|
4
2
|
import { invalidDesignResourceHandoff } from "./design-resource-handoff-validation-primitives.js";
|
|
5
3
|
import { resolveDesignResourceLocatorValue } from "./design-resource-fact-locator-validation.js";
|
|
6
4
|
import { validateDesignResourceImplementationDependencyClosure } from "./design-resource-handoff-web-dependency-validation.js";
|
|
7
|
-
|
|
8
|
-
export async function validateDesignResourceFiles(repository, parsed) {
|
|
5
|
+
export function validateDesignResourceFiles(parsed, contents) {
|
|
9
6
|
const resources = new Map(parsed.handoff.resources.map((resource) => [resource.key, resource]));
|
|
10
|
-
const contents = new Map();
|
|
11
|
-
for (const resource of parsed.handoff.resources) {
|
|
12
|
-
const file = await assertProtectedRepositoryFile(repository, path.resolve(repository, ...resource.path.split("/")), `design_resource:${resource.key}`);
|
|
13
|
-
contents.set(resource.key, await readFile(file));
|
|
14
|
-
}
|
|
15
7
|
for (const evidence of parsed.handoff.evidence)
|
|
16
8
|
validateLocator(evidence, resources.get(evidence.resource_ref), contents.get(evidence.resource_ref));
|
|
17
9
|
for (const target of parsed.handoff.targets)
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { DesignResourceObservableFactManifestV1 } from "./design-resource-fact-manifest-types.js";
|
|
2
|
+
import type { DesignResourceHandoffManifestBackedV1, DesignResourceHandoffV1 } from "./design-resource-handoff-types.js";
|
|
3
|
+
export declare function hydrateManifestBackedDesignResourceHandoff(descriptor: DesignResourceHandoffManifestBackedV1, manifests: Map<string, DesignResourceObservableFactManifestV1>): DesignResourceHandoffV1;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { invalidDesignResourceHandoff } from "./design-resource-handoff-validation-primitives.js";
|
|
2
|
+
export function hydrateManifestBackedDesignResourceHandoff(descriptor, manifests) {
|
|
3
|
+
if (descriptor.targets.length !== 1)
|
|
4
|
+
invalidDesignResourceHandoff("manifest_backed_one_target_required", String(descriptor.targets.length));
|
|
5
|
+
const target = descriptor.targets[0];
|
|
6
|
+
const manifest = manifests.get(target.key);
|
|
7
|
+
if (!manifest)
|
|
8
|
+
invalidDesignResourceHandoff("fact_manifest_target_missing", target.key);
|
|
9
|
+
return {
|
|
10
|
+
schema_version: descriptor.schema_version,
|
|
11
|
+
intent: descriptor.intent,
|
|
12
|
+
scope: descriptor.scope,
|
|
13
|
+
provenance: descriptor.provenance,
|
|
14
|
+
resources: descriptor.resources,
|
|
15
|
+
axis_dispositions: manifest.axis_dispositions,
|
|
16
|
+
condition_exclusions: manifest.condition_exclusions,
|
|
17
|
+
conditions: manifest.conditions,
|
|
18
|
+
subjects: manifest.subjects,
|
|
19
|
+
variation_axis_dispositions: manifest.variation_axis_dispositions,
|
|
20
|
+
variation_exclusions: manifest.variation_exclusions,
|
|
21
|
+
variations: manifest.variations,
|
|
22
|
+
properties: manifest.properties,
|
|
23
|
+
lineage_nodes: manifest.lineage_nodes,
|
|
24
|
+
targets: descriptor.targets,
|
|
25
|
+
evidence: manifest.evidence,
|
|
26
|
+
fact_cells: manifest.fact_cells,
|
|
27
|
+
facts: manifest.facts,
|
|
28
|
+
proof_obligations: manifest.proof_obligations,
|
|
29
|
+
oracles: manifest.oracles,
|
|
30
|
+
environments: manifest.environments,
|
|
31
|
+
asset_bindings: manifest.asset_bindings,
|
|
32
|
+
resource_fact_closure: descriptor.resource_fact_closure,
|
|
33
|
+
coverage: descriptor.coverage,
|
|
34
|
+
acceptance_blockers: manifest.acceptance_blockers,
|
|
35
|
+
proposal: descriptor.proposal,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ParsedDesignResourceHandoffInputV1 } from "./design-resource-handoff-types.js";
|
|
2
|
+
export interface DesignResourceHandoffBlockSpan {
|
|
3
|
+
bodyStartOffset: number;
|
|
4
|
+
bodyEndOffset: number;
|
|
5
|
+
}
|
|
2
6
|
export declare function containsDesignResourceHandoff(content: string): boolean;
|
|
3
|
-
export declare function
|
|
7
|
+
export declare function scanDesignResourceHandoffBlocks(content: string): DesignResourceHandoffBlockSpan[];
|
|
8
|
+
export declare function parseDesignResourceHandoffMarkdown(handoffPath: string, content: string): ParsedDesignResourceHandoffInputV1;
|
|
@@ -1,23 +1,43 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
const
|
|
1
|
+
import { parseSourceDocument } from "./long-task-source-item-parser.js";
|
|
2
|
+
import { forEachSourceLine } from "./source-line-scanner.js";
|
|
3
|
+
const DESIGN_RESOURCE_START = /^```yaml[ \t]+design-resource-handoff-v1[ \t]*$/u;
|
|
4
|
+
const FORMAL_BLOCK_END = /^```[ \t]*$/u;
|
|
5
5
|
export function containsDesignResourceHandoff(content) {
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
return scanDesignResourceHandoffBlocks(content).length > 0;
|
|
7
|
+
}
|
|
8
|
+
export function scanDesignResourceHandoffBlocks(content) {
|
|
9
|
+
const blocks = [];
|
|
10
|
+
let bodyStartOffset = null;
|
|
11
|
+
forEachSourceLine(content, (line, startOffset, _endOffset, nextOffset) => {
|
|
12
|
+
if (bodyStartOffset === null) {
|
|
13
|
+
if (DESIGN_RESOURCE_START.test(line))
|
|
14
|
+
bodyStartOffset = nextOffset;
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
if (!FORMAL_BLOCK_END.test(line))
|
|
18
|
+
return;
|
|
19
|
+
blocks.push({
|
|
20
|
+
bodyStartOffset,
|
|
21
|
+
bodyEndOffset: startOffset,
|
|
22
|
+
});
|
|
23
|
+
bodyStartOffset = null;
|
|
24
|
+
});
|
|
25
|
+
return blocks;
|
|
8
26
|
}
|
|
9
27
|
export function parseDesignResourceHandoffMarkdown(handoffPath, content) {
|
|
10
|
-
|
|
11
|
-
const blocks = [...content.matchAll(FENCE)];
|
|
28
|
+
const blocks = scanDesignResourceHandoffBlocks(content);
|
|
12
29
|
if (blocks.length !== 1)
|
|
13
30
|
throw new Error(`design_resource_handoff_invalid:block_count:${handoffPath}:${blocks.length}`);
|
|
14
31
|
try {
|
|
15
|
-
const
|
|
32
|
+
const parsedSource = parseSourceDocument(handoffPath, content);
|
|
33
|
+
const handoff = parsedSource.designResourceHandoff;
|
|
34
|
+
if (!handoff)
|
|
35
|
+
throw new Error("design-resource-handoff-v1 block was not decoded");
|
|
16
36
|
return {
|
|
17
37
|
handoff_path: handoffPath,
|
|
18
|
-
handoff
|
|
19
|
-
source_item_keys:
|
|
20
|
-
source_item_kinds: Object.fromEntries(
|
|
38
|
+
handoff,
|
|
39
|
+
source_item_keys: parsedSource.items.map((item) => item.key),
|
|
40
|
+
source_item_kinds: Object.fromEntries(parsedSource.items.map((item) => [item.key, item.kind])),
|
|
21
41
|
};
|
|
22
42
|
}
|
|
23
43
|
catch (error) {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { DesignResourceHandoffPreflightV1 } from "./design-resource-handoff-types.js";
|
|
2
|
+
type HandoffSetFailure = (code: string, detail: string) => never;
|
|
3
|
+
export interface DesignResourceHandoffSetIntegrity {
|
|
4
|
+
consume(preflight: DesignResourceHandoffPreflightV1): void;
|
|
5
|
+
finish(): void;
|
|
6
|
+
}
|
|
7
|
+
export declare function createDesignResourceHandoffSetIntegrity(fail: HandoffSetFailure): DesignResourceHandoffSetIntegrity;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { canonicalValueJson, sha256Hex } from "./strict-codec.js";
|
|
2
|
+
export function createDesignResourceHandoffSetIntegrity(fail) {
|
|
3
|
+
const scopes = new Map();
|
|
4
|
+
const sourceItems = new Set();
|
|
5
|
+
let issue = null;
|
|
6
|
+
function record(code, detail) {
|
|
7
|
+
issue ??= { code, detail };
|
|
8
|
+
}
|
|
9
|
+
function sharedHeader(scope, name, value) {
|
|
10
|
+
const digest = canonicalDigest(value);
|
|
11
|
+
const previous = scope.headers.get(name);
|
|
12
|
+
if (previous && previous !== digest)
|
|
13
|
+
record("handoff_set_header_conflict", name);
|
|
14
|
+
else
|
|
15
|
+
scope.headers.set(name, digest);
|
|
16
|
+
}
|
|
17
|
+
function sharedCollection(scope, name, rows) {
|
|
18
|
+
const index = scope.sharedRows.get(name) ?? new Map();
|
|
19
|
+
scope.sharedRows.set(name, index);
|
|
20
|
+
for (const row of rows) {
|
|
21
|
+
const digest = canonicalDigest(row);
|
|
22
|
+
const previous = index.get(row.key);
|
|
23
|
+
if (previous && previous !== digest)
|
|
24
|
+
record("handoff_set_shared_row_conflict", `${name}:${row.key}`);
|
|
25
|
+
else
|
|
26
|
+
index.set(row.key, digest);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
consume(preflight) {
|
|
31
|
+
if (issue)
|
|
32
|
+
return;
|
|
33
|
+
const handoff = preflight.handoff;
|
|
34
|
+
const scope = scopeIndex(scopes, handoff.scope.key);
|
|
35
|
+
sharedHeader(scope, "provenance", handoff.provenance);
|
|
36
|
+
sharedHeader(scope, "proposal", handoff.proposal);
|
|
37
|
+
sharedCollection(scope, "resources", handoff.resources);
|
|
38
|
+
sharedCollection(scope, "conditions", handoff.conditions);
|
|
39
|
+
sharedCollection(scope, "properties", handoff.properties);
|
|
40
|
+
sharedCollection(scope, "resource_fact_closure", handoff.resource_fact_closure);
|
|
41
|
+
for (const closure of handoff.resource_fact_closure) {
|
|
42
|
+
const digest = canonicalDigest(closure);
|
|
43
|
+
const previous = scope.resourceClosures.get(closure.resource_ref);
|
|
44
|
+
if (previous &&
|
|
45
|
+
(previous.key !== closure.key || previous.digest !== digest))
|
|
46
|
+
record("handoff_set_resource_closure_conflict", `${closure.resource_ref}:${previous.key}:${closure.key}`);
|
|
47
|
+
else
|
|
48
|
+
scope.resourceClosures.set(closure.resource_ref, {
|
|
49
|
+
key: closure.key,
|
|
50
|
+
digest,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
for (const resource of handoff.resources) {
|
|
54
|
+
const digest = canonicalDigest(resource);
|
|
55
|
+
const previous = scope.resourcePaths.get(resource.path);
|
|
56
|
+
if (previous &&
|
|
57
|
+
(previous.key !== resource.key || previous.digest !== digest))
|
|
58
|
+
record("handoff_set_resource_path_conflict", `${resource.path}:${previous.key}:${resource.key}`);
|
|
59
|
+
else
|
|
60
|
+
scope.resourcePaths.set(resource.path, {
|
|
61
|
+
key: resource.key,
|
|
62
|
+
digest,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
for (const sourceItem of preflight.source_item_keys) {
|
|
66
|
+
if (sourceItems.has(sourceItem))
|
|
67
|
+
record("handoff_set_source_item_duplicate", sourceItem);
|
|
68
|
+
else
|
|
69
|
+
sourceItems.add(sourceItem);
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
finish() {
|
|
73
|
+
if (issue)
|
|
74
|
+
fail(issue.code, issue.detail);
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function scopeIndex(scopes, key) {
|
|
79
|
+
const existing = scopes.get(key);
|
|
80
|
+
if (existing)
|
|
81
|
+
return existing;
|
|
82
|
+
const created = {
|
|
83
|
+
headers: new Map(),
|
|
84
|
+
sharedRows: new Map(),
|
|
85
|
+
resourcePaths: new Map(),
|
|
86
|
+
resourceClosures: new Map(),
|
|
87
|
+
};
|
|
88
|
+
scopes.set(key, created);
|
|
89
|
+
return created;
|
|
90
|
+
}
|
|
91
|
+
function canonicalDigest(value) {
|
|
92
|
+
return sha256Hex(canonicalValueJson(value));
|
|
93
|
+
}
|
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
import type { DesignResourceHandoffV1 } from "./design-resource-handoff-types.js";
|
|
1
|
+
import type { DesignResourceHandoffInputV1, DesignResourceHandoffV1 } from "./design-resource-handoff-types.js";
|
|
2
|
+
export declare function parseDesignResourceHandoffInputShape(value: unknown): DesignResourceHandoffInputV1;
|
|
2
3
|
export declare function parseDesignResourceHandoffShape(value: unknown): DesignResourceHandoffV1;
|
|
@@ -3,6 +3,14 @@ import { parseDesignResourceHandoffBlockers, parseDesignResourceHandoffCoverage,
|
|
|
3
3
|
import { contractKey, stableKeys, } from "./design-resource-handoff-shape-primitives.js";
|
|
4
4
|
import { parseDesignResourceHandoffConditions, parseDesignResourceHandoffResources, parseDesignResourceHandoffSubjects, parseDesignResourceHandoffTargets, } from "./design-resource-handoff-shape-structure.js";
|
|
5
5
|
import { literal, object, repositoryFile, string, strings, } from "./long-task-shape-primitives.js";
|
|
6
|
+
export function parseDesignResourceHandoffInputShape(value) {
|
|
7
|
+
if (value &&
|
|
8
|
+
typeof value === "object" &&
|
|
9
|
+
!Array.isArray(value) &&
|
|
10
|
+
value.representation === "manifest_backed")
|
|
11
|
+
return parseManifestBackedDesignResourceHandoffShape(value);
|
|
12
|
+
return parseDesignResourceHandoffShape(value);
|
|
13
|
+
}
|
|
6
14
|
export function parseDesignResourceHandoffShape(value) {
|
|
7
15
|
const root = object(value, "design_resource_handoff", [
|
|
8
16
|
"schema_version",
|
|
@@ -102,3 +110,57 @@ export function parseDesignResourceHandoffShape(value) {
|
|
|
102
110
|
},
|
|
103
111
|
};
|
|
104
112
|
}
|
|
113
|
+
function parseManifestBackedDesignResourceHandoffShape(value) {
|
|
114
|
+
const root = object(value, "design_resource_handoff", [
|
|
115
|
+
"schema_version",
|
|
116
|
+
"representation",
|
|
117
|
+
"intent",
|
|
118
|
+
"scope",
|
|
119
|
+
"provenance",
|
|
120
|
+
"resources",
|
|
121
|
+
"targets",
|
|
122
|
+
"resource_fact_closure",
|
|
123
|
+
"coverage",
|
|
124
|
+
"proposal",
|
|
125
|
+
]);
|
|
126
|
+
const parsed = parseDesignResourceHandoffShape({
|
|
127
|
+
schema_version: root.schema_version,
|
|
128
|
+
intent: root.intent,
|
|
129
|
+
scope: root.scope,
|
|
130
|
+
provenance: root.provenance,
|
|
131
|
+
resources: root.resources,
|
|
132
|
+
axis_dispositions: [],
|
|
133
|
+
condition_exclusions: [],
|
|
134
|
+
conditions: [],
|
|
135
|
+
subjects: [],
|
|
136
|
+
variation_axis_dispositions: [],
|
|
137
|
+
variation_exclusions: [],
|
|
138
|
+
variations: [],
|
|
139
|
+
properties: [],
|
|
140
|
+
lineage_nodes: [],
|
|
141
|
+
targets: root.targets,
|
|
142
|
+
evidence: [],
|
|
143
|
+
fact_cells: [],
|
|
144
|
+
facts: [],
|
|
145
|
+
proof_obligations: [],
|
|
146
|
+
oracles: [],
|
|
147
|
+
environments: [],
|
|
148
|
+
asset_bindings: [],
|
|
149
|
+
resource_fact_closure: root.resource_fact_closure,
|
|
150
|
+
coverage: root.coverage,
|
|
151
|
+
acceptance_blockers: [],
|
|
152
|
+
proposal: root.proposal,
|
|
153
|
+
});
|
|
154
|
+
return {
|
|
155
|
+
schema_version: parsed.schema_version,
|
|
156
|
+
representation: literal(root.representation, ["manifest_backed"], "design_resource_handoff.representation"),
|
|
157
|
+
intent: parsed.intent,
|
|
158
|
+
scope: parsed.scope,
|
|
159
|
+
provenance: parsed.provenance,
|
|
160
|
+
resources: parsed.resources,
|
|
161
|
+
targets: parsed.targets,
|
|
162
|
+
resource_fact_closure: parsed.resource_fact_closure,
|
|
163
|
+
coverage: parsed.coverage,
|
|
164
|
+
proposal: parsed.proposal,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ParsedDesignResourceHandoffInputV1 } from "./design-resource-handoff-types.js";
|
|
2
|
+
export interface DesignResourceSnapshot {
|
|
3
|
+
contents: Map<string, Buffer>;
|
|
4
|
+
hashes: Record<string, string>;
|
|
5
|
+
}
|
|
6
|
+
export declare function readDesignResourceSnapshot(repository: string, parsed: ParsedDesignResourceHandoffInputV1): Promise<DesignResourceSnapshot>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { invalidDesignResourceHandoff } from "./design-resource-handoff-validation-primitives.js";
|
|
4
|
+
import { assertProtectedRepositoryFile } from "./long-task-protected-files.js";
|
|
5
|
+
import { sha256Hex } from "./strict-codec.js";
|
|
6
|
+
export async function readDesignResourceSnapshot(repository, parsed) {
|
|
7
|
+
const contents = new Map();
|
|
8
|
+
const hashes = {};
|
|
9
|
+
for (const resource of parsed.handoff.resources) {
|
|
10
|
+
if (resource.path === parsed.handoff_path)
|
|
11
|
+
invalidDesignResourceHandoff("resource_must_not_be_handoff", resource.key);
|
|
12
|
+
const file = await assertProtectedRepositoryFile(repository, path.resolve(repository, ...resource.path.split("/")), `design_resource:${resource.key}`);
|
|
13
|
+
const bytes = await readFile(file);
|
|
14
|
+
const digest = sha256Hex(bytes);
|
|
15
|
+
if (digest !== resource.sha256)
|
|
16
|
+
invalidDesignResourceHandoff("resource_digest_mismatch", `${resource.key}:${resource.sha256}:${digest}`);
|
|
17
|
+
contents.set(resource.key, bytes);
|
|
18
|
+
hashes[resource.key] = digest;
|
|
19
|
+
}
|
|
20
|
+
return { contents, hashes };
|
|
21
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ExecutionTargetCapabilityV2 } from "./execution-target-capabilities.js";
|
|
2
|
-
import type { DesignResourceAssetBindingV1, DesignResourceAxisDispositionV1, DesignResourceConditionCombinationDispositionV1, DesignResourceEnvironmentV1, DesignResourceFactCellV1, DesignResourceFactV1, DesignResourceLineageNodeV1, DesignResourceOracleV1, DesignResourcePropertyDefinitionV1, DesignResourceProofObligationV1, DesignResourceRelationEndpointV1, DesignResourceSubjectKind, DesignResourceSubjectPresenceKind, DesignResourceSubjectVariationV1, DesignResourceVariationAxisDispositionV1, DesignResourceVariationCombinationDispositionV1 } from "./design-resource-fact-manifest-types.js";
|
|
2
|
+
import type { DesignResourceAssetBindingV1, DesignResourceAxisDispositionV1, DesignResourceConditionCombinationDispositionV1, DesignResourceEnvironmentV1, DesignResourceFactCellV1, DesignResourceFactV1, DesignResourceLineageNodeV1, DesignResourceManifestCollectionName, DesignResourceOracleV1, DesignResourcePropertyDefinitionV1, DesignResourceProofObligationV1, DesignResourceRelationEndpointV1, DesignResourceSubjectKind, DesignResourceSubjectPresenceKind, DesignResourceSubjectVariationV1, DesignResourceVariationAxisDispositionV1, DesignResourceVariationCombinationDispositionV1 } from "./design-resource-fact-manifest-types.js";
|
|
3
3
|
export declare const DESIGN_RESOURCE_DIMENSIONS: readonly ["surface_flow", "visual_content", "component_control", "state_interaction", "motion", "adaptation_input", "accessibility", "assets"];
|
|
4
4
|
export type DesignResourceDimension = (typeof DESIGN_RESOURCE_DIMENSIONS)[number];
|
|
5
5
|
export declare const DESIGN_RESOURCE_EVIDENCE_KINDS: readonly ["frame", "component_variant", "prototype_state", "prototype_transition", "motion_spec", "motion_capture", "responsive_spec", "input_spec", "accessibility_spec", "semantic_tree", "token_spec", "asset", "annotation", "localization_spec", "system_ui_spec", "haptic_spec", "sound_spec", "sound_capture", "render_environment", "relation_spec"];
|
|
@@ -57,6 +57,10 @@ export interface DesignResourceHandoffV1 {
|
|
|
57
57
|
revision: string;
|
|
58
58
|
};
|
|
59
59
|
}
|
|
60
|
+
export type DesignResourceHandoffManifestBackedV1 = Pick<DesignResourceHandoffV1, "schema_version" | "intent" | "scope" | "provenance" | "resources" | "targets" | "resource_fact_closure" | "coverage" | "proposal"> & {
|
|
61
|
+
representation: "manifest_backed";
|
|
62
|
+
};
|
|
63
|
+
export type DesignResourceHandoffInputV1 = DesignResourceHandoffV1 | DesignResourceHandoffManifestBackedV1;
|
|
60
64
|
export interface DesignResourceHandoffResourceV1 {
|
|
61
65
|
key: string;
|
|
62
66
|
role: "exact_target" | "constraint" | "supporting";
|
|
@@ -216,10 +220,28 @@ export interface ParsedDesignResourceHandoffV1 {
|
|
|
216
220
|
source_item_keys: string[];
|
|
217
221
|
source_item_kinds: Record<string, string>;
|
|
218
222
|
}
|
|
223
|
+
export interface ParsedDesignResourceHandoffInputV1 {
|
|
224
|
+
handoff_path: string;
|
|
225
|
+
handoff: DesignResourceHandoffInputV1;
|
|
226
|
+
source_item_keys: string[];
|
|
227
|
+
source_item_kinds: Record<string, string>;
|
|
228
|
+
}
|
|
219
229
|
export interface DesignResourceHandoffPreflightV1 extends ParsedDesignResourceHandoffV1 {
|
|
220
230
|
schema_version: "design-resource-handoff-preflight-v1";
|
|
221
231
|
status: "ready";
|
|
222
232
|
resource_hashes: Record<string, string>;
|
|
233
|
+
manifest_identities: Array<{
|
|
234
|
+
resource_ref: string;
|
|
235
|
+
path: string;
|
|
236
|
+
sha256: string;
|
|
237
|
+
scope_key: string;
|
|
238
|
+
target_key: string;
|
|
239
|
+
collections: Array<{
|
|
240
|
+
name: DesignResourceManifestCollectionName;
|
|
241
|
+
expected_count: number;
|
|
242
|
+
identity_sha256: string;
|
|
243
|
+
}>;
|
|
244
|
+
}>;
|
|
223
245
|
counts: {
|
|
224
246
|
resources: number;
|
|
225
247
|
manifests: number;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
import type { DesignResourceHandoffPreflightV1, ParsedDesignResourceHandoffV1 } from "./design-resource-handoff-types.js";
|
|
1
|
+
import type { DesignResourceHandoffPreflightV1, ParsedDesignResourceHandoffInputV1, ParsedDesignResourceHandoffV1 } from "./design-resource-handoff-types.js";
|
|
2
2
|
export declare function preflightDesignResourceHandoff(repository: string, handoffPath: string): Promise<DesignResourceHandoffPreflightV1>;
|
|
3
|
+
export declare function preflightParsedDesignResourceHandoff(repository: string, parsed: ParsedDesignResourceHandoffInputV1): Promise<DesignResourceHandoffPreflightV1>;
|
|
3
4
|
export declare function validateDesignResourceHandoffSemantics(parsed: ParsedDesignResourceHandoffV1): void;
|