@pixel-point/toolcraft 0.0.16 → 0.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/package.json +1 -1
  2. package/src/generate-performance-iteration-execution.test.mjs +2 -2
  3. package/src/generate-test-demand-only-lifecycle.mjs +14 -3
  4. package/src/generate-test-dependency-sandbox.mjs +42 -5
  5. package/src/generate-test-doc-assertions.mjs +68 -18
  6. package/src/generate-test-doc-assertions.test.mjs +22 -0
  7. package/src/generate-test-file-assertions.mjs +43 -76
  8. package/src/generate-test-output-assertions.mjs +95 -0
  9. package/src/generate-test-package-assertions.mjs +6 -0
  10. package/src/generate-test-performance-boundary-assertions.mjs +45 -0
  11. package/src/generate-test-product-fixture.mjs +1 -0
  12. package/src/generate-test-product-fixture.test.mjs +18 -0
  13. package/src/generate-test-product-iteration.mjs +1 -0
  14. package/src/generate-test-product-iteration.test.mjs +1 -0
  15. package/src/generate-test-standalone-delivery-stages.mjs +14 -6
  16. package/src/generate.test.mjs +14 -0
  17. package/templates/runtime/contracts/component-contracts.choices.test.ts +12 -9
  18. package/templates/runtime/contracts/component-contracts.choices.ts +10 -9
  19. package/templates/runtime/contracts/component-contracts.runtime-acceptance.test.ts +17 -2
  20. package/templates/runtime/contracts/component-contracts.runtime-panels.test.ts +1 -1
  21. package/templates/runtime/contracts/component-contracts.runtime-shell.test.ts +1 -1
  22. package/templates/runtime/contracts/component-contracts.runtime.ts +3 -2
  23. package/templates/runtime/contracts/decision-contracts.test.ts +25 -4
  24. package/templates/runtime/contracts/decision-contracts.ts +3 -3
  25. package/templates/runtime/contracts/performance-verification-policy.test.ts +59 -6
  26. package/templates/runtime/contracts/performance-verification-policy.ts +5 -2
  27. package/templates/runtime/export/artifact-download.test.ts +71 -0
  28. package/templates/runtime/export/artifact-download.ts +80 -0
  29. package/templates/runtime/export/artifact-export-request.ts +23 -0
  30. package/templates/runtime/export/artifact-export-settings.test.ts +75 -0
  31. package/templates/runtime/export/artifact-export-settings.ts +96 -0
  32. package/templates/runtime/export/artifact-frame-renderer.test.ts +107 -0
  33. package/templates/runtime/export/artifact-frame-renderer.ts +96 -0
  34. package/templates/runtime/export/artifact-frame-state.test.ts +54 -0
  35. package/templates/runtime/export/artifact-frame-state.ts +44 -0
  36. package/templates/runtime/export/artifact-scene-frame.test.ts +84 -0
  37. package/templates/runtime/export/artifact-scene-frame.ts +118 -0
  38. package/templates/runtime/export/export-background.ts +65 -0
  39. package/templates/runtime/export/export-error.ts +43 -0
  40. package/templates/runtime/export/export.test.ts +6 -186
  41. package/templates/runtime/export/image-artifact-export.test.ts +111 -0
  42. package/templates/runtime/export/image-artifact-export.ts +145 -0
  43. package/templates/runtime/export/index.ts +14 -1
  44. package/templates/runtime/export/product-export-renderer.test.ts +77 -0
  45. package/templates/runtime/export/product-export-renderer.ts +63 -0
  46. package/templates/runtime/export/video-artifact-export.test.ts +166 -0
  47. package/templates/runtime/export/video-artifact-export.ts +164 -0
  48. package/templates/runtime/export/video-encoding-backend.ts +124 -0
  49. package/templates/runtime/export/video-encoding-policy.test.ts +68 -0
  50. package/templates/runtime/export/video-encoding-policy.ts +86 -0
  51. package/templates/runtime/export/video-frame-schedule.test.ts +58 -0
  52. package/templates/runtime/export/video-frame-schedule.ts +42 -0
  53. package/templates/runtime/index.ts +1 -0
  54. package/templates/runtime/react/app-shell/runtime-public-api-runtime.test.tsx +291 -0
  55. package/templates/runtime/react/app-shell/runtime-public-api-test-support.ts +21 -0
  56. package/templates/runtime/react/app-shell/runtime-public-api.test.tsx +4 -303
  57. package/templates/runtime/react/app-shell/toolcraft-app.scene-export.test.tsx +12 -27
  58. package/templates/runtime/react/app-shell/toolcraft-app.tsx +22 -4
  59. package/templates/runtime/react/app-shell/toolcraft-source-asset-context.test.tsx +16 -57
  60. package/templates/runtime/react/app-shell/toolcraft-source-asset-context.tsx +75 -434
  61. package/templates/runtime/react/app-shell/toolcraft-source-asset-owner.test.ts +194 -0
  62. package/templates/runtime/react/app-shell/toolcraft-source-asset-owner.ts +227 -0
  63. package/templates/runtime/react/app-shell/toolcraft-source-asset-provider-ownership.test.tsx +243 -0
  64. package/templates/runtime/react/app-shell/toolcraft-source-asset-test-support.ts +51 -0
  65. package/templates/runtime/react/app-shell/toolcraft-unavailable-resource-proof.test.ts +138 -0
  66. package/templates/runtime/react/app-shell/toolcraft-unavailable-resource-proof.ts +378 -0
  67. package/templates/runtime/react/controls-panel/__tests__/controls-panel.footer-actions.test.tsx +7 -12
  68. package/templates/runtime/react/controls-panel/__tests__/controls-panel.scene-export-actions.test.tsx +17 -83
  69. package/templates/runtime/react/controls-panel/actions/controls-panel-actions.ts +42 -84
  70. package/templates/runtime/react/controls-panel/actions/export-action-runner.ts +62 -0
  71. package/templates/runtime/react/panel-host/panel-host-runtime-placement.test.tsx +30 -0
  72. package/templates/runtime/react/panel-host/panel-host.tsx +4 -0
  73. package/templates/runtime/scene/scene-bounds.ts +1 -2
  74. package/templates/runtime/schema/canvas-render-scale.ts +62 -0
  75. package/templates/runtime/schema/controls-panel-section-id.test.ts +1 -1
  76. package/templates/runtime/schema/controls-panel-section-roundtrip.test.ts +3 -3
  77. package/templates/runtime/schema/define-toolcraft.setup-background.test.ts +1 -1
  78. package/templates/runtime/schema/define-toolcraft.setup-render-scale-timeline.test.ts +71 -3
  79. package/templates/runtime/schema/define-toolcraft.ts +1 -1
  80. package/templates/runtime/schema/schema-resolvers.ts +0 -56
  81. package/templates/runtime/schema/types.ts +4 -4
  82. package/templates/runtime/state/canvas-background-state.ts +6 -0
  83. package/templates/runtime/testing/performance-render-plan-integration.test.ts +64 -0
  84. package/templates/runtime/testing/performance-renderer-pipeline-validation.ts +18 -1
  85. package/templates/starter/AGENTS.md +8 -7
  86. package/templates/starter/NOTICE.md +6 -0
  87. package/templates/starter/docs/toolcraft/README.md +2 -1
  88. package/templates/starter/docs/toolcraft/acceptance-testing.md +8 -6
  89. package/templates/starter/docs/toolcraft/agent-worklog.md +3 -3
  90. package/templates/starter/docs/toolcraft/assembly-workflow.md +4 -3
  91. package/templates/starter/docs/toolcraft/core/media-upload.md +2 -2
  92. package/templates/starter/docs/toolcraft/core/performance.md +14 -3
  93. package/templates/starter/docs/toolcraft/core/runtime-boundary.md +3 -1
  94. package/templates/starter/docs/toolcraft/core/setup-export.md +16 -12
  95. package/templates/starter/docs/toolcraft/core/timeline-animation.md +4 -4
  96. package/templates/starter/docs/toolcraft/performance.md +20 -6
  97. package/templates/starter/docs/toolcraft/renderer-technique.md +1 -1
  98. package/templates/starter/docs/toolcraft/schema-reference.md +2 -6
  99. package/templates/starter/docs/toolcraft/workflow.md +7 -2
  100. package/templates/starter/e2e/app-browser-control-coverage.spec.ts +33 -0
  101. package/templates/starter/e2e/app-browser-infinity-canvas-evidence.spec.ts +43 -1
  102. package/templates/starter/e2e/app-browser-render-scale-evidence.spec.ts +60 -6
  103. package/templates/starter/e2e/app-browser-render-scale-requirements.spec.ts +77 -1
  104. package/templates/starter/e2e/app-browser-runtime-provenance.spec.ts +8 -0
  105. package/templates/starter/e2e/app-browser-runtime-reporter.spec.ts +35 -0
  106. package/templates/starter/e2e/app-browser-runtime-requirements.spec.ts +4 -1
  107. package/templates/starter/e2e/app-browser-semantic-evidence.spec.ts +0 -1
  108. package/templates/starter/e2e/app-browser-shell.spec.ts +13 -5
  109. package/templates/starter/e2e/app-performance-path-adapters.ts +8 -1
  110. package/templates/starter/e2e/app-performance.spec.ts +16 -9
  111. package/templates/starter/e2e/browser-infinity-canvas-evidence.ts +9 -330
  112. package/templates/starter/e2e/browser-infinity-canvas-unavailable-image-evidence.ts +370 -0
  113. package/templates/starter/e2e/browser-media-export-evidence.spec.ts +77 -0
  114. package/templates/starter/e2e/browser-media-export-evidence.ts +208 -0
  115. package/templates/starter/e2e/browser-render-scale-evidence.ts +37 -29
  116. package/templates/starter/e2e/browser-runtime-evidence-reporter.ts +4 -1
  117. package/templates/starter/e2e/browser-runtime-evidence-requirements.ts +43 -9
  118. package/templates/starter/e2e/browser-standard-timeline-evidence.ts +106 -0
  119. package/templates/starter/e2e/decoded-pixel-observation.spec.ts +74 -0
  120. package/templates/starter/e2e/decoded-pixel-observation.ts +227 -0
  121. package/templates/starter/e2e/export-artifact-helpers.ts +206 -25
  122. package/templates/starter/e2e/image-artifact-inspection.spec.ts +41 -0
  123. package/templates/starter/e2e/image-artifact-inspection.ts +99 -0
  124. package/templates/starter/e2e/performance-canvas-helpers.ts +22 -21
  125. package/templates/starter/e2e/performance-canvas-quality-guard.spec.ts +312 -0
  126. package/templates/starter/e2e/performance-canvas-quality-guard.ts +196 -0
  127. package/templates/starter/e2e/performance-canvas-quality-observer.ts +351 -0
  128. package/templates/starter/e2e/performance-control-layout-helpers.ts +2 -2
  129. package/templates/starter/e2e/performance-path-adapter-contract.ts +4 -4
  130. package/templates/starter/e2e/performance-path-helpers.spec.ts +203 -70
  131. package/templates/starter/e2e/performance-path-helpers.ts +210 -91
  132. package/templates/starter/e2e/performance-pipeline-evidence-continuity.spec.ts +76 -0
  133. package/templates/starter/e2e/performance-pipeline-evidence-test-fixtures.ts +11 -2
  134. package/templates/starter/e2e/performance-pipeline-invariants.ts +47 -4
  135. package/templates/starter/e2e/performance-render-scale-runner.spec.ts +140 -38
  136. package/templates/starter/e2e/video-artifact-inspection.spec.ts +64 -0
  137. package/templates/starter/e2e/video-artifact-inspection.ts +281 -0
  138. package/templates/starter/gitignore +1 -0
  139. package/templates/starter/package.json +1 -0
  140. package/templates/starter/scripts/check-toolcraft-docs.mjs +17 -0
  141. package/templates/starter/scripts/toolcraft-delivery-plan-authority.test.mjs +60 -0
  142. package/templates/starter/scripts/toolcraft-delivery-plan-errors.test.mjs +7 -2
  143. package/templates/starter/scripts/toolcraft-delivery-plan.mjs +7 -3
  144. package/templates/starter/scripts/toolcraft-product-boundary-ast.mjs +24 -2
  145. package/templates/starter/scripts/toolcraft-product-boundary.mjs +12 -0
  146. package/templates/starter/scripts/toolcraft-product-boundary.test.mjs +46 -0
  147. package/templates/starter/scripts/toolcraft-product-evidence-boundary.test.mjs +85 -20
  148. package/templates/starter/scripts/toolcraft-product-evidence-import-policy.mjs +43 -0
  149. package/templates/starter/scripts/toolcraft-product-export-boundary.mjs +168 -0
  150. package/templates/starter/scripts/toolcraft-typescript-analysis.d.mts +73 -0
  151. package/templates/starter/scripts/toolcraft-unavailable-resource-evidence-facade-test-helpers.mjs +318 -0
  152. package/templates/starter/scripts/toolcraft-unavailable-resource-evidence-facade.test.mjs +155 -206
  153. package/templates/starter/scripts/toolcraft-verification-inventory.mjs +1 -0
  154. package/templates/starter/scripts/toolcraft-verification-receipt.test.mjs +51 -0
  155. package/templates/starter/scripts/toolcraft-vite-env-activation.test.mjs +128 -59
  156. package/templates/starter/src/app/acceptance/export-artifact-coverage.ts +113 -0
  157. package/templates/starter/src/app/acceptance/output-background-rules.ts +1 -1
  158. package/templates/starter/src/app/acceptance/output-export.ts +13 -2
  159. package/templates/starter/src/app/acceptance/types.ts +7 -0
  160. package/templates/starter/src/app/acceptance/validate-coverage.ts +2 -1
  161. package/templates/starter/src/app/starter-acceptance.export-artifact.test.ts +131 -0
  162. package/templates/starter/src/app/starter-acceptance.keyframes.test.ts +14 -4
  163. package/templates/starter/src/app/starter-acceptance.output-export-rules.test.ts +42 -1
  164. package/templates/starter/src/app/starter-acceptance.ts +2 -0
  165. package/templates/starter/src/app/starter-performance-test-utils.ts +103 -0
  166. package/templates/starter/src/app/starter-performance.gates.test.ts +231 -278
  167. package/templates/starter/src/app/starter-performance.lifecycle.test.ts +261 -0
  168. package/templates/starter/src/app/test-evidence/browser-runtime-contract.ts +2 -0
  169. package/templates/starter/src/routes/index.tsx +1 -0
  170. package/templates/runtime/export/export.ts +0 -166
@@ -239,6 +239,66 @@ test("creates a changed authorized iteration with exact affected proof", () => {
239
239
  assert.equal(getToolcraftDeliveryDiagnosticTier(plan), 3);
240
240
  });
241
241
 
242
+ test("creates an authorized iteration for a passless viewport path", () => {
243
+ const passlessPathId =
244
+ "performance-path:%5B%22interactive-continuous%22%2C%22viewport-drag%22%2C%5B%5D%2C%5B%5D%2C%5B%5D%5D";
245
+ const testName = `browser perf: toolcraft path ${passlessPathId}`;
246
+ const source = {
247
+ heading: "Delivery 2 - Slow viewport drag",
248
+ pathIds: [passlessPathId],
249
+ request: "The canvas is slow while panning.",
250
+ requestEvidence: "The canvas is slow while panning.",
251
+ };
252
+ const planningInputs = inputs({
253
+ requestAuthority: {
254
+ hash: createToolcraftPerformanceRequestAuthorityHash(source),
255
+ ...source,
256
+ },
257
+ });
258
+ planningInputs.catalog = {
259
+ ...catalog,
260
+ performance: [{ passIds: [], pathId: passlessPathId, testName }],
261
+ };
262
+ planningInputs.changeSet.impact.performanceCandidates = {
263
+ passIds: [],
264
+ pathIds: [passlessPathId],
265
+ testNames: [testName],
266
+ };
267
+
268
+ const plan = createToolcraftDeliveryPlan(planningInputs);
269
+
270
+ assert.equal(plan.kind, "performance-iteration");
271
+ assert.deepEqual(
272
+ plan.steps.find(({ kind }) => kind === "browser-performance"),
273
+ {
274
+ kind: "browser-performance",
275
+ passIds: [],
276
+ pathIds: [passlessPathId],
277
+ testNames: [testName],
278
+ },
279
+ );
280
+ });
281
+
282
+ test("keeps performance-adapter-only changes functional without inferred measurement", () => {
283
+ const planningInputs = inputs();
284
+ planningInputs.changeSet.impact = {
285
+ acceptanceIds: [],
286
+ browserTestNames: [],
287
+ buildRequired: false,
288
+ performanceCandidates: {
289
+ passIds: ["preview-composite"],
290
+ pathIds: [pathId],
291
+ testNames: [performanceTestName],
292
+ },
293
+ productTestFiles: [],
294
+ };
295
+
296
+ const plan = createToolcraftDeliveryPlan(planningInputs);
297
+
298
+ assert.equal(plan.kind, "functional");
299
+ assert.deepEqual(plan.steps, [{ kind: "code-health" }]);
300
+ });
301
+
242
302
  test("rejects malformed basis and functional performance proof", () => {
243
303
  const valid = createToolcraftDeliveryPlan(inputs());
244
304
  for (const basis of [
@@ -203,14 +203,19 @@ test("rejects malformed catalogs and inconsistent exact impact relations", () =>
203
203
  }
204
204
  });
205
205
 
206
- test("performance impact cannot omit functional acceptance proof", () => {
206
+ test("impact cannot omit both functional and performance proof", () => {
207
207
  const value = inputs();
208
208
  value.changeSet.impact.acceptanceIds = [];
209
209
  value.changeSet.impact.browserTestNames = [];
210
210
  value.changeSet.impact.productTestFiles = [];
211
+ value.changeSet.impact.performanceCandidates = {
212
+ passIds: [],
213
+ pathIds: [],
214
+ testNames: [],
215
+ };
211
216
  assert.throws(
212
217
  () => createToolcraftDeliveryPlan(value),
213
- /functional proof/iu,
218
+ /functional or performance proof/iu,
214
219
  );
215
220
  });
216
221
 
@@ -124,8 +124,9 @@ function impactError(impact, catalog, allTests) {
124
124
  impact.acceptanceIds.includes(row.acceptanceId));
125
125
  const performance = catalog.performance.filter((row) =>
126
126
  impact.performanceCandidates.pathIds.includes(row.pathId));
127
- if (!impact.browserTestNames.length && !impact.productTestFiles.length)
128
- return "Verification impact does not select functional proof.";
127
+ if (!impact.browserTestNames.length && !impact.productTestFiles.length &&
128
+ !impact.performanceCandidates.pathIds.length)
129
+ return "Verification impact does not select functional or performance proof.";
129
130
  if (acceptance.length !== impact.acceptanceIds.length ||
130
131
  !same(acceptance.map((row) => row.testName).sort(compare), impact.browserTestNames) ||
131
132
  performance.length !== impact.performanceCandidates.pathIds.length ||
@@ -227,7 +228,10 @@ function stepError(step) {
227
228
  if (step.kind === "dependencies" && !managers.has(step.packageManager))
228
229
  return "Dependency proof has an invalid package manager.";
229
230
  const badTarget = ["files", "testNames", "pathIds", "passIds"].find((key) =>
230
- Object.hasOwn(step, key) && !targets(step[key], { paths: key === "files" }));
231
+ Object.hasOwn(step, key) && !targets(step[key], {
232
+ empty: step.kind === "browser-performance" && key === "passIds",
233
+ paths: key === "files",
234
+ }));
231
235
  if (badTarget)
232
236
  return `Delivery proof ${badTarget} must be nonempty, sorted, and unique.`;
233
237
  }
@@ -6,6 +6,11 @@ import {
6
6
  builtInControlExportNames,
7
7
  runtimeSurfaceComponentNames,
8
8
  } from "./toolcraft-integrity-policy.mjs";
9
+ import {
10
+ createToolcraftProductExportInspector,
11
+ getToolcraftProductExportModuleKind,
12
+ isForbiddenToolcraftRuntimeExport,
13
+ } from "./toolcraft-product-export-boundary.mjs";
9
14
  import { inspectToolcraftProductStyleNode } from "./toolcraft-product-style-boundary.mjs";
10
15
 
11
16
  const runtimeSurfaceNames = new Set(runtimeSurfaceComponentNames);
@@ -42,6 +47,11 @@ function getSensitiveModuleKind(sourceFilePath, moduleSpecifier, rootDir) {
42
47
  normalizedSpecifier,
43
48
  rootDir,
44
49
  );
50
+ const exportModuleKind = getToolcraftProductExportModuleKind(
51
+ normalizedSpecifier,
52
+ resolvedSpecifier,
53
+ );
54
+ if (exportModuleKind) return exportModuleKind;
45
55
 
46
56
  if (
47
57
  /^@repo\/toolcraft-runtime\/react\/model-rendering(?:\/|$)/u.test(
@@ -94,6 +104,7 @@ function getSensitiveModuleKind(sourceFilePath, moduleSpecifier, rootDir) {
94
104
  }
95
105
 
96
106
  function isForbiddenNamedExport(moduleKind, importedName) {
107
+ if (isForbiddenToolcraftRuntimeExport(moduleKind, importedName)) return true;
97
108
  if (
98
109
  moduleKind === "runtime-model-import" ||
99
110
  moduleKind === "runtime-model-presentation"
@@ -117,7 +128,9 @@ function createViolation({
117
128
  }) {
118
129
  const subject = importedName ? ` ${importedName}` : "";
119
130
  const message =
120
- kind === "runtime-surface"
131
+ kind === "runtime-export-ownership"
132
+ ? `Product source must not own artifact encoding, recording, or download mechanics${subject} from ${moduleSpecifier}. Declare exportRenderer and let runtime export actions encode and download the artifact.`
133
+ : kind === "runtime-surface"
121
134
  ? `Product source must not import or re-export host-owned runtime surface${subject} from ${moduleSpecifier}. Use ToolcraftAppComposition, schema, canvasContent, controlRenderers, onPanelAction, and runtime commands.`
122
135
  : kind === "runtime-model-presentation-ownership"
123
136
  ? `Product source must not import raw model render hosts or standard model canvas internals${subject} from ${moduleSpecifier}. Declare modelPresentation and use useToolcraftModelPresentationConsumer for visible custom model output.`
@@ -141,6 +154,11 @@ export function inspectToolcraftProductSource({
141
154
  sourceFile,
142
155
  }) {
143
156
  const violations = [];
157
+ const inspectExportMechanics = createToolcraftProductExportInspector({
158
+ getNodeLocation,
159
+ repoPath,
160
+ sourceFile,
161
+ });
144
162
 
145
163
  function report(node, moduleSpecifier, moduleKind, importedName) {
146
164
  if (importedName && !isForbiddenNamedExport(moduleKind, importedName)) {
@@ -151,7 +169,10 @@ export function inspectToolcraftProductSource({
151
169
  getNodeLocation,
152
170
  importedName,
153
171
  kind:
154
- moduleKind === "runtime-model-presentation"
172
+ moduleKind === "runtime-export-api" ||
173
+ moduleKind === "runtime-export-mechanics"
174
+ ? "runtime-export-ownership"
175
+ : moduleKind === "runtime-model-presentation"
155
176
  ? "runtime-model-presentation-ownership"
156
177
  : moduleKind === "runtime-react"
157
178
  ? runtimeModelPresentationInternalNames.has(importedName)
@@ -180,6 +201,7 @@ export function inspectToolcraftProductSource({
180
201
  resolveStaticString,
181
202
  sourceFile,
182
203
  }),
204
+ ...inspectExportMechanics(node),
183
205
  );
184
206
 
185
207
  if (
@@ -4,6 +4,7 @@ import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
 
7
+ import { createToolcraftPrivateEvidenceImportViolation } from "./toolcraft-product-evidence-import-policy.mjs";
7
8
  import { isToolcraftTypeScriptCompilerAvailable } from "./toolcraft-typescript-source-evidence.mjs";
8
9
 
9
10
  function compareCodeUnits(left, right) {
@@ -94,6 +95,15 @@ export async function evaluateToolcraftProductBoundary({
94
95
  importerEvidence.push(evidence);
95
96
  importsByImporter.set(evidence.importerRepoPath, importerEvidence);
96
97
  }
98
+ const appendPrivateEvidenceImportViolations = (entry) => {
99
+ for (const moduleImport of importsByImporter.get(entry.repoPath) ?? []) {
100
+ const violation = createToolcraftPrivateEvidenceImportViolation({
101
+ importerRepoPath: entry.repoPath,
102
+ moduleImport,
103
+ });
104
+ if (violation) violations.push(violation);
105
+ }
106
+ };
97
107
 
98
108
  for (const entry of productEntries) {
99
109
  const sourceRecord = localDependencyGraph.sourceRecords.get(entry.repoPath);
@@ -113,6 +123,7 @@ export async function evaluateToolcraftProductBoundary({
113
123
  ...boundaryEvidence.reservedEvidenceViolations,
114
124
  ...boundaryEvidence.playwrightAuthorityViolations,
115
125
  );
126
+ appendPrivateEvidenceImportViolations(entry);
116
127
  for (const evidence of importsByImporter.get(entry.repoPath) ?? []) {
117
128
  if (evidence.resolution === "outside-inventory") {
118
129
  violations.push({
@@ -165,6 +176,7 @@ export async function evaluateToolcraftProductBoundary({
165
176
  ...boundaryEvidence.playwrightAuthorityViolations,
166
177
  ...boundaryEvidence.moduleLoadingViolations,
167
178
  );
179
+ appendPrivateEvidenceImportViolations(entry);
168
180
  }
169
181
 
170
182
  return {
@@ -143,6 +143,52 @@ test("keeps model import, topology, and repair infrastructure runtime-owned", as
143
143
  );
144
144
  });
145
145
 
146
+ test("keeps artifact encoding and download mechanics runtime-owned", async (context) => {
147
+ const rootDir = await createFixture(context, {
148
+ "src/features/export-mechanics.ts": `
149
+ import { createToolcraftPngExportCanvas } from "@/toolcraft/runtime";
150
+ import { Output } from "mediabunny";
151
+ export function exportArtifact(canvas: HTMLCanvasElement) {
152
+ const recorder = new MediaRecorder(canvas.captureStream());
153
+ const encoder = new VideoEncoder({ error() {}, output() {} });
154
+ canvas.toBlob(() => undefined);
155
+ canvas.toDataURL();
156
+ URL.createObjectURL(new Blob());
157
+ void createToolcraftPngExportCanvas;
158
+ void Output;
159
+ void recorder;
160
+ void encoder;
161
+ }
162
+ `,
163
+ "src/features/safe-renderer.ts": `
164
+ export const renderFrame = ({ context }) => context.fillRect(0, 0, 1, 1);
165
+ `,
166
+ "src/features/shadowed-globals.ts": `
167
+ export function localNames(MediaRecorder, VideoEncoder, URL) {
168
+ void new MediaRecorder();
169
+ void new VideoEncoder();
170
+ URL.createObjectURL();
171
+ }
172
+ `,
173
+ });
174
+
175
+ const result = await evaluateToolcraftProductBoundary({ rootDir });
176
+
177
+ assert.equal(result.violations.length, 8);
178
+ assert.equal(
179
+ result.violations.every(
180
+ (violation) => violation.kind === "runtime-export-ownership",
181
+ ),
182
+ true,
183
+ );
184
+ assert.equal(
185
+ result.violations.some((violation) =>
186
+ violation.repoPath.endsWith("shadowed-globals.ts"),
187
+ ),
188
+ false,
189
+ );
190
+ });
191
+
146
192
  test("ignores tests but checks a product bridge imported by production", async (context) => {
147
193
  const rootDir = await createFixture(context, {
148
194
  "src/features/bridge.ts": `
@@ -12,6 +12,44 @@ const starterRoot = path.resolve(
12
12
  path.dirname(fileURLToPath(import.meta.url)),
13
13
  "..",
14
14
  );
15
+ const unavailableImageEvidenceFacadePath =
16
+ "e2e/browser-infinity-canvas-evidence.ts";
17
+ const unavailableImageEvidencePrivatePath =
18
+ "e2e/browser-infinity-canvas-unavailable-image-evidence.ts";
19
+
20
+ async function evaluateUnavailableImageEvidenceImport(context, specifier) {
21
+ const rootDir = await createFixture(context, {
22
+ "e2e/app-canvas.spec.ts": `
23
+ import { createToolcraftUnavailableImageResourceFixture } from "${specifier}";
24
+ void createToolcraftUnavailableImageResourceFixture;
25
+ `,
26
+ [unavailableImageEvidenceFacadePath]: `
27
+ export {
28
+ createToolcraftUnavailableImageResourceFixture,
29
+ } from "./browser-infinity-canvas-unavailable-image-evidence";
30
+ `,
31
+ [unavailableImageEvidencePrivatePath]: `
32
+ export function createToolcraftUnavailableImageResourceFixture() {}
33
+ `,
34
+ "tsconfig.json": JSON.stringify({
35
+ compilerOptions: {
36
+ baseUrl: ".",
37
+ paths: {
38
+ "@private-proof": [`./${unavailableImageEvidencePrivatePath}`],
39
+ "@public-proof": [`./${unavailableImageEvidenceFacadePath}`],
40
+ },
41
+ },
42
+ }),
43
+ });
44
+
45
+ return evaluateToolcraftProductBoundary({
46
+ protectedFilePaths: [
47
+ unavailableImageEvidenceFacadePath,
48
+ unavailableImageEvidencePrivatePath,
49
+ ],
50
+ rootDir,
51
+ });
52
+ }
15
53
 
16
54
  test("protects every starter helper that writes reserved runtime evidence", async () => {
17
55
  const e2eRoot = path.join(starterRoot, "e2e");
@@ -201,27 +239,54 @@ test("allows product layer specs to use the public proof facade without reserved
201
239
  assert.deepEqual(result.violations, []);
202
240
  });
203
241
 
204
- test("allows product specs to use the unavailable-resource facade but rejects its reserved bridge", async (context) => {
205
- const allowedRoot = await createFixture(context, {
206
- "e2e/app-canvas.spec.ts": `
207
- import {
208
- createToolcraftUnavailableImageResourceFixture,
209
- expectToolcraftInfinityCanvasUnavailableImageExportEvidence,
210
- } from "./browser-infinity-canvas-evidence";
211
- void createToolcraftUnavailableImageResourceFixture;
212
- void expectToolcraftInfinityCanvasUnavailableImageExportEvidence;
213
- `,
214
- "e2e/browser-infinity-canvas-evidence.ts": `
215
- export function createToolcraftUnavailableImageResourceFixture() {}
216
- export async function expectToolcraftInfinityCanvasUnavailableImageExportEvidence() {}
217
- `,
218
- });
219
- const allowed = await evaluateToolcraftProductBoundary({
220
- protectedFilePaths: ["e2e/browser-infinity-canvas-evidence.ts"],
221
- rootDir: allowedRoot,
222
- });
223
- assert.deepEqual(allowed.violations, []);
242
+ test("rejects relative and configured-alias imports of private unavailable-image evidence", async (context) => {
243
+ const results = await Promise.all(
244
+ [
245
+ "./browser-infinity-canvas-unavailable-image-evidence",
246
+ "@private-proof",
247
+ ].map((specifier) =>
248
+ evaluateUnavailableImageEvidenceImport(context, specifier),
249
+ ),
250
+ );
251
+ assert.deepEqual(
252
+ results.map((result) =>
253
+ result.violations.map((violation) => violation.kind),
254
+ ),
255
+ [
256
+ ["reserved-runtime-evidence"],
257
+ ["reserved-runtime-evidence"],
258
+ ],
259
+ );
260
+ for (const result of results) {
261
+ assert.match(result.violations[0].message, /browser-infinity-canvas-evidence/u);
262
+ }
263
+ });
264
+
265
+ test("allows relative and configured-alias imports of the public unavailable-image facade", async (context) => {
266
+ for (const specifier of [
267
+ "./browser-infinity-canvas-evidence",
268
+ "@public-proof",
269
+ ]) {
270
+ const result = await evaluateUnavailableImageEvidenceImport(
271
+ context,
272
+ specifier,
273
+ );
274
+ assert.deepEqual(result.violations, []);
275
+ }
276
+ });
277
+
278
+ test("normalizes private unavailable-image evidence import paths before enforcing policy", async (context) => {
279
+ const result = await evaluateUnavailableImageEvidenceImport(
280
+ context,
281
+ "./nested/../browser-infinity-canvas-unavailable-image-evidence",
282
+ );
283
+ assert.deepEqual(
284
+ result.violations.map((violation) => violation.kind),
285
+ ["reserved-runtime-evidence"],
286
+ );
287
+ });
224
288
 
289
+ test("rejects product access to the reserved unavailable-resource bridge", async (context) => {
225
290
  const rejectedRoot = await createFixture(context, {
226
291
  "e2e/app-canvas.spec.ts": `
227
292
  const fixtureEvent =
@@ -0,0 +1,43 @@
1
+ const PRIVATE_EVIDENCE_MESSAGE =
2
+ "Product-owned source must use the protected public evidence facade instead of importing its private implementation module.";
3
+
4
+ export const toolcraftProductEvidenceModulePolicies = Object.freeze([
5
+ Object.freeze({
6
+ capability: "unavailable-image-resource-evidence",
7
+ privateModuleRepoPath:
8
+ "e2e/browser-infinity-canvas-unavailable-image-evidence.ts",
9
+ publicFacadeRepoPath: "e2e/browser-infinity-canvas-evidence.ts",
10
+ }),
11
+ ]);
12
+
13
+ const privateEvidencePolicyByRepoPath = new Map(
14
+ toolcraftProductEvidenceModulePolicies.map((policy) => [
15
+ policy.privateModuleRepoPath,
16
+ policy,
17
+ ]),
18
+ );
19
+
20
+ function normalizeRepoPath(repoPath) {
21
+ return String(repoPath).replaceAll("\\", "/").replace(/^\.\//u, "");
22
+ }
23
+
24
+ export function createToolcraftPrivateEvidenceImportViolation({
25
+ importerRepoPath,
26
+ moduleImport,
27
+ }) {
28
+ if (moduleImport.resolution !== "resolved") return null;
29
+ const policy = privateEvidencePolicyByRepoPath.get(
30
+ normalizeRepoPath(moduleImport.resolvedRepoPath),
31
+ );
32
+ if (!policy) return null;
33
+
34
+ return {
35
+ column: moduleImport.column,
36
+ kind: "reserved-runtime-evidence",
37
+ line: moduleImport.line,
38
+ message:
39
+ `${PRIVATE_EVIDENCE_MESSAGE} Import ${policy.publicFacadeRepoPath} ` +
40
+ `for ${policy.capability}.`,
41
+ repoPath: importerRepoPath,
42
+ };
43
+ }
@@ -0,0 +1,168 @@
1
+ import ts from "typescript";
2
+
3
+ const forbiddenRuntimeExportNames = new Set([
4
+ "createToolcraftPngExportCanvas",
5
+ ]);
6
+ const forbiddenGlobalExportConstructors = new Set([
7
+ "MediaRecorder",
8
+ "VideoEncoder",
9
+ ]);
10
+ const forbiddenCanvasExportMethods = new Set([
11
+ "captureStream",
12
+ "toBlob",
13
+ "toDataURL",
14
+ ]);
15
+
16
+ export function getToolcraftProductExportModuleKind(
17
+ normalizedSpecifier,
18
+ resolvedSpecifier,
19
+ ) {
20
+ if (/^mediabunny(?:\/|$)/u.test(normalizedSpecifier)) {
21
+ return "runtime-export-mechanics";
22
+ }
23
+ if (
24
+ normalizedSpecifier === "@repo/toolcraft-runtime" ||
25
+ /^[#@]\/toolcraft\/runtime\/?$/u.test(normalizedSpecifier) ||
26
+ /\/src\/toolcraft\/runtime$/u.test(resolvedSpecifier ?? "")
27
+ ) {
28
+ return "runtime-export-api";
29
+ }
30
+ return null;
31
+ }
32
+
33
+ export function isForbiddenToolcraftRuntimeExport(
34
+ moduleKind,
35
+ importedName,
36
+ ) {
37
+ return (
38
+ moduleKind === "runtime-export-mechanics" ||
39
+ (moduleKind === "runtime-export-api" &&
40
+ forbiddenRuntimeExportNames.has(importedName))
41
+ );
42
+ }
43
+
44
+ function getDeclarationScope(node, sourceFile) {
45
+ let current = node.parent;
46
+ while (current) {
47
+ if (
48
+ ts.isSourceFile(current) ||
49
+ ts.isFunctionLike(current) ||
50
+ ts.isBlock(current) ||
51
+ ts.isCatchClause(current)
52
+ ) {
53
+ return current;
54
+ }
55
+ current = current.parent;
56
+ }
57
+ return sourceFile;
58
+ }
59
+
60
+ function collectLexicalDeclarations(sourceFile) {
61
+ const declarations = [];
62
+
63
+ function collectBindingNames(name, scope) {
64
+ if (ts.isIdentifier(name)) {
65
+ declarations.push({ name: name.text, scope });
66
+ } else if (
67
+ ts.isObjectBindingPattern(name) ||
68
+ ts.isArrayBindingPattern(name)
69
+ ) {
70
+ for (const element of name.elements) {
71
+ if (ts.isBindingElement(element)) collectBindingNames(element.name, scope);
72
+ }
73
+ }
74
+ }
75
+
76
+ function visit(node) {
77
+ if (ts.isImportClause(node) && node.name) {
78
+ collectBindingNames(node.name, sourceFile);
79
+ } else if (ts.isImportSpecifier(node) || ts.isNamespaceImport(node)) {
80
+ collectBindingNames(node.name, sourceFile);
81
+ } else if (ts.isVariableDeclaration(node)) {
82
+ collectBindingNames(node.name, getDeclarationScope(node, sourceFile));
83
+ } else if (ts.isParameter(node)) {
84
+ collectBindingNames(node.name, node.parent);
85
+ } else if (
86
+ (ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node)) &&
87
+ node.name
88
+ ) {
89
+ collectBindingNames(node.name, getDeclarationScope(node, sourceFile));
90
+ }
91
+ ts.forEachChild(node, visit);
92
+ }
93
+
94
+ visit(sourceFile);
95
+ return declarations;
96
+ }
97
+
98
+ export function createToolcraftProductExportInspector({
99
+ getNodeLocation,
100
+ repoPath,
101
+ sourceFile,
102
+ }) {
103
+ const lexicalDeclarations = collectLexicalDeclarations(sourceFile);
104
+
105
+ function isLexicallyDeclared(identifier) {
106
+ const ancestorScopes = new Set();
107
+ let current = identifier.parent;
108
+ while (current) {
109
+ if (
110
+ ts.isSourceFile(current) ||
111
+ ts.isFunctionLike(current) ||
112
+ ts.isBlock(current) ||
113
+ ts.isCatchClause(current)
114
+ ) {
115
+ ancestorScopes.add(current);
116
+ }
117
+ current = current.parent;
118
+ }
119
+ return lexicalDeclarations.some(
120
+ (declaration) =>
121
+ declaration.name === identifier.text &&
122
+ ancestorScopes.has(declaration.scope),
123
+ );
124
+ }
125
+
126
+ function violation(node, subject) {
127
+ return {
128
+ ...getNodeLocation(sourceFile, node),
129
+ kind: "runtime-export-ownership",
130
+ message: `Product source must not own ${subject}. Declare exportRenderer and let runtime export actions encode and download the artifact.`,
131
+ repoPath,
132
+ };
133
+ }
134
+
135
+ return (node) => {
136
+ if (
137
+ ts.isNewExpression(node) &&
138
+ ts.isIdentifier(node.expression) &&
139
+ forbiddenGlobalExportConstructors.has(node.expression.text) &&
140
+ !isLexicallyDeclared(node.expression)
141
+ ) {
142
+ return [
143
+ violation(
144
+ node,
145
+ `${node.expression.text} encoder or recorder construction`,
146
+ ),
147
+ ];
148
+ }
149
+ if (
150
+ ts.isCallExpression(node) &&
151
+ ts.isPropertyAccessExpression(node.expression)
152
+ ) {
153
+ const propertyName = node.expression.name.text;
154
+ if (forbiddenCanvasExportMethods.has(propertyName)) {
155
+ return [violation(node, `canvas.${propertyName}() export mechanics`)];
156
+ }
157
+ if (
158
+ propertyName === "createObjectURL" &&
159
+ ts.isIdentifier(node.expression.expression) &&
160
+ node.expression.expression.text === "URL" &&
161
+ !isLexicallyDeclared(node.expression.expression)
162
+ ) {
163
+ return [violation(node, "URL.createObjectURL() artifact delivery")];
164
+ }
165
+ }
166
+ return [];
167
+ };
168
+ }
@@ -0,0 +1,73 @@
1
+ import type ts from "typescript";
2
+
3
+ export type ToolcraftTypeScriptImportedBinding = Readonly<{
4
+ importedName: string;
5
+ localName: string;
6
+ specifier: string;
7
+ }>;
8
+
9
+ export type ToolcraftTypeScriptImportedCallTarget = Readonly<{
10
+ callerCallableId: string | null;
11
+ callerExportNames: readonly string[];
12
+ importedName: string;
13
+ localName: string;
14
+ memberPath: string;
15
+ operation: string;
16
+ specifier: string;
17
+ }>;
18
+
19
+ export type ToolcraftTypeScriptModuleBindings = Readonly<{
20
+ callables: unknown;
21
+ exportedCallableBindings: readonly Readonly<{
22
+ callableId: string;
23
+ exportedName: string;
24
+ }>[];
25
+ exportedRuntimeBindings: readonly string[];
26
+ importedBindingSymbols: readonly Readonly<{
27
+ binding: ToolcraftTypeScriptImportedBinding;
28
+ symbol: ts.Symbol;
29
+ }>[];
30
+ importedRuntimeBindings: readonly ToolcraftTypeScriptImportedBinding[];
31
+ }>;
32
+
33
+ export function createToolcraftTypeScriptChecker(
34
+ sourceFile: ts.SourceFile,
35
+ typescript: typeof ts,
36
+ ): ts.TypeChecker;
37
+
38
+ export function collectToolcraftTypeScriptModuleBindings(
39
+ sourceFile: ts.SourceFile,
40
+ typescript: typeof ts,
41
+ checker?: ts.TypeChecker,
42
+ callables?: unknown,
43
+ ): ToolcraftTypeScriptModuleBindings;
44
+
45
+ export function collectToolcraftTypeScriptCallTargets(input: Readonly<{
46
+ bindings: ToolcraftTypeScriptModuleBindings;
47
+ checker: ts.TypeChecker;
48
+ sourceFile: ts.SourceFile;
49
+ ts: typeof ts;
50
+ }>): Readonly<{
51
+ importedCallTargets: readonly ToolcraftTypeScriptImportedCallTarget[];
52
+ localCallTargets: readonly Readonly<{
53
+ calleeCallableId: string;
54
+ callerCallableId: string;
55
+ }>[];
56
+ }>;
57
+
58
+ export function getToolcraftTypeScriptImportedCallTarget(input: Readonly<{
59
+ bindings: ToolcraftTypeScriptModuleBindings;
60
+ checker: ts.TypeChecker;
61
+ expression: ts.Expression;
62
+ ts: typeof ts;
63
+ }>):
64
+ | Omit<
65
+ ToolcraftTypeScriptImportedCallTarget,
66
+ "callerCallableId" | "callerExportNames"
67
+ >
68
+ | undefined;
69
+
70
+ export function unwrapToolcraftTypeScriptExpression(
71
+ node: ts.Expression,
72
+ typescript: typeof ts,
73
+ ): ts.Expression;