@tiic-tech/openworkflow 0.1.0 → 0.1.2
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 +10 -0
- package/dist/adapters/codex/src/cleanCodexAdapter.d.ts +7 -0
- package/dist/adapters/codex/src/cleanCodexAdapter.js +99 -0
- package/dist/adapters/codex/src/cleanCodexAdapter.js.map +1 -0
- package/dist/adapters/codex/src/constants.d.ts +1 -0
- package/dist/adapters/codex/src/constants.js +2 -0
- package/dist/adapters/codex/src/constants.js.map +1 -0
- package/dist/adapters/codex/src/generateCommands.js +14 -9
- package/dist/adapters/codex/src/generateCommands.js.map +1 -1
- package/dist/adapters/codex/src/generateSkills.js +13 -0
- package/dist/adapters/codex/src/generateSkills.js.map +1 -1
- package/dist/adapters/codex/src/generatedFiles.js +1 -1
- package/dist/adapters/codex/src/manifest.js +11 -2
- package/dist/adapters/codex/src/manifest.js.map +1 -1
- package/dist/adapters/codex/src/templates.d.ts +0 -1
- package/dist/adapters/codex/src/templates.js +0 -1
- package/dist/adapters/codex/src/templates.js.map +1 -1
- package/dist/adapters/src/registry.d.ts +20 -0
- package/dist/adapters/src/registry.js +81 -0
- package/dist/adapters/src/registry.js.map +1 -0
- package/dist/cli/src/commands/brief.d.ts +46 -0
- package/dist/cli/src/commands/brief.js +294 -0
- package/dist/cli/src/commands/brief.js.map +1 -0
- package/dist/cli/src/commands/check.d.ts +42 -0
- package/dist/cli/src/commands/check.js +326 -0
- package/dist/cli/src/commands/check.js.map +1 -0
- package/dist/cli/src/commands/clean.d.ts +1 -0
- package/dist/cli/src/commands/clean.js +98 -0
- package/dist/cli/src/commands/clean.js.map +1 -0
- package/dist/cli/src/commands/context.d.ts +1 -0
- package/dist/cli/src/commands/context.js +471 -0
- package/dist/cli/src/commands/context.js.map +1 -0
- package/dist/cli/src/commands/doctor.js +122 -12
- package/dist/cli/src/commands/doctor.js.map +1 -1
- package/dist/cli/src/commands/draft.d.ts +1 -0
- package/dist/cli/src/commands/draft.js +175 -0
- package/dist/cli/src/commands/draft.js.map +1 -0
- package/dist/cli/src/commands/gitAutomation.d.ts +1 -0
- package/dist/cli/src/commands/gitAutomation.js +378 -0
- package/dist/cli/src/commands/gitAutomation.js.map +1 -0
- package/dist/cli/src/commands/handoff.d.ts +22 -0
- package/dist/cli/src/commands/handoff.js +122 -0
- package/dist/cli/src/commands/handoff.js.map +1 -0
- package/dist/cli/src/commands/init.js +52 -1
- package/dist/cli/src/commands/init.js.map +1 -1
- package/dist/cli/src/commands/inspect.d.ts +23 -0
- package/dist/cli/src/commands/inspect.js +157 -0
- package/dist/cli/src/commands/inspect.js.map +1 -0
- package/dist/cli/src/commands/register.d.ts +1 -0
- package/dist/cli/src/commands/register.js +251 -0
- package/dist/cli/src/commands/register.js.map +1 -0
- package/dist/cli/src/commands/resume.d.ts +59 -0
- package/dist/cli/src/commands/resume.js +280 -0
- package/dist/cli/src/commands/resume.js.map +1 -0
- package/dist/cli/src/commands/shared.js +6 -2
- package/dist/cli/src/commands/shared.js.map +1 -1
- package/dist/cli/src/commands/summaries.d.ts +1 -0
- package/dist/cli/src/commands/summaries.js +77 -0
- package/dist/cli/src/commands/summaries.js.map +1 -0
- package/dist/cli/src/commands/summarize.d.ts +1 -0
- package/dist/cli/src/commands/summarize.js +316 -0
- package/dist/cli/src/commands/summarize.js.map +1 -0
- package/dist/cli/src/commands/sync.js +135 -12
- package/dist/cli/src/commands/sync.js.map +1 -1
- package/dist/cli/src/commands/validate.js +25 -1
- package/dist/cli/src/commands/validate.js.map +1 -1
- package/dist/cli/src/dev/verifyAgentE2E.d.ts +2 -0
- package/dist/cli/src/dev/verifyAgentE2E.js +391 -0
- package/dist/cli/src/dev/verifyAgentE2E.js.map +1 -0
- package/dist/cli/src/dev/verifyCleanCommand.d.ts +2 -0
- package/dist/cli/src/dev/verifyCleanCommand.js +338 -0
- package/dist/cli/src/dev/verifyCleanCommand.js.map +1 -0
- package/dist/cli/src/dev/verifyRuntimeSurface.js +4940 -54
- package/dist/cli/src/dev/verifyRuntimeSurface.js.map +1 -1
- package/dist/cli/src/dev/verifyWorkflowE2E.js +477 -45
- package/dist/cli/src/dev/verifyWorkflowE2E.js.map +1 -1
- package/dist/cli/src/index.js +189 -5
- package/dist/cli/src/index.js.map +1 -1
- package/dist/cli/src/report.d.ts +26 -0
- package/dist/cli/src/report.js +17 -0
- package/dist/cli/src/report.js.map +1 -0
- package/dist/core/src/artifacts/readiness.d.ts +7 -0
- package/dist/core/src/artifacts/readiness.js +240 -0
- package/dist/core/src/artifacts/readiness.js.map +1 -0
- package/dist/core/src/artifacts/registry.d.ts +9 -2
- package/dist/core/src/artifacts/registry.js +687 -60
- package/dist/core/src/artifacts/registry.js.map +1 -1
- package/dist/core/src/commands/registry.js +1425 -146
- package/dist/core/src/commands/registry.js.map +1 -1
- package/dist/core/src/contracts/index.d.ts +1 -1
- package/dist/core/src/fs/index.d.ts +24 -0
- package/dist/core/src/fs/index.js +48 -1
- package/dist/core/src/fs/index.js.map +1 -1
- package/dist/core/src/git/autonomousSimulator.d.ts +46 -0
- package/dist/core/src/git/autonomousSimulator.js +163 -0
- package/dist/core/src/git/autonomousSimulator.js.map +1 -0
- package/dist/core/src/git/branchIdentity.d.ts +19 -0
- package/dist/core/src/git/branchIdentity.js +75 -0
- package/dist/core/src/git/branchIdentity.js.map +1 -0
- package/dist/core/src/git/draftPrPilot.d.ts +47 -0
- package/dist/core/src/git/draftPrPilot.js +196 -0
- package/dist/core/src/git/draftPrPilot.js.map +1 -0
- package/dist/core/src/git/localEvidenceReader.d.ts +21 -0
- package/dist/core/src/git/localEvidenceReader.js +142 -0
- package/dist/core/src/git/localEvidenceReader.js.map +1 -0
- package/dist/core/src/git/localGitAutomation.d.ts +68 -0
- package/dist/core/src/git/localGitAutomation.js +470 -0
- package/dist/core/src/git/localGitAutomation.js.map +1 -0
- package/dist/core/src/git/mergeReadinessCheckpoint.d.ts +31 -0
- package/dist/core/src/git/mergeReadinessCheckpoint.js +110 -0
- package/dist/core/src/git/mergeReadinessCheckpoint.js.map +1 -0
- package/dist/core/src/git/prReadySummary.d.ts +16 -0
- package/dist/core/src/git/prReadySummary.js +144 -0
- package/dist/core/src/git/prReadySummary.js.map +1 -0
- package/dist/core/src/git/remoteReadonlyPlanner.d.ts +60 -0
- package/dist/core/src/git/remoteReadonlyPlanner.js +223 -0
- package/dist/core/src/git/remoteReadonlyPlanner.js.map +1 -0
- package/dist/core/src/onboarding/agentsGuide.d.ts +32 -0
- package/dist/core/src/onboarding/agentsGuide.js +164 -0
- package/dist/core/src/onboarding/agentsGuide.js.map +1 -0
- package/dist/core/src/validators/validateOpenWorkflow.js +1331 -15
- package/dist/core/src/validators/validateOpenWorkflow.js.map +1 -1
- package/dist/core/src/validators/validateRepositoryContracts.js +2327 -306
- package/dist/core/src/validators/validateRepositoryContracts.js.map +1 -1
- package/dist/core/src/workflow/cleanOpenWorkflow.d.ts +18 -0
- package/dist/core/src/workflow/cleanOpenWorkflow.js +124 -0
- package/dist/core/src/workflow/cleanOpenWorkflow.js.map +1 -0
- package/dist/core/src/workflow/doctorOpenWorkflow.d.ts +7 -0
- package/dist/core/src/workflow/doctorOpenWorkflow.js +26 -0
- package/dist/core/src/workflow/doctorOpenWorkflow.js.map +1 -0
- package/dist/core/src/workflow/initOpenWorkflow.d.ts +7 -0
- package/dist/core/src/workflow/initOpenWorkflow.js +96 -8
- package/dist/core/src/workflow/initOpenWorkflow.js.map +1 -1
- package/dist/core/src/workflow/planningQueueResume.d.ts +105 -0
- package/dist/core/src/workflow/planningQueueResume.js +596 -0
- package/dist/core/src/workflow/planningQueueResume.js.map +1 -0
- package/dist/core/src/workflow/readWorkflowConfig.d.ts +6 -0
- package/dist/core/src/workflow/readWorkflowConfig.js +28 -0
- package/dist/core/src/workflow/readWorkflowConfig.js.map +1 -0
- package/dist/core/src/workflow/summaryHealth.d.ts +60 -0
- package/dist/core/src/workflow/summaryHealth.js +713 -0
- package/dist/core/src/workflow/summaryHealth.js.map +1 -0
- package/dist/core/src/workflow/syncOpenWorkflow.d.ts +22 -0
- package/dist/core/src/workflow/syncOpenWorkflow.js +235 -0
- package/dist/core/src/workflow/syncOpenWorkflow.js.map +1 -0
- package/package.json +4 -2
- package/references/artifact-authoring-templates.md +14 -12
- package/references/artifact-instruction-envelope.md +133 -0
- package/references/coder-continuous-growth-loop.md +68 -0
- package/references/gh-operation-governance.md +114 -0
- package/references/git-automation-governance.md +324 -0
- package/references/git-version-control-governance.md +227 -0
- package/references/internal-coder-protocol.md +202 -0
- package/references/issue-governance.md +115 -0
- package/references/planning-artifact-contracts.md +595 -0
- package/references/planning-skill-runtime-exposure.md +159 -0
- package/references/proto-redesign-artifact-contracts.md +217 -0
- package/references/proto2html-artifact-contracts.md +113 -0
- package/references/skill-system-lifecycle.md +198 -0
- package/references/validation-trust-domains.md +286 -0
- package/references/workflow-blueprint-runtime-alignment.md +287 -0
- package/schemas/atom-tasks.schema.json +101 -0
- package/schemas/candidate-changes.schema.json +323 -0
- package/schemas/current-state.schema.json +113 -0
- package/schemas/html-prototype.schema.json +288 -0
- package/schemas/openworkflow-contract.schema.json +9 -1
- package/schemas/proto-prompt-pack.schema.json +1333 -0
- package/schemas/prototype-evidence.schema.json +684 -142
- package/schemas/selected-change.schema.json +104 -0
- package/schemas/validation-target.schema.json +187 -1
- package/schemas/validation.schema.json +187 -1
- package/schemas/vision-session.schema.json +151 -0
- package/skills/analyze-changes/SKILL.md +92 -0
- package/skills/analyze-changes/agents/openai.yaml +4 -0
- package/skills/analyze-changes/references/analysis-protocol.md +116 -0
- package/skills/build-proto-prompt/SKILL.md +125 -0
- package/skills/build-proto-prompt/references/output-boundary.md +54 -0
- package/skills/build-proto-prompt/references/prompt-pack-compiler-protocol.md +80 -0
- package/skills/build-prototype/SKILL.md +162 -38
- package/skills/build-prototype/agents/openai.yaml +2 -2
- package/skills/build-prototype/references/philosophy-engine.md +61 -0
- package/skills/build-prototype/references/strategic-prompt-pack-protocol.md +365 -0
- package/skills/build-prototype/references/vision2prompt/01_input_contract.md +84 -0
- package/skills/build-prototype/references/vision2prompt/02_vision_decomposition.md +108 -0
- package/skills/build-prototype/references/vision2prompt/03_strategy_hypothesis_generation.md +89 -0
- package/skills/build-prototype/references/vision2prompt/04_product_system_extraction.md +78 -0
- package/skills/build-prototype/references/vision2prompt/05_prototype_prompt_schema.md +189 -0
- package/skills/build-prototype/references/vision2prompt/06_output_templates.md +125 -0
- package/skills/build-prototype/references/vision2prompt/07_quality_rubric.md +171 -0
- package/skills/build-validation/SKILL.md +136 -54
- package/skills/build-validation/references/prototype-validation-target-rubric.md +35 -0
- package/skills/build-validation/references/return-to-vision-gate.md +32 -0
- package/skills/build-vision/SKILL.md +192 -0
- package/skills/build-vision/references/proto-readiness-rubric.md +48 -0
- package/skills/build-vision/references/vision-interview-protocol.md +48 -0
- package/skills/coder/SKILL.md +204 -0
- package/skills/decompose-to-changes/SKILL.md +176 -0
- package/skills/decompose-to-changes/agents/openai.yaml +4 -0
- package/skills/decompose-to-changes/references/decomposition-protocol.md +278 -0
- package/skills/prompt2proto/SKILL.md +157 -0
- package/skills/prompt2proto/agents/openai.yaml +4 -0
- package/skills/prompt2proto/references/00_role_philosophy_engine.md +96 -0
- package/skills/prompt2proto/references/01_input_contract.md +53 -0
- package/skills/prompt2proto/references/02_prompt_pack_readiness.md +50 -0
- package/skills/prompt2proto/references/03_visual_translation_workflow.md +64 -0
- package/skills/prompt2proto/references/04_output_contract.md +67 -0
- package/skills/prompt2proto/references/05_quality_rubric.md +46 -0
- package/skills/proto2html/SKILL.md +136 -0
- package/skills/proto2html/agents/openai.yaml +4 -0
- package/skills/proto2html/references/proto2html-protocol.md +115 -0
- package/skills/run-team/SKILL.md +4 -0
- package/skills/select-change/SKILL.md +200 -0
- package/skills/select-change/agents/openai.yaml +4 -0
- package/skills/select-change/references/selection-protocol.md +281 -0
- package/skills/tune-prototype/SKILL.md +121 -0
- package/skills/tune-prototype/agents/openai.yaml +4 -0
- package/skills/tune-prototype/references/refined-prompt-pack-protocol.md +161 -0
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { readdir, readFile, stat } from "node:fs/promises";
|
|
2
2
|
import { statSync } from "node:fs";
|
|
3
3
|
import { basename, join, relative, resolve } from "node:path";
|
|
4
|
+
import { getWorkflowCommands } from "../commands/registry.js";
|
|
4
5
|
import { SCHEMA_VERSION } from "../contracts/index.js";
|
|
5
6
|
import { parseYaml } from "../contracts/yaml.js";
|
|
6
|
-
import { isNotFound } from "../fs/index.js";
|
|
7
|
+
import { isExternalReference, isNotFound, resolveLocalReference } from "../fs/index.js";
|
|
8
|
+
import { assessBranchIdentity, branchIdentityExceptionFrom } from "../git/branchIdentity.js";
|
|
7
9
|
const REQUIRED_FILES = [
|
|
8
10
|
"AGENT.md",
|
|
9
11
|
"README.md",
|
|
@@ -16,7 +18,9 @@ const REQUIRED_FILES = [
|
|
|
16
18
|
"references/discovery-artifact-contracts.md",
|
|
17
19
|
"references/artifact-authoring-templates.md",
|
|
18
20
|
"references/runtime-command-surface.md",
|
|
21
|
+
"references/proto2html-artifact-contracts.md",
|
|
19
22
|
"schemas/openworkflow-contract.schema.json",
|
|
23
|
+
"schemas/current-state.schema.json",
|
|
20
24
|
"schemas/workflow-index.schema.json",
|
|
21
25
|
"schemas/contract-graph.schema.json",
|
|
22
26
|
"schemas/artifact-contracts.schema.json",
|
|
@@ -24,6 +28,7 @@ const REQUIRED_FILES = [
|
|
|
24
28
|
"schemas/vision-session.schema.json",
|
|
25
29
|
"schemas/validation-target.schema.json",
|
|
26
30
|
"schemas/prototype-evidence.schema.json",
|
|
31
|
+
"schemas/html-prototype.schema.json",
|
|
27
32
|
"schemas/decision-record.schema.json",
|
|
28
33
|
"schemas/product-design.schema.json",
|
|
29
34
|
"schemas/change.schema.json",
|
|
@@ -33,19 +38,39 @@ const REQUIRED_FILES = [
|
|
|
33
38
|
"package.json",
|
|
34
39
|
"tsconfig.json",
|
|
35
40
|
"packages/cli/src/index.ts",
|
|
41
|
+
"packages/cli/src/report.ts",
|
|
42
|
+
"packages/cli/src/commands/brief.ts",
|
|
43
|
+
"packages/cli/src/commands/check.ts",
|
|
44
|
+
"packages/cli/src/commands/clean.ts",
|
|
45
|
+
"packages/cli/src/commands/context.ts",
|
|
46
|
+
"packages/cli/src/commands/draft.ts",
|
|
36
47
|
"packages/cli/src/commands/init.ts",
|
|
48
|
+
"packages/cli/src/commands/inspect.ts",
|
|
49
|
+
"packages/cli/src/commands/register.ts",
|
|
50
|
+
"packages/cli/src/commands/summaries.ts",
|
|
51
|
+
"packages/cli/src/commands/summarize.ts",
|
|
37
52
|
"packages/cli/src/commands/validate.ts",
|
|
38
53
|
"packages/cli/src/commands/sync.ts",
|
|
39
54
|
"packages/cli/src/commands/doctor.ts",
|
|
40
55
|
"packages/cli/src/dev/validateRepositoryContractsCli.ts",
|
|
41
56
|
"packages/cli/src/dev/verifyRuntimeSurface.ts",
|
|
42
57
|
"packages/cli/src/dev/verifyWorkflowE2E.ts",
|
|
58
|
+
"packages/cli/src/dev/verifyAgentE2E.ts",
|
|
59
|
+
"packages/cli/src/dev/verifyCleanCommand.ts",
|
|
60
|
+
"packages/adapters/src/registry.ts",
|
|
43
61
|
"packages/core/src/artifacts/registry.ts",
|
|
62
|
+
"packages/core/src/artifacts/readiness.ts",
|
|
44
63
|
"packages/core/src/contracts/index.ts",
|
|
45
64
|
"packages/core/src/contracts/yaml.ts",
|
|
46
65
|
"packages/core/src/commands/registry.ts",
|
|
47
66
|
"packages/core/src/fs/index.ts",
|
|
67
|
+
"packages/core/src/onboarding/agentsGuide.ts",
|
|
68
|
+
"packages/core/src/workflow/doctorOpenWorkflow.ts",
|
|
48
69
|
"packages/core/src/workflow/initOpenWorkflow.ts",
|
|
70
|
+
"packages/core/src/workflow/readWorkflowConfig.ts",
|
|
71
|
+
"packages/core/src/workflow/cleanOpenWorkflow.ts",
|
|
72
|
+
"packages/core/src/workflow/summaryHealth.ts",
|
|
73
|
+
"packages/core/src/workflow/syncOpenWorkflow.ts",
|
|
49
74
|
"packages/core/src/validators/validateOpenWorkflow.ts",
|
|
50
75
|
"packages/core/src/validators/validateRepositoryContracts.ts",
|
|
51
76
|
"packages/core/src/graph/README.md",
|
|
@@ -53,9 +78,11 @@ const REQUIRED_FILES = [
|
|
|
53
78
|
"packages/adapters/codex/src/generateCommands.ts",
|
|
54
79
|
"packages/adapters/codex/src/generateSkills.ts",
|
|
55
80
|
"packages/adapters/codex/src/doctorCodexAdapter.ts",
|
|
81
|
+
"packages/adapters/codex/src/cleanCodexAdapter.ts",
|
|
56
82
|
"packages/adapters/codex/src/templates.ts",
|
|
57
83
|
"templates/openworkflow/README.md",
|
|
58
84
|
"templates/codex/README.md",
|
|
85
|
+
"skills/build-vision/SKILL.md",
|
|
59
86
|
"skills/build-validation/SKILL.md",
|
|
60
87
|
"skills/build-validation/scripts/init_validation.py",
|
|
61
88
|
"skills/build-prototype/SKILL.md",
|
|
@@ -107,15 +134,107 @@ const REQUIRED_FILES = [
|
|
|
107
134
|
"changes/M20-workflow-e2e-regression/WORK_ITEMS.yaml",
|
|
108
135
|
"changes/M21-npm-package-release-readiness/CHANGE.yaml",
|
|
109
136
|
"changes/M21-npm-package-release-readiness/WORK_ITEMS.yaml",
|
|
137
|
+
"changes/M22-project-clean-command/CHANGE.yaml",
|
|
138
|
+
"changes/M22-project-clean-command/WORK_ITEMS.yaml",
|
|
139
|
+
"changes/M23-production-command-lazy-contracts/CHANGE.yaml",
|
|
140
|
+
"changes/M23-production-command-lazy-contracts/WORK_ITEMS.yaml",
|
|
141
|
+
"changes/M24-agent-context-state-and-summaries/CHANGE.yaml",
|
|
142
|
+
"changes/M24-agent-context-state-and-summaries/WORK_ITEMS.yaml",
|
|
143
|
+
"changes/M25-agent-onboarding-entrypoint/CHANGE.yaml",
|
|
144
|
+
"changes/M25-agent-onboarding-entrypoint/WORK_ITEMS.yaml",
|
|
145
|
+
"changes/M26-non-destructive-multi-platform-sync/CHANGE.yaml",
|
|
146
|
+
"changes/M26-non-destructive-multi-platform-sync/WORK_ITEMS.yaml",
|
|
147
|
+
"changes/M27-agent-brief-status/CHANGE.yaml",
|
|
148
|
+
"changes/M27-agent-brief-status/WORK_ITEMS.yaml",
|
|
149
|
+
"changes/M28-cli-json-report-surface/CHANGE.yaml",
|
|
150
|
+
"changes/M28-cli-json-report-surface/WORK_ITEMS.yaml",
|
|
151
|
+
"changes/M29-command-readiness-check/CHANGE.yaml",
|
|
152
|
+
"changes/M29-command-readiness-check/WORK_ITEMS.yaml",
|
|
153
|
+
"changes/M30-artifact-summary-health/CHANGE.yaml",
|
|
154
|
+
"changes/M30-artifact-summary-health/WORK_ITEMS.yaml",
|
|
155
|
+
"changes/M31-unified-health-semantics/CHANGE.yaml",
|
|
156
|
+
"changes/M31-unified-health-semantics/WORK_ITEMS.yaml",
|
|
157
|
+
"changes/M32-agent-inspect-entry/CHANGE.yaml",
|
|
158
|
+
"changes/M32-agent-inspect-entry/WORK_ITEMS.yaml",
|
|
159
|
+
"changes/M33-summary-refresh-command/CHANGE.yaml",
|
|
160
|
+
"changes/M33-summary-refresh-command/WORK_ITEMS.yaml",
|
|
161
|
+
"changes/M34-agent-context-packet/CHANGE.yaml",
|
|
162
|
+
"changes/M34-agent-context-packet/WORK_ITEMS.yaml",
|
|
163
|
+
"changes/M35-artifact-draft-command/CHANGE.yaml",
|
|
164
|
+
"changes/M35-artifact-draft-command/WORK_ITEMS.yaml",
|
|
165
|
+
"changes/M36-artifact-register-command/CHANGE.yaml",
|
|
166
|
+
"changes/M36-artifact-register-command/WORK_ITEMS.yaml",
|
|
167
|
+
"changes/M37-managed-clean-boundary/CHANGE.yaml",
|
|
168
|
+
"changes/M37-managed-clean-boundary/WORK_ITEMS.yaml",
|
|
169
|
+
"changes/M38-json-exit-code-semantics/CHANGE.yaml",
|
|
170
|
+
"changes/M38-json-exit-code-semantics/WORK_ITEMS.yaml",
|
|
171
|
+
"changes/M39-stage-readiness-gates/CHANGE.yaml",
|
|
172
|
+
"changes/M39-stage-readiness-gates/WORK_ITEMS.yaml",
|
|
173
|
+
"changes/M40-compact-context-default/CHANGE.yaml",
|
|
174
|
+
"changes/M40-compact-context-default/WORK_ITEMS.yaml",
|
|
175
|
+
"changes/M41-compact-command-audit-slice/CHANGE.yaml",
|
|
176
|
+
"changes/M41-compact-command-audit-slice/WORK_ITEMS.yaml",
|
|
177
|
+
"changes/M42-health-errors-surface/CHANGE.yaml",
|
|
178
|
+
"changes/M42-health-errors-surface/WORK_ITEMS.yaml",
|
|
179
|
+
"changes/M43-summary-quality-signals/CHANGE.yaml",
|
|
180
|
+
"changes/M43-summary-quality-signals/WORK_ITEMS.yaml",
|
|
181
|
+
"changes/M44-clean-sync-recovery-e2e/CHANGE.yaml",
|
|
182
|
+
"changes/M44-clean-sync-recovery-e2e/WORK_ITEMS.yaml",
|
|
183
|
+
"changes/M45-sync-state-reconciliation/CHANGE.yaml",
|
|
184
|
+
"changes/M45-sync-state-reconciliation/WORK_ITEMS.yaml",
|
|
185
|
+
"changes/M46-strict-summary-quality/CHANGE.yaml",
|
|
186
|
+
"changes/M46-strict-summary-quality/WORK_ITEMS.yaml",
|
|
187
|
+
"changes/M47-doctor-handoff-quality-split/CHANGE.yaml",
|
|
188
|
+
"changes/M47-doctor-handoff-quality-split/WORK_ITEMS.yaml",
|
|
189
|
+
"changes/M48-handoff-quality-summary/CHANGE.yaml",
|
|
190
|
+
"changes/M48-handoff-quality-summary/WORK_ITEMS.yaml",
|
|
191
|
+
"changes/M49-single-handoff-entry/CHANGE.yaml",
|
|
192
|
+
"changes/M49-single-handoff-entry/WORK_ITEMS.yaml",
|
|
193
|
+
"changes/M50-context-handoff-mode/CHANGE.yaml",
|
|
194
|
+
"changes/M50-context-handoff-mode/WORK_ITEMS.yaml",
|
|
195
|
+
"changes/M51-agent-first-e2e-suite/CHANGE.yaml",
|
|
196
|
+
"changes/M51-agent-first-e2e-suite/WORK_ITEMS.yaml",
|
|
197
|
+
"changes/M52-default-sync-adapter-recovery/CHANGE.yaml",
|
|
198
|
+
"changes/M52-default-sync-adapter-recovery/WORK_ITEMS.yaml",
|
|
110
199
|
];
|
|
111
200
|
const IGNORED_DIRS = new Set([".git", "node_modules", "dist", "build", "coverage"]);
|
|
112
201
|
const COMMON_REQUIRED = ["schema_version", "contract_id", "contract_type", "title", "status"];
|
|
202
|
+
const CODEX_SKILL_METADATA_FIELDS = [
|
|
203
|
+
"generated_by",
|
|
204
|
+
"adapter",
|
|
205
|
+
"adapter_version",
|
|
206
|
+
"template_id",
|
|
207
|
+
"source_command_id",
|
|
208
|
+
"semantic_trigger",
|
|
209
|
+
"skill_name",
|
|
210
|
+
];
|
|
211
|
+
const REQUIRED_CODEX_SKILL_BLOCKS = [
|
|
212
|
+
"user_behavior",
|
|
213
|
+
"agent_protocol",
|
|
214
|
+
"working_protocol",
|
|
215
|
+
"artifact_checkpoint",
|
|
216
|
+
"codex_skill",
|
|
217
|
+
];
|
|
218
|
+
const HIGH_RISK_REPORT_SECTIONS = [
|
|
219
|
+
"Trigger",
|
|
220
|
+
"Change",
|
|
221
|
+
"Concrete Risks",
|
|
222
|
+
"Decision Options",
|
|
223
|
+
"Recommended Path",
|
|
224
|
+
"Guardrails",
|
|
225
|
+
"Go Criteria",
|
|
226
|
+
"Stop Criteria",
|
|
227
|
+
"Validation Expectations",
|
|
228
|
+
];
|
|
113
229
|
export async function validateRepositoryContracts(rootInput) {
|
|
114
230
|
const root = resolve(rootInput);
|
|
115
231
|
const errors = [];
|
|
116
232
|
await validateRequiredFiles(root, errors);
|
|
117
233
|
await validateJsonSchemas(root, errors);
|
|
118
234
|
await validateYamlContracts(root, errors);
|
|
235
|
+
await validateGeneratedCodexSkills(root, errors);
|
|
236
|
+
await validateGeneratedSurfaceParity(root, errors);
|
|
237
|
+
await validateHighRiskDecisionReports(root, errors);
|
|
119
238
|
return { ok: errors.length === 0, errors };
|
|
120
239
|
}
|
|
121
240
|
async function validateRequiredFiles(root, errors) {
|
|
@@ -165,417 +284,2267 @@ async function validateYamlContracts(root, errors) {
|
|
|
165
284
|
validatePrototype(root, path, data, errors);
|
|
166
285
|
validateArtifactContracts(root, path, data, errors);
|
|
167
286
|
validateDisclosureLevels(root, path, data, errors);
|
|
287
|
+
validateConfig(root, path, data, errors);
|
|
288
|
+
validateCurrentState(root, path, data, errors);
|
|
168
289
|
validateActivePointer(root, path, data, errors);
|
|
169
290
|
validateDiscoveryArtifact(root, path, data, errors);
|
|
170
291
|
validateWorkflowIndex(root, path, data, errors);
|
|
171
292
|
validateContractGraph(root, path, data, errors);
|
|
293
|
+
await validateCandidateChanges(root, path, data, errors);
|
|
294
|
+
validateLocalCommitEvidence(root, path, data, errors);
|
|
172
295
|
}
|
|
173
296
|
}
|
|
174
297
|
}
|
|
175
|
-
function
|
|
176
|
-
|
|
298
|
+
async function validateGeneratedCodexSkills(root, errors) {
|
|
299
|
+
const manifestPath = join(root, ".agents", "openworkflow-adapter.yaml");
|
|
300
|
+
if (!(await exists(manifestPath))) {
|
|
177
301
|
return;
|
|
178
302
|
}
|
|
179
|
-
|
|
303
|
+
const label = relative(root, manifestPath);
|
|
304
|
+
let manifest;
|
|
305
|
+
try {
|
|
306
|
+
manifest = parseYaml(await readFile(manifestPath, "utf8"));
|
|
307
|
+
}
|
|
308
|
+
catch (error) {
|
|
309
|
+
errors.push(`${label} is not valid YAML for generated skill validation: ${messageFor(error)}`);
|
|
180
310
|
return;
|
|
181
311
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
312
|
+
if (!isRecord(manifest)) {
|
|
313
|
+
errors.push(`${label} must be a mapping`);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (manifest.generated_by !== "openworkflow") {
|
|
317
|
+
errors.push(`${label} generated_by must be openworkflow for Codex skill validation`);
|
|
318
|
+
}
|
|
319
|
+
if (manifest.adapter !== "codex") {
|
|
320
|
+
errors.push(`${label} adapter must be codex for Codex skill validation`);
|
|
321
|
+
}
|
|
322
|
+
const adapterVersion = typeof manifest.adapter_version === "string" ? manifest.adapter_version : null;
|
|
323
|
+
if (!adapterVersion) {
|
|
324
|
+
errors.push(`${label} adapter_version must be a non-empty string`);
|
|
325
|
+
}
|
|
326
|
+
const namespace = typeof manifest.command_namespace === "string" ? manifest.command_namespace : "ow";
|
|
327
|
+
validateManifestSkillSurface(label, manifest.skill_surface, errors);
|
|
328
|
+
const commands = manifest.commands;
|
|
329
|
+
if (!Array.isArray(commands) || commands.length === 0) {
|
|
330
|
+
errors.push(`${label} commands must be a non-empty list`);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
for (const [index, command] of commands.entries()) {
|
|
334
|
+
if (!isRecord(command)) {
|
|
335
|
+
errors.push(`${label} command ${index} must be a mapping`);
|
|
336
|
+
continue;
|
|
186
337
|
}
|
|
338
|
+
await validateGeneratedCodexSkill(root, label, command, namespace, adapterVersion ?? "", errors);
|
|
187
339
|
}
|
|
188
|
-
|
|
189
|
-
|
|
340
|
+
}
|
|
341
|
+
function validateManifestSkillSurface(label, value, errors) {
|
|
342
|
+
if (!isRecord(value)) {
|
|
343
|
+
errors.push(`${label} skill_surface must be a mapping`);
|
|
344
|
+
return;
|
|
190
345
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
346
|
+
const frontmatter = value.frontmatter;
|
|
347
|
+
if (!Array.isArray(frontmatter) || !["name", "description", "metadata"].every((field) => frontmatter.includes(field))) {
|
|
348
|
+
errors.push(`${label} skill_surface.frontmatter must include name, description, and metadata`);
|
|
349
|
+
}
|
|
350
|
+
const metadataFields = value.metadata_fields;
|
|
351
|
+
if (!Array.isArray(metadataFields)) {
|
|
352
|
+
errors.push(`${label} skill_surface.metadata_fields must list generated metadata fields`);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
for (const field of CODEX_SKILL_METADATA_FIELDS) {
|
|
356
|
+
if (!metadataFields.includes(field)) {
|
|
357
|
+
errors.push(`${label} skill_surface.metadata_fields missing ${field}`);
|
|
199
358
|
}
|
|
200
359
|
}
|
|
201
360
|
}
|
|
202
|
-
function
|
|
203
|
-
|
|
361
|
+
async function validateGeneratedCodexSkill(root, manifestLabel, command, namespace, adapterVersion, errors) {
|
|
362
|
+
const commandId = stringField(command, "id");
|
|
363
|
+
const trigger = stringField(command, "trigger");
|
|
364
|
+
const skillName = stringField(command, "skill_name");
|
|
365
|
+
const skillPath = stringField(command, "skill_path");
|
|
366
|
+
const commandLabel = `${manifestLabel} command ${commandId || "<unknown>"}`;
|
|
367
|
+
if (!commandId || !trigger || !skillName || !skillPath) {
|
|
368
|
+
errors.push(`${commandLabel} must include id, trigger, skill_name, and skill_path`);
|
|
204
369
|
return;
|
|
205
370
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
371
|
+
const absoluteSkillPath = join(root, skillPath);
|
|
372
|
+
if (!existsSyncSafe(absoluteSkillPath)) {
|
|
373
|
+
errors.push(`${commandLabel} references missing generated skill ${skillPath}; update adapter source and run openworkflow sync --tools codex`);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
let content;
|
|
377
|
+
try {
|
|
378
|
+
if (!statSync(absoluteSkillPath).isFile()) {
|
|
379
|
+
errors.push(`${skillPath} must be a generated skill file`);
|
|
380
|
+
return;
|
|
209
381
|
}
|
|
382
|
+
content = await readFile(absoluteSkillPath, "utf8");
|
|
210
383
|
}
|
|
211
|
-
|
|
212
|
-
errors.push(`${
|
|
384
|
+
catch (error) {
|
|
385
|
+
errors.push(`${skillPath} could not be read for generated skill validation: ${messageFor(error)}`);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const frontmatter = parseSkillFrontmatter(skillPath, content, errors);
|
|
389
|
+
if (!frontmatter) {
|
|
390
|
+
return;
|
|
213
391
|
}
|
|
392
|
+
validateSkillFrontmatter(skillPath, frontmatter, {
|
|
393
|
+
adapterVersion,
|
|
394
|
+
commandId,
|
|
395
|
+
namespace,
|
|
396
|
+
skillName,
|
|
397
|
+
trigger,
|
|
398
|
+
}, errors);
|
|
399
|
+
validateSkillGeneratedMarker(skillPath, content, namespace, commandId, errors);
|
|
400
|
+
validateSkillProtocolBlocks(skillPath, content, errors);
|
|
214
401
|
}
|
|
215
|
-
function
|
|
216
|
-
if (
|
|
402
|
+
function parseSkillFrontmatter(skillPath, content, errors) {
|
|
403
|
+
if (!content.startsWith("---\n")) {
|
|
404
|
+
errors.push(`${skillPath} missing SKILL.md frontmatter; update adapter source and run openworkflow sync --tools codex`);
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
const frontmatterEnd = content.indexOf("\n---\n", 4);
|
|
408
|
+
if (frontmatterEnd === -1) {
|
|
409
|
+
errors.push(`${skillPath} has unterminated SKILL.md frontmatter`);
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
let frontmatter;
|
|
413
|
+
try {
|
|
414
|
+
frontmatter = parseYaml(content.slice(4, frontmatterEnd));
|
|
415
|
+
}
|
|
416
|
+
catch (error) {
|
|
417
|
+
errors.push(`${skillPath} has invalid SKILL.md frontmatter YAML: ${messageFor(error)}`);
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
if (!isRecord(frontmatter)) {
|
|
421
|
+
errors.push(`${skillPath} SKILL.md frontmatter must be a mapping`);
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
return frontmatter;
|
|
425
|
+
}
|
|
426
|
+
function validateSkillFrontmatter(skillPath, frontmatter, expected, errors) {
|
|
427
|
+
if (frontmatter.name !== expected.skillName) {
|
|
428
|
+
errors.push(`${skillPath} frontmatter.name must be ${expected.skillName}`);
|
|
429
|
+
}
|
|
430
|
+
if (!nonEmptyString(frontmatter.description)) {
|
|
431
|
+
errors.push(`${skillPath} frontmatter.description must be a non-empty string`);
|
|
432
|
+
}
|
|
433
|
+
const metadata = frontmatter.metadata;
|
|
434
|
+
if (!isRecord(metadata)) {
|
|
435
|
+
errors.push(`${skillPath} missing generated metadata; update adapter source and run openworkflow sync --tools codex`);
|
|
217
436
|
return;
|
|
218
437
|
}
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
438
|
+
const expectedMetadata = {
|
|
439
|
+
generated_by: "openworkflow",
|
|
440
|
+
adapter: "codex",
|
|
441
|
+
adapter_version: expected.adapterVersion,
|
|
442
|
+
template_id: `codex.skill.${expected.namespace}.${expected.commandId}`,
|
|
443
|
+
source_command_id: expected.commandId,
|
|
444
|
+
semantic_trigger: expected.trigger,
|
|
445
|
+
skill_name: expected.skillName,
|
|
446
|
+
};
|
|
447
|
+
for (const field of CODEX_SKILL_METADATA_FIELDS) {
|
|
448
|
+
if (metadata[field] !== expectedMetadata[field]) {
|
|
449
|
+
errors.push(`${skillPath} metadata.${field} must be ${expectedMetadata[field]}`);
|
|
450
|
+
}
|
|
222
451
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
452
|
+
}
|
|
453
|
+
function validateSkillGeneratedMarker(skillPath, content, namespace, commandId, errors) {
|
|
454
|
+
if (!content.includes("generated-by: openworkflow")) {
|
|
455
|
+
errors.push(`${skillPath} missing generated marker; update adapter source and run openworkflow sync --tools codex`);
|
|
226
456
|
}
|
|
227
|
-
|
|
228
|
-
|
|
457
|
+
const templateMarker = `template-id: codex.skill.${namespace}.${commandId}`;
|
|
458
|
+
if (!content.includes(templateMarker)) {
|
|
459
|
+
errors.push(`${skillPath} missing generated marker ${templateMarker}`);
|
|
229
460
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
461
|
+
}
|
|
462
|
+
function validateSkillProtocolBlocks(skillPath, content, errors) {
|
|
463
|
+
if (/<\/?skill(?:\s|>)/.test(content)) {
|
|
464
|
+
errors.push(`${skillPath} must not use a top-level <skill> XML wrapper; generated skills are Markdown with YAML frontmatter and XML-like protocol blocks`);
|
|
465
|
+
}
|
|
466
|
+
for (const tag of REQUIRED_CODEX_SKILL_BLOCKS) {
|
|
467
|
+
validateRequiredSkillBlock(skillPath, content, tag, errors);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
function validateRequiredSkillBlock(skillPath, content, tag, errors) {
|
|
471
|
+
const openTag = `<${tag}>`;
|
|
472
|
+
const closeTag = `</${tag}>`;
|
|
473
|
+
const openCount = countOccurrences(content, openTag);
|
|
474
|
+
const closeCount = countOccurrences(content, closeTag);
|
|
475
|
+
if (openCount === 0 || closeCount === 0 || openCount !== closeCount) {
|
|
476
|
+
errors.push(`${skillPath} must contain a balanced <${tag}> protocol block`);
|
|
233
477
|
return;
|
|
234
478
|
}
|
|
235
|
-
|
|
236
|
-
|
|
479
|
+
if (tag !== "artifact_checkpoint" && (openCount !== 1 || closeCount !== 1)) {
|
|
480
|
+
errors.push(`${skillPath} must contain exactly one <${tag}> protocol block`);
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
if (content.indexOf(openTag) > content.indexOf(closeTag)) {
|
|
484
|
+
errors.push(`${skillPath} <${tag}> protocol block closes before it opens`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
function countOccurrences(content, needle) {
|
|
488
|
+
return content.split(needle).length - 1;
|
|
489
|
+
}
|
|
490
|
+
function stringField(record, key) {
|
|
491
|
+
const value = record[key];
|
|
492
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
493
|
+
}
|
|
494
|
+
function recordField(record, key) {
|
|
495
|
+
const value = record[key];
|
|
496
|
+
return isRecord(value) ? value : {};
|
|
497
|
+
}
|
|
498
|
+
async function validateGeneratedSurfaceParity(root, errors) {
|
|
499
|
+
const commands = [...getWorkflowCommands()];
|
|
500
|
+
const commandAudit = await readYamlRecordForParity(root, ".openworkflow/audit/COMMAND_AUDIT_INDEX.yaml", errors);
|
|
501
|
+
if (commandAudit) {
|
|
502
|
+
validateCommandAuditParity(".openworkflow/audit/COMMAND_AUDIT_INDEX.yaml", commandAudit.commands, commands, errors);
|
|
503
|
+
}
|
|
504
|
+
const contextPackets = await readYamlRecordForParity(root, ".openworkflow/audit/CONTEXT_PACKETS.yaml", errors);
|
|
505
|
+
if (contextPackets) {
|
|
506
|
+
validateContextPacketParity(".openworkflow/audit/CONTEXT_PACKETS.yaml", contextPackets.packets, commands, errors);
|
|
507
|
+
}
|
|
508
|
+
const codexManifest = await readYamlRecordForParity(root, ".agents/openworkflow-adapter.yaml", errors);
|
|
509
|
+
if (codexManifest) {
|
|
510
|
+
await validateCodexManifestParity(root, ".agents/openworkflow-adapter.yaml", codexManifest, commands, errors);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
async function readYamlRecordForParity(root, relativePath, errors) {
|
|
514
|
+
const path = join(root, relativePath);
|
|
515
|
+
if (!(await exists(path))) {
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
try {
|
|
519
|
+
const data = parseYaml(await readFile(path, "utf8"));
|
|
520
|
+
if (isRecord(data)) {
|
|
521
|
+
return data;
|
|
522
|
+
}
|
|
523
|
+
errors.push(`${relativePath} must be a mapping for generated-surface parity validation`);
|
|
524
|
+
}
|
|
525
|
+
catch (error) {
|
|
526
|
+
errors.push(`${relativePath} is not valid YAML for generated-surface parity validation: ${messageFor(error)}`);
|
|
527
|
+
}
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
function validateCommandAuditParity(label, value, commands, errors) {
|
|
531
|
+
const records = recordsById(label, "commands", value, "id", errors);
|
|
532
|
+
if (!records) {
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
assertCommandIds(label, records, commands, errors);
|
|
536
|
+
for (const command of commands) {
|
|
537
|
+
const actual = records.get(command.id);
|
|
538
|
+
if (!actual) {
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
assertField(label, command.id, actual, "trigger", command.trigger, errors);
|
|
542
|
+
assertField(label, command.id, actual, "stage", command.stage, errors);
|
|
543
|
+
assertField(label, command.id, actual, "visibility", command.visibility, errors);
|
|
544
|
+
assertField(label, command.id, actual, "depth", command.protocol?.depth ?? "shallow", errors);
|
|
545
|
+
assertField(label, command.id, actual, "context_packet", `context:${command.id}`, errors);
|
|
546
|
+
assertStringArray(label, command.id, actual, "allowed_outputs", command.protocol?.allowedOutputs ?? command.targetArtifacts, errors);
|
|
547
|
+
assertStringArray(label, command.id, actual, "conditional_outputs", command.protocol?.conditionalOutputs ?? [], errors);
|
|
548
|
+
assertStringArray(label, command.id, actual, "forbidden_outputs", command.protocol?.forbiddenOutputs ?? [], errors);
|
|
549
|
+
assertStringArray(label, command.id, actual, "handoff_commands", command.protocol?.handoffCommands ?? [], errors);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
function validateContextPacketParity(label, value, commands, errors) {
|
|
553
|
+
const records = recordsById(label, "packets", value, "packet_id", errors);
|
|
554
|
+
if (!records) {
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
const expectedIds = new Set(commands.map((command) => `context:${command.id}`));
|
|
558
|
+
assertIdSet(label, "packets", records, expectedIds, errors);
|
|
559
|
+
for (const command of commands) {
|
|
560
|
+
const packetId = `context:${command.id}`;
|
|
561
|
+
const actual = records.get(packetId);
|
|
562
|
+
if (!actual) {
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
assertField(label, packetId, actual, "command", command.trigger, errors);
|
|
566
|
+
assertField(label, packetId, actual, "visibility", command.visibility, errors);
|
|
567
|
+
assertStringArray(label, packetId, actual, "required", command.protocol?.requiredContext ?? [".openworkflow/workflow/WORKFLOW_INDEX.yaml"], errors);
|
|
568
|
+
assertStringArray(label, packetId, actual, "optional", command.protocol?.optionalContext ?? [], errors);
|
|
569
|
+
assertStringArray(label, packetId, actual, "forbidden", command.protocol?.forbiddenContext ?? [], errors);
|
|
570
|
+
assertStringArray(label, packetId, actual, "conditional_outputs", command.protocol?.conditionalOutputs ?? [], errors);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
async function validateCodexManifestParity(root, label, manifest, commands, errors) {
|
|
574
|
+
const records = recordsById(label, "commands", manifest.commands, "id", errors);
|
|
575
|
+
if (!records) {
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
assertCommandIds(label, records, commands, errors);
|
|
579
|
+
const expectedGeneratedFiles = new Set();
|
|
580
|
+
for (const command of commands) {
|
|
581
|
+
const actual = records.get(command.id);
|
|
582
|
+
if (!actual) {
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
const skillName = `ow-${command.id}`;
|
|
586
|
+
const skillPath = `.agents/skills/${skillName}/SKILL.md`;
|
|
587
|
+
const interfacePath = `.agents/skills/${skillName}/agents/openai.yaml`;
|
|
588
|
+
expectedGeneratedFiles.add(skillPath);
|
|
589
|
+
expectedGeneratedFiles.add(interfacePath);
|
|
590
|
+
assertField(label, command.id, actual, "trigger", command.trigger, errors);
|
|
591
|
+
assertField(label, command.id, actual, "visibility", command.visibility, errors);
|
|
592
|
+
assertField(label, command.id, actual, "skill_name", skillName, errors);
|
|
593
|
+
assertField(label, command.id, actual, "explicit_invocation", `$${skillName}`, errors);
|
|
594
|
+
assertField(label, command.id, actual, "skill_path", skillPath, errors);
|
|
595
|
+
assertField(label, command.id, actual, "interface_path", interfacePath, errors);
|
|
596
|
+
assertStringArray(label, command.id, actual, "legacy_triggers", command.legacyTriggers, errors);
|
|
597
|
+
}
|
|
598
|
+
await validateManifestGeneratedFiles(root, label, manifest.generated_files, expectedGeneratedFiles, errors);
|
|
599
|
+
}
|
|
600
|
+
async function validateManifestGeneratedFiles(root, label, value, expectedGeneratedFiles, errors) {
|
|
601
|
+
if (!Array.isArray(value)) {
|
|
602
|
+
errors.push(`${label} generated_files must be a list; update adapter source and run openworkflow sync --tools codex`);
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
const actual = new Set();
|
|
606
|
+
for (const item of value) {
|
|
607
|
+
if (typeof item !== "string") {
|
|
608
|
+
errors.push(`${label} generated_files contains a non-string path`);
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
actual.add(item);
|
|
612
|
+
const path = join(root, item);
|
|
613
|
+
if (!existsSyncSafe(path)) {
|
|
614
|
+
errors.push(`${label} generated_files references missing ${item}; update adapter source and run openworkflow sync --tools codex`);
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
const content = await readFile(path, "utf8");
|
|
618
|
+
if (!content.includes("generated-by: openworkflow")) {
|
|
619
|
+
errors.push(`${label} generated_files ${item} is missing generated marker; update adapter source and run openworkflow sync --tools codex`);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
for (const expected of expectedGeneratedFiles) {
|
|
623
|
+
if (!actual.has(expected)) {
|
|
624
|
+
errors.push(`${label} generated_files missing ${expected}; update adapter source and run openworkflow sync --tools codex`);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
function recordsById(label, collectionName, value, idKey, errors) {
|
|
629
|
+
if (!Array.isArray(value)) {
|
|
630
|
+
errors.push(`${label} ${collectionName} must be a list for generated-surface parity validation`);
|
|
631
|
+
return null;
|
|
632
|
+
}
|
|
633
|
+
const records = new Map();
|
|
634
|
+
value.forEach((item, index) => {
|
|
237
635
|
if (!isRecord(item)) {
|
|
238
|
-
errors.push(`${label}
|
|
636
|
+
errors.push(`${label} ${collectionName}[${index}] must be a mapping`);
|
|
239
637
|
return;
|
|
240
638
|
}
|
|
241
|
-
const
|
|
242
|
-
if (typeof
|
|
243
|
-
errors.push(`${label}
|
|
639
|
+
const id = item[idKey];
|
|
640
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
641
|
+
errors.push(`${label} ${collectionName}[${index}] missing ${idKey}`);
|
|
244
642
|
return;
|
|
245
643
|
}
|
|
246
|
-
if (
|
|
247
|
-
errors.push(`${label} duplicate
|
|
248
|
-
|
|
249
|
-
seen.add(taskId);
|
|
250
|
-
for (const key of ["title", "status", "owned_paths", "acceptance"]) {
|
|
251
|
-
if (!(key in item)) {
|
|
252
|
-
errors.push(`${label} ${taskId} missing ${key}`);
|
|
253
|
-
}
|
|
644
|
+
if (records.has(id)) {
|
|
645
|
+
errors.push(`${label} ${collectionName} duplicate ${idKey} ${id}`);
|
|
646
|
+
return;
|
|
254
647
|
}
|
|
648
|
+
records.set(id, item);
|
|
255
649
|
});
|
|
650
|
+
return records;
|
|
256
651
|
}
|
|
257
|
-
function
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
652
|
+
function assertCommandIds(label, records, commands, errors) {
|
|
653
|
+
assertIdSet(label, "commands", records, new Set(commands.map((command) => command.id)), errors);
|
|
654
|
+
}
|
|
655
|
+
function assertIdSet(label, collectionName, records, expectedIds, errors) {
|
|
656
|
+
for (const expected of expectedIds) {
|
|
657
|
+
if (!records.has(expected)) {
|
|
658
|
+
errors.push(`${label} ${collectionName} missing ${expected}; update source registry and run openworkflow sync --tools codex`);
|
|
659
|
+
}
|
|
264
660
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
"feature_classification",
|
|
269
|
-
"critical_assumptions",
|
|
270
|
-
"prototype_scope",
|
|
271
|
-
"acceptance",
|
|
272
|
-
"decision_options",
|
|
273
|
-
]) {
|
|
274
|
-
if (!(key in data)) {
|
|
275
|
-
errors.push(`${label} missing validation key ${key}`);
|
|
661
|
+
for (const actual of records.keys()) {
|
|
662
|
+
if (!expectedIds.has(actual)) {
|
|
663
|
+
errors.push(`${label} ${collectionName} has unexpected ${actual}; update source registry and run openworkflow sync --tools codex`);
|
|
276
664
|
}
|
|
277
665
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
666
|
+
}
|
|
667
|
+
function assertField(label, id, record, field, expected, errors) {
|
|
668
|
+
if (record[field] !== expected) {
|
|
669
|
+
errors.push(`${label} ${id} ${field} must be ${expected}; update source registry and run openworkflow sync --tools codex`);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
function assertStringArray(label, id, record, field, expected, errors) {
|
|
673
|
+
const actual = record[field];
|
|
674
|
+
if (!Array.isArray(actual) || !actual.every((item) => typeof item === "string")) {
|
|
675
|
+
errors.push(`${label} ${id} ${field} must be a string list; update source registry and run openworkflow sync --tools codex`);
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
if (!stringArraysEqual(actual, expected)) {
|
|
679
|
+
errors.push(`${label} ${id} ${field} drifted from source registry; update source registry and run openworkflow sync --tools codex`);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
function stringArraysEqual(actual, expected) {
|
|
683
|
+
return actual.length === expected.length && actual.every((item, index) => item === expected[index]);
|
|
684
|
+
}
|
|
685
|
+
async function validateHighRiskDecisionReports(root, errors) {
|
|
686
|
+
for (const path of await findFiles(root, (entry) => entry === "HIGH_RISK_DECISION_REPORT.md")) {
|
|
687
|
+
const label = relative(root, path);
|
|
688
|
+
const content = await readFile(path, "utf8");
|
|
689
|
+
for (const section of HIGH_RISK_REPORT_SECTIONS) {
|
|
690
|
+
if (!hasMarkdownHeading(content, section)) {
|
|
691
|
+
errors.push(`${label} missing high-risk report section: ${section}`);
|
|
284
692
|
}
|
|
285
693
|
}
|
|
694
|
+
if (!content.includes("explicit") || !content.includes("approval")) {
|
|
695
|
+
errors.push(`${label} must state that implementation resumes only after explicit approval`);
|
|
696
|
+
}
|
|
286
697
|
}
|
|
287
698
|
}
|
|
288
|
-
function
|
|
289
|
-
|
|
699
|
+
function hasMarkdownHeading(content, heading) {
|
|
700
|
+
const escaped = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
701
|
+
return new RegExp(`^#{2,4}\\s+.*${escaped}.*$`, "m").test(content);
|
|
702
|
+
}
|
|
703
|
+
function validateConfig(root, path, data, errors) {
|
|
704
|
+
if (basename(path) !== "config.yaml") {
|
|
290
705
|
return;
|
|
291
706
|
}
|
|
292
707
|
const label = relative(root, path);
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
708
|
+
if (!nonEmptyString(data.project_slug) || data.project_slug === "project") {
|
|
709
|
+
errors.push(`${label} project_slug must be a useful non-empty slug`);
|
|
710
|
+
}
|
|
711
|
+
if (!nonEmptyString(data.project_title) || data.project_title === ".") {
|
|
712
|
+
errors.push(`${label} project_title must be a useful non-empty title`);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
function validateCurrentState(root, path, data, errors) {
|
|
716
|
+
if (basename(path) !== "CURRENT_STATE.yaml") {
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
const label = relative(root, path);
|
|
720
|
+
for (const key of ["active_stage", "next_command", "blocked_by", "read_this_first", "last_decision"]) {
|
|
302
721
|
if (!(key in data)) {
|
|
303
|
-
errors.push(`${label} missing
|
|
722
|
+
errors.push(`${label} missing current state key ${key}`);
|
|
304
723
|
}
|
|
305
724
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
const artifact = data.artifact;
|
|
309
|
-
if (isRecord(artifact) && typeof artifact.path === "string" && !existsSyncSafe(join(contractRootFor(root, path), artifact.path))) {
|
|
310
|
-
errors.push(`${label} references missing artifact path ${artifact.path}`);
|
|
725
|
+
if (!nonEmptyString(data.active_stage)) {
|
|
726
|
+
errors.push(`${label} active_stage must be a non-empty string`);
|
|
311
727
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
728
|
+
if (typeof data.next_command !== "string" && data.next_command !== null) {
|
|
729
|
+
errors.push(`${label} next_command must be a string or null`);
|
|
730
|
+
}
|
|
731
|
+
for (const key of ["blocked_by", "read_this_first"]) {
|
|
732
|
+
if (!Array.isArray(data[key])) {
|
|
733
|
+
errors.push(`${label} ${key} must be an array`);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
if (!isRecord(data.last_decision)) {
|
|
737
|
+
errors.push(`${label} last_decision must be a mapping`);
|
|
315
738
|
}
|
|
316
739
|
}
|
|
317
|
-
function
|
|
318
|
-
if (basename(path) !== "
|
|
740
|
+
async function validateCandidateChanges(root, path, data, errors) {
|
|
741
|
+
if (basename(path) !== "CANDIDATE_CHANGES.yaml") {
|
|
319
742
|
return;
|
|
320
743
|
}
|
|
321
744
|
const label = relative(root, path);
|
|
322
|
-
|
|
323
|
-
if (!Array.isArray(artifacts) || artifacts.length === 0) {
|
|
324
|
-
errors.push(`${label} must contain artifacts`);
|
|
745
|
+
if (data.planning_artifact_type !== "candidate_changes") {
|
|
325
746
|
return;
|
|
326
747
|
}
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
errors.push(`${label} artifact ${index} missing ${key}`);
|
|
348
|
-
}
|
|
748
|
+
const queuePolicy = data.queue_policy;
|
|
749
|
+
if (isRecord(queuePolicy) && "branch_boundary" in queuePolicy) {
|
|
750
|
+
validateBranchBoundary(label, queuePolicy.branch_boundary, errors);
|
|
751
|
+
}
|
|
752
|
+
if (isRecord(queuePolicy) && "branch_identity_exception" in queuePolicy) {
|
|
753
|
+
validateBranchIdentityException(label, queuePolicy.branch_identity_exception, errors);
|
|
754
|
+
}
|
|
755
|
+
const strictCommitGate = isRecord(queuePolicy) && queuePolicy.selected_change_commit_gate === "strict";
|
|
756
|
+
const strictLifecycleGate = isRecord(queuePolicy) && queuePolicy.git_lifecycle_gate === "strict";
|
|
757
|
+
if (strictLifecycleGate) {
|
|
758
|
+
validateStrictGitLifecycle(root, label, data, queuePolicy, errors);
|
|
759
|
+
}
|
|
760
|
+
if (!Array.isArray(data.changes)) {
|
|
761
|
+
errors.push(`${label} changes must be a list`);
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
for (const candidate of data.changes) {
|
|
765
|
+
if (!isRecord(candidate)) {
|
|
766
|
+
errors.push(`${label} changes entries must be mappings`);
|
|
767
|
+
continue;
|
|
349
768
|
}
|
|
350
|
-
|
|
351
|
-
});
|
|
352
|
-
for (const artifactType of [...missing].sort()) {
|
|
353
|
-
errors.push(`${label} missing artifact_type ${artifactType}`);
|
|
769
|
+
validateCandidateCompletionEvidence(root, label, candidate, errors, { strictCommitGate });
|
|
354
770
|
}
|
|
355
771
|
}
|
|
356
|
-
function
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
772
|
+
function validateStrictGitLifecycle(root, label, data, queuePolicy, errors) {
|
|
773
|
+
const planId = stringField(data, "plan_id");
|
|
774
|
+
const branchBoundary = stringField(queuePolicy, "branch_boundary");
|
|
775
|
+
if (!branchBoundary) {
|
|
776
|
+
errors.push(`${label} strict git lifecycle requires queue_policy.branch_boundary`);
|
|
777
|
+
}
|
|
778
|
+
else if (planId) {
|
|
779
|
+
const branchIdentity = assessBranchIdentity(planId, branchBoundary, branchIdentityExceptionFrom(queuePolicy.branch_identity_exception), "validate");
|
|
780
|
+
errors.push(...branchIdentity.errors.map((error) => `${label} ${error}`));
|
|
781
|
+
}
|
|
782
|
+
const status = stringField(data, "status");
|
|
783
|
+
if (status === "completed" || status === "done") {
|
|
784
|
+
validateCompletedQueuePrEvidence(root, label, data, errors);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
function validateCompletedQueuePrEvidence(root, label, data, errors) {
|
|
788
|
+
const evidencePath = queuePrEvidencePath(data);
|
|
789
|
+
if (!evidencePath) {
|
|
790
|
+
errors.push(`${label} strict git lifecycle completed queue must include DRAFT_PR_OPERATION_EVIDENCE.yaml`);
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
if (isExternalRef(evidencePath) || evidencePath.startsWith("commit:")) {
|
|
794
|
+
errors.push(`${label} DRAFT_PR_OPERATION_EVIDENCE.yaml path must be repo-relative`);
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
const resolved = resolve(root, evidencePath);
|
|
798
|
+
if (resolved !== root && !resolved.startsWith(`${root}/`)) {
|
|
799
|
+
errors.push(`${label} DRAFT_PR_OPERATION_EVIDENCE.yaml path escapes repository root`);
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
if (!existsSyncSafe(resolved)) {
|
|
803
|
+
errors.push(`${label} references missing DRAFT_PR_OPERATION_EVIDENCE.yaml ${evidencePath}`);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
function queuePrEvidencePath(data) {
|
|
807
|
+
const completion = recordField(data, "completion");
|
|
808
|
+
const explicit = stringField(completion, "draft_pr_evidence_path")
|
|
809
|
+
?? stringField(completion, "draft_pr_evidence")
|
|
810
|
+
?? stringField(completion, "pr_evidence_path")
|
|
811
|
+
?? stringField(completion, "pr_evidence");
|
|
812
|
+
if (explicit && explicit.endsWith("DRAFT_PR_OPERATION_EVIDENCE.yaml")) {
|
|
813
|
+
return explicit;
|
|
814
|
+
}
|
|
815
|
+
const evidence = Array.isArray(completion.evidence) ? completion.evidence : [];
|
|
816
|
+
for (const item of evidence) {
|
|
817
|
+
if (typeof item === "string" && item.endsWith("DRAFT_PR_OPERATION_EVIDENCE.yaml")) {
|
|
818
|
+
return item;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
return null;
|
|
822
|
+
}
|
|
823
|
+
function validateBranchIdentityException(label, value, errors) {
|
|
824
|
+
if (!isRecord(value)) {
|
|
825
|
+
errors.push(`${label} queue_policy.branch_identity_exception must be a mapping when present`);
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
if (value.mode !== "temporary_continuation_branch") {
|
|
829
|
+
errors.push(`${label} queue_policy.branch_identity_exception.mode must be temporary_continuation_branch`);
|
|
830
|
+
}
|
|
831
|
+
if (value.approved !== true) {
|
|
832
|
+
errors.push(`${label} queue_policy.branch_identity_exception.approved must be true`);
|
|
833
|
+
}
|
|
834
|
+
if (!Array.isArray(value.allowed_operations) || !value.allowed_operations.every((item) => typeof item === "string")) {
|
|
835
|
+
errors.push(`${label} queue_policy.branch_identity_exception.allowed_operations must be a string list`);
|
|
836
|
+
}
|
|
837
|
+
if (!nonEmptyString(value.reason)) {
|
|
838
|
+
errors.push(`${label} queue_policy.branch_identity_exception.reason must explain the temporary continuation branch`);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
function validateBranchBoundary(label, value, errors) {
|
|
842
|
+
if (!nonEmptyString(value)) {
|
|
843
|
+
errors.push(`${label} queue_policy.branch_boundary must be a non-empty string when present`);
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
const branch = String(value).trim();
|
|
847
|
+
if (branch !== value || branch.includes(" ") || branch.startsWith("/") || branch.endsWith("/")) {
|
|
848
|
+
errors.push(`${label} queue_policy.branch_boundary must be a branch-like string without spaces or leading/trailing slashes`);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
function validateCandidateCompletionEvidence(root, label, candidate, errors, options) {
|
|
852
|
+
if (candidate.status !== "done") {
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
const id = typeof candidate.id === "string" ? candidate.id : "<unknown>";
|
|
856
|
+
const completion = candidate.completion;
|
|
857
|
+
if (!isRecord(completion)) {
|
|
858
|
+
errors.push(`${label} ${id} done candidate must include completion evidence`);
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
const evidence = completion.evidence;
|
|
862
|
+
if (!Array.isArray(evidence) || evidence.length === 0) {
|
|
863
|
+
errors.push(`${label} ${id} completion.evidence must be a non-empty list`);
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
for (const item of evidence) {
|
|
867
|
+
if (typeof item !== "string") {
|
|
868
|
+
errors.push(`${label} ${id} completion.evidence values must be strings`);
|
|
869
|
+
continue;
|
|
870
|
+
}
|
|
871
|
+
if (item.startsWith("commit:") && !/^commit:\s+[0-9a-f]{7,40}$/i.test(item)) {
|
|
872
|
+
errors.push(`${label} ${id} completion commit evidence must use 'commit: <7-40 hex chars>'`);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
const selectedChangeId = stringField(recordField(candidate, "selection"), "selected_change_id");
|
|
876
|
+
const implementationChangedFiles = completion.implementation_changed_files;
|
|
877
|
+
if (implementationChangedFiles === false) {
|
|
878
|
+
if (!nonEmptyString(completion.commit_not_required_reason)) {
|
|
879
|
+
errors.push(`${label} ${id} planning-only completion must include commit_not_required_reason`);
|
|
880
|
+
}
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
if (implementationChangedFiles === true) {
|
|
884
|
+
validateRequiredLocalCommitEvidence(root, label, id, completion, evidence, errors);
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
if (options.strictCommitGate && selectedChangeId) {
|
|
888
|
+
errors.push(`${label} ${id} strict selected-change completion must set implementation_changed_files true or false`);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
function validateRequiredLocalCommitEvidence(root, label, candidateId, completion, evidence, errors) {
|
|
892
|
+
const evidencePath = localCommitEvidencePath(completion, evidence);
|
|
893
|
+
if (!evidencePath) {
|
|
894
|
+
errors.push(`${label} ${candidateId} implementation completion must include LOCAL_COMMIT_EVIDENCE.yaml`);
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
if (isExternalRef(evidencePath) || evidencePath.startsWith("commit:")) {
|
|
898
|
+
errors.push(`${label} ${candidateId} LOCAL_COMMIT_EVIDENCE.yaml path must be repo-relative`);
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
const resolved = resolve(root, evidencePath);
|
|
902
|
+
if (resolved !== root && !resolved.startsWith(`${root}/`)) {
|
|
903
|
+
errors.push(`${label} ${candidateId} LOCAL_COMMIT_EVIDENCE.yaml path escapes repository root`);
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
if (!existsSyncSafe(resolved)) {
|
|
907
|
+
errors.push(`${label} ${candidateId} references missing LOCAL_COMMIT_EVIDENCE.yaml ${evidencePath}`);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
function localCommitEvidencePath(completion, evidence) {
|
|
911
|
+
const explicit = stringField(completion, "local_commit_evidence_path") ?? stringField(completion, "local_commit_evidence");
|
|
912
|
+
if (explicit && explicit.endsWith("LOCAL_COMMIT_EVIDENCE.yaml")) {
|
|
913
|
+
return explicit;
|
|
914
|
+
}
|
|
915
|
+
for (const item of evidence) {
|
|
916
|
+
if (typeof item === "string" && item.endsWith("LOCAL_COMMIT_EVIDENCE.yaml")) {
|
|
917
|
+
return item;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
return null;
|
|
921
|
+
}
|
|
922
|
+
function validateLocalCommitEvidence(root, path, data, errors) {
|
|
923
|
+
if (basename(path) !== "LOCAL_COMMIT_EVIDENCE.yaml") {
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
const label = relative(root, path);
|
|
927
|
+
const planId = stringField(data, "source_plan_id") ?? stringField(data, "plan_id");
|
|
928
|
+
const candidateId = stringField(data, "source_candidate_id") ?? stringField(data, "candidate_id");
|
|
929
|
+
const selectedChangeId = stringField(data, "selected_change_id") ?? stringField(data, "change_id");
|
|
930
|
+
const commit = stringField(data, "primary_commit") ?? stringField(data, "implementation_commit") ?? stringField(data, "commit");
|
|
931
|
+
if (!planId) {
|
|
932
|
+
errors.push(`${label} missing source_plan_id or plan_id`);
|
|
933
|
+
}
|
|
934
|
+
if (!candidateId) {
|
|
935
|
+
errors.push(`${label} missing source_candidate_id or candidate_id`);
|
|
936
|
+
}
|
|
937
|
+
if (!selectedChangeId) {
|
|
938
|
+
errors.push(`${label} missing selected_change_id or change_id`);
|
|
939
|
+
}
|
|
940
|
+
if (!commit || !/^[0-9a-f]{7,40}$/i.test(commit)) {
|
|
941
|
+
errors.push(`${label} must include primary_commit or implementation_commit with a 7-40 character git hash`);
|
|
942
|
+
}
|
|
943
|
+
if (!hasValidationEvidence(data)) {
|
|
944
|
+
errors.push(`${label} must include validation_evidence, validations, or validation.commands_run`);
|
|
945
|
+
}
|
|
946
|
+
validateOptionalCoderEvidence(label, data.coder_evidence, errors);
|
|
947
|
+
}
|
|
948
|
+
function hasValidationEvidence(data) {
|
|
949
|
+
if (Array.isArray(data.validation_evidence) && data.validation_evidence.some((item) => typeof item === "string" && item.trim().length > 0)) {
|
|
950
|
+
return true;
|
|
951
|
+
}
|
|
952
|
+
if (Array.isArray(data.validations) && data.validations.some((item) => typeof item === "string" && item.trim().length > 0)) {
|
|
953
|
+
return true;
|
|
954
|
+
}
|
|
955
|
+
const validation = recordField(data, "validation");
|
|
956
|
+
return Array.isArray(validation.commands_run) && validation.commands_run.some((item) => typeof item === "string" && item.trim().length > 0);
|
|
957
|
+
}
|
|
958
|
+
function validateOptionalCoderEvidence(label, value, errors) {
|
|
959
|
+
if (value === undefined || value === null) {
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
if (!isRecord(value)) {
|
|
963
|
+
errors.push(`${label} coder_evidence must be a mapping when present`);
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
const status = stringField(value, "status");
|
|
967
|
+
if (!status) {
|
|
968
|
+
errors.push(`${label} coder_evidence.status must be recorded, skipped, or not_applicable`);
|
|
969
|
+
}
|
|
970
|
+
else if (!["recorded", "skipped", "not_applicable"].includes(status)) {
|
|
971
|
+
errors.push(`${label} coder_evidence.status has invalid value ${status}`);
|
|
972
|
+
}
|
|
973
|
+
const enforcement = stringField(value, "enforcement");
|
|
974
|
+
if (enforcement && enforcement !== "guidance_only") {
|
|
975
|
+
errors.push(`${label} coder_evidence.enforcement must be guidance_only when present`);
|
|
976
|
+
}
|
|
977
|
+
const evidenceKeys = ["preflight", "red_evidence", "green_evidence", "self_check", "validation_ladder", "lessons"];
|
|
978
|
+
let hasEvidence = false;
|
|
979
|
+
for (const key of evidenceKeys) {
|
|
980
|
+
const field = value[key];
|
|
981
|
+
if (field === undefined || field === null) {
|
|
982
|
+
continue;
|
|
983
|
+
}
|
|
984
|
+
if (!Array.isArray(field)) {
|
|
985
|
+
errors.push(`${label} coder_evidence.${key} must be a list of non-empty strings when present`);
|
|
986
|
+
continue;
|
|
987
|
+
}
|
|
988
|
+
for (const item of field) {
|
|
989
|
+
if (!nonEmptyString(item)) {
|
|
990
|
+
errors.push(`${label} coder_evidence.${key} values must be non-empty strings`);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
if (field.some((item) => nonEmptyString(item))) {
|
|
994
|
+
hasEvidence = true;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
if (status === "recorded" && !hasEvidence) {
|
|
998
|
+
errors.push(`${label} coder_evidence.status recorded requires at least one evidence list entry`);
|
|
999
|
+
}
|
|
1000
|
+
const notes = value.notes;
|
|
1001
|
+
if (notes !== undefined && notes !== null && !nonEmptyString(notes)) {
|
|
1002
|
+
errors.push(`${label} coder_evidence.notes must be a non-empty string when present`);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
function validateCommonContract(root, path, data, errors) {
|
|
1006
|
+
if (!isRecord(data)) {
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
if (!("contract_type" in data) && !("schema_version" in data)) {
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
const label = relative(root, path);
|
|
1013
|
+
for (const key of COMMON_REQUIRED) {
|
|
1014
|
+
if (!(key in data)) {
|
|
1015
|
+
errors.push(`${label} missing contract key ${key}`);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
if (data.schema_version !== SCHEMA_VERSION) {
|
|
1019
|
+
errors.push(`${label} must use schema_version ${SCHEMA_VERSION}`);
|
|
1020
|
+
}
|
|
1021
|
+
for (const listKey of ["depends_on", "produces"]) {
|
|
1022
|
+
const value = data[listKey];
|
|
1023
|
+
if (Array.isArray(value)) {
|
|
1024
|
+
for (const item of value) {
|
|
1025
|
+
if (typeof item !== "string") {
|
|
1026
|
+
errors.push(`${label} has non-string ${listKey} value`);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
function validateChange(root, path, data, errors) {
|
|
1033
|
+
if (basename(path) !== "CHANGE.yaml") {
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
for (const key of ["problem", "goals", "non_goals", "affected_paths", "acceptance", "validation"]) {
|
|
1037
|
+
if (!(key in data)) {
|
|
1038
|
+
errors.push(`${relative(root, path)} missing change key ${key}`);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
if (data.contract_type !== "change") {
|
|
1042
|
+
errors.push(`${relative(root, path)} contract_type must be change`);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
function validateWorkItems(root, path, data, errors) {
|
|
1046
|
+
if (basename(path) !== "WORK_ITEMS.yaml") {
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
const label = relative(root, path);
|
|
1050
|
+
if (data.contract_type !== "work_items") {
|
|
1051
|
+
errors.push(`${label} contract_type must be work_items`);
|
|
1052
|
+
}
|
|
1053
|
+
const changeContract = data.change_contract;
|
|
1054
|
+
if (typeof changeContract !== "string") {
|
|
1055
|
+
errors.push(`${label} missing change_contract`);
|
|
1056
|
+
}
|
|
1057
|
+
else if (!(existsSyncSafe(join(contractRootFor(root, path), changeContract)) || existsSyncSafe(join(root, changeContract)))) {
|
|
1058
|
+
errors.push(`${label} references missing change_contract ${changeContract}`);
|
|
1059
|
+
}
|
|
1060
|
+
const items = data.items;
|
|
1061
|
+
if (!Array.isArray(items) || items.length === 0) {
|
|
1062
|
+
errors.push(`${label} must contain non-empty items`);
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
const seen = new Set();
|
|
1066
|
+
items.forEach((item, index) => {
|
|
1067
|
+
if (!isRecord(item)) {
|
|
1068
|
+
errors.push(`${label} item ${index} is not a mapping`);
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
const taskId = item.task_id;
|
|
1072
|
+
if (typeof taskId !== "string" || taskId.length === 0) {
|
|
1073
|
+
errors.push(`${label} item ${index} missing task_id`);
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
if (seen.has(taskId)) {
|
|
1077
|
+
errors.push(`${label} duplicate task_id ${taskId}`);
|
|
1078
|
+
}
|
|
1079
|
+
seen.add(taskId);
|
|
1080
|
+
for (const key of ["title", "status", "owned_paths", "acceptance"]) {
|
|
1081
|
+
if (!(key in item)) {
|
|
1082
|
+
errors.push(`${label} ${taskId} missing ${key}`);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
function validateValidation(root, path, data, errors) {
|
|
1088
|
+
if (basename(path) !== "VALIDATION.yaml") {
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
const label = relative(root, path);
|
|
1092
|
+
if (data.contract_type !== "validation") {
|
|
1093
|
+
errors.push(`${label} contract_type must be validation`);
|
|
1094
|
+
}
|
|
1095
|
+
validateValidationTarget(label, data, errors);
|
|
1096
|
+
for (const key of [
|
|
1097
|
+
"core_question",
|
|
1098
|
+
"feature_classification",
|
|
1099
|
+
"critical_assumptions",
|
|
1100
|
+
"prototype_scope",
|
|
1101
|
+
"acceptance",
|
|
1102
|
+
"decision_options",
|
|
1103
|
+
]) {
|
|
1104
|
+
if (!(key in data)) {
|
|
1105
|
+
errors.push(`${label} missing validation key ${key}`);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
const decisionOptions = data.decision_options;
|
|
1109
|
+
if (Array.isArray(decisionOptions)) {
|
|
1110
|
+
const allowed = new Set(["continue", "revise", "pivot", "stop", "needs_more_evidence"]);
|
|
1111
|
+
for (const option of decisionOptions) {
|
|
1112
|
+
if (typeof option !== "string" || !allowed.has(option)) {
|
|
1113
|
+
errors.push(`${label} has invalid decision option ${String(option)}`);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
function validatePrototype(root, path, data, errors) {
|
|
1119
|
+
if (basename(path) !== "TODO.yaml" || data.contract_type !== "prototype") {
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
const label = relative(root, path);
|
|
1123
|
+
for (const key of [
|
|
1124
|
+
"validation_contract",
|
|
1125
|
+
"core_question",
|
|
1126
|
+
"prototype_scope",
|
|
1127
|
+
"todo",
|
|
1128
|
+
"acceptance",
|
|
1129
|
+
"artifact",
|
|
1130
|
+
"decision_handoff",
|
|
1131
|
+
]) {
|
|
1132
|
+
if (!(key in data)) {
|
|
1133
|
+
errors.push(`${label} missing prototype key ${key}`);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
validatePrototypeScope(label, data.prototype_scope, errors);
|
|
1137
|
+
validatePrototypeTodo(label, data.todo, errors);
|
|
1138
|
+
const artifact = data.artifact;
|
|
1139
|
+
if (isRecord(artifact) && typeof artifact.path === "string" && !existsSyncSafe(join(contractRootFor(root, path), artifact.path))) {
|
|
1140
|
+
errors.push(`${label} references missing artifact path ${artifact.path}`);
|
|
1141
|
+
}
|
|
1142
|
+
const decisionHandoff = data.decision_handoff;
|
|
1143
|
+
if (isRecord(decisionHandoff) && decisionHandoff.requires_user_review !== true) {
|
|
1144
|
+
errors.push(`${label} decision_handoff.requires_user_review must be true`);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
function validateArtifactContracts(root, path, data, errors) {
|
|
1148
|
+
if (basename(path) !== "ARTIFACT_CONTRACTS.yaml") {
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
const label = relative(root, path);
|
|
1152
|
+
const artifacts = data.artifacts;
|
|
1153
|
+
if (!Array.isArray(artifacts) || artifacts.length === 0) {
|
|
1154
|
+
errors.push(`${label} must contain artifacts`);
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
const missing = new Set([
|
|
1158
|
+
"vision_session",
|
|
1159
|
+
"validation_target",
|
|
1160
|
+
"prototype_evidence",
|
|
1161
|
+
"decision_record",
|
|
1162
|
+
"product_design",
|
|
1163
|
+
"production_spec",
|
|
1164
|
+
"production_change",
|
|
1165
|
+
"team_runtime",
|
|
1166
|
+
]);
|
|
1167
|
+
artifacts.forEach((artifact, index) => {
|
|
1168
|
+
if (!isRecord(artifact)) {
|
|
1169
|
+
errors.push(`${label} artifact ${index} is not a mapping`);
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
if (typeof artifact.artifact_type === "string") {
|
|
1173
|
+
missing.delete(artifact.artifact_type);
|
|
1174
|
+
}
|
|
1175
|
+
for (const key of [
|
|
1176
|
+
"artifact_type",
|
|
1177
|
+
"contract_type",
|
|
1178
|
+
"command",
|
|
1179
|
+
"source_of_truth_path",
|
|
1180
|
+
"template_path",
|
|
1181
|
+
"read_policy",
|
|
1182
|
+
"active_pointer",
|
|
1183
|
+
"required_keys",
|
|
1184
|
+
]) {
|
|
1185
|
+
if (!(key in artifact)) {
|
|
1186
|
+
errors.push(`${label} artifact ${index} missing ${key}`);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
validateArtifactContractMetadata(label, index, artifact, errors);
|
|
1190
|
+
});
|
|
1191
|
+
for (const artifactType of [...missing].sort()) {
|
|
1192
|
+
errors.push(`${label} missing artifact_type ${artifactType}`);
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
function validateArtifactContractMetadata(label, index, artifact, errors) {
|
|
1196
|
+
const readPolicy = artifact.read_policy;
|
|
1197
|
+
if (isRecord(readPolicy)) {
|
|
1198
|
+
for (const key of ["load_by_default", "agent_read_order", "max_yaml_lines", "max_note_lines", "raw_evidence"]) {
|
|
1199
|
+
if (!(key in readPolicy)) {
|
|
1200
|
+
errors.push(`${label} artifact ${index} read_policy missing ${key}`);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
else {
|
|
1205
|
+
errors.push(`${label} artifact ${index} read_policy must be a mapping`);
|
|
1206
|
+
}
|
|
1207
|
+
const activePointer = artifact.active_pointer;
|
|
1208
|
+
if (isRecord(activePointer)) {
|
|
1209
|
+
for (const key of ["index_path", "pointer_key", "collection_key", "id_key", "path_key"]) {
|
|
1210
|
+
if (!(key in activePointer)) {
|
|
1211
|
+
errors.push(`${label} artifact ${index} active_pointer missing ${key}`);
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
else {
|
|
1216
|
+
errors.push(`${label} artifact ${index} active_pointer must be a mapping`);
|
|
1217
|
+
}
|
|
1218
|
+
const summaryPolicy = artifact.summary_policy;
|
|
1219
|
+
if (summaryPolicy !== null && summaryPolicy !== undefined) {
|
|
1220
|
+
if (!isRecord(summaryPolicy)) {
|
|
1221
|
+
errors.push(`${label} artifact ${index} summary_policy must be null or a mapping`);
|
|
1222
|
+
}
|
|
1223
|
+
else {
|
|
1224
|
+
for (const key of ["strategy", "path", "load_before_full", "refresh_when"]) {
|
|
1225
|
+
if (!(key in summaryPolicy)) {
|
|
1226
|
+
errors.push(`${label} artifact ${index} summary_policy missing ${key}`);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
function validateDisclosureLevels(root, path, data, errors) {
|
|
1233
|
+
if (basename(path) !== "DISCLOSURE_LEVELS.yaml") {
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
const label = relative(root, path);
|
|
1237
|
+
const levels = data.levels;
|
|
1238
|
+
if (!Array.isArray(levels) || levels.length < 5) {
|
|
1239
|
+
errors.push(`${label} must contain disclosure levels 0 through 4`);
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
const seen = new Set();
|
|
1243
|
+
levels.forEach((level, index) => {
|
|
1244
|
+
if (!isRecord(level)) {
|
|
1245
|
+
errors.push(`${label} level ${index} is not a mapping`);
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
if (typeof level.level === "number") {
|
|
1249
|
+
seen.add(level.level);
|
|
1250
|
+
}
|
|
1251
|
+
for (const key of ["name", "default_for_agents", "purpose", "examples"]) {
|
|
1252
|
+
if (!(key in level)) {
|
|
1253
|
+
errors.push(`${label} level ${index} missing ${key}`);
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
});
|
|
1257
|
+
for (let level = 0; level <= 4; level += 1) {
|
|
1258
|
+
if (!seen.has(level)) {
|
|
1259
|
+
errors.push(`${label} missing disclosure level ${level}`);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
function validateDiscoveryArtifact(root, path, data, errors) {
|
|
1264
|
+
if (data.contract_type === "planning") {
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
if (typeof data.artifact_type !== "string") {
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
const label = relative(root, path);
|
|
1271
|
+
const required = artifactRequiredKeys(data.artifact_type);
|
|
1272
|
+
if (!required) {
|
|
1273
|
+
errors.push(`${label} has unknown artifact_type ${data.artifact_type}`);
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
for (const key of required) {
|
|
1277
|
+
if (!(key in data)) {
|
|
1278
|
+
errors.push(`${label} missing artifact key ${key}`);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
if (data.artifact_type === "validation_target") {
|
|
1282
|
+
validateValidationTarget(label, data, errors);
|
|
1283
|
+
}
|
|
1284
|
+
else if (data.artifact_type === "prototype_evidence") {
|
|
1285
|
+
validatePrototypeEvidence(root, label, data, errors);
|
|
1286
|
+
}
|
|
1287
|
+
else if (data.artifact_type === "decision_record") {
|
|
1288
|
+
validateDecisionRecord(label, data, errors);
|
|
1289
|
+
}
|
|
1290
|
+
else if (data.artifact_type === "product_design") {
|
|
1291
|
+
validateProductDesign(label, data, errors);
|
|
1292
|
+
}
|
|
1293
|
+
else if (data.artifact_type === "production_spec") {
|
|
1294
|
+
validateProductionSpec(label, data, errors);
|
|
1295
|
+
}
|
|
1296
|
+
else if (data.artifact_type === "production_change") {
|
|
1297
|
+
validateProductionChange(label, data, errors);
|
|
1298
|
+
}
|
|
1299
|
+
else if (data.artifact_type === "team_runtime") {
|
|
1300
|
+
validateTeamRuntime(label, data, errors);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
function artifactRequiredKeys(artifactType) {
|
|
1304
|
+
const requiredByType = {
|
|
1305
|
+
vision_session: ["current_question", "stable_answers", "unresolved_questions", "vision_delta", "handoff"],
|
|
1306
|
+
validation_target: [
|
|
1307
|
+
"trigger",
|
|
1308
|
+
"core_question",
|
|
1309
|
+
"central_uncertainty",
|
|
1310
|
+
"hypothesis",
|
|
1311
|
+
"target_behavior",
|
|
1312
|
+
"feature_classification",
|
|
1313
|
+
"critical_assumptions",
|
|
1314
|
+
"prototype_scope",
|
|
1315
|
+
"prototype_experiment",
|
|
1316
|
+
"observable_signals",
|
|
1317
|
+
"acceptance",
|
|
1318
|
+
"decision_rules",
|
|
1319
|
+
"decision_options",
|
|
1320
|
+
"vision_gaps",
|
|
1321
|
+
"agent_readiness_gate",
|
|
1322
|
+
],
|
|
1323
|
+
prototype_evidence: [
|
|
1324
|
+
"validation_target",
|
|
1325
|
+
"core_question",
|
|
1326
|
+
"prototype_mode",
|
|
1327
|
+
"prompt_pack_type",
|
|
1328
|
+
"validation_input",
|
|
1329
|
+
"source",
|
|
1330
|
+
"negative_constraints",
|
|
1331
|
+
"review_plan",
|
|
1332
|
+
"result",
|
|
1333
|
+
"handoff",
|
|
1334
|
+
],
|
|
1335
|
+
decision_record: [
|
|
1336
|
+
"reviewed_evidence",
|
|
1337
|
+
"outcome",
|
|
1338
|
+
"rationale",
|
|
1339
|
+
"accepted_scope",
|
|
1340
|
+
"rejected_scope",
|
|
1341
|
+
"revision_scope",
|
|
1342
|
+
"next_command",
|
|
1343
|
+
"follow_up_questions",
|
|
1344
|
+
],
|
|
1345
|
+
product_design: [
|
|
1346
|
+
"accepted_prototype_evidence",
|
|
1347
|
+
"personas",
|
|
1348
|
+
"journey_map",
|
|
1349
|
+
"user_stories",
|
|
1350
|
+
"feature_matrix",
|
|
1351
|
+
"kano_classification",
|
|
1352
|
+
"behavior_model",
|
|
1353
|
+
"ux_states",
|
|
1354
|
+
"scope",
|
|
1355
|
+
"open_questions",
|
|
1356
|
+
"conditional_packets",
|
|
1357
|
+
"spec_readiness",
|
|
1358
|
+
],
|
|
1359
|
+
production_spec: [
|
|
1360
|
+
"source_design",
|
|
1361
|
+
"goal",
|
|
1362
|
+
"scope",
|
|
1363
|
+
"requirements",
|
|
1364
|
+
"interfaces",
|
|
1365
|
+
"acceptance",
|
|
1366
|
+
"verification",
|
|
1367
|
+
"risks",
|
|
1368
|
+
"change_readiness",
|
|
1369
|
+
],
|
|
1370
|
+
production_change: [
|
|
1371
|
+
"source_spec",
|
|
1372
|
+
"problem",
|
|
1373
|
+
"goals",
|
|
1374
|
+
"non_goals",
|
|
1375
|
+
"affected_paths",
|
|
1376
|
+
"acceptance",
|
|
1377
|
+
"validation",
|
|
1378
|
+
"work_items",
|
|
1379
|
+
"risks",
|
|
1380
|
+
"runtime_readiness",
|
|
1381
|
+
],
|
|
1382
|
+
team_runtime: [
|
|
1383
|
+
"source_change",
|
|
1384
|
+
"active_work_item",
|
|
1385
|
+
"execution_mode",
|
|
1386
|
+
"work_queue",
|
|
1387
|
+
"agents",
|
|
1388
|
+
"verification",
|
|
1389
|
+
"issues",
|
|
1390
|
+
"checkpoints",
|
|
1391
|
+
"handoff",
|
|
1392
|
+
],
|
|
1393
|
+
};
|
|
1394
|
+
return requiredByType[artifactType] ?? null;
|
|
1395
|
+
}
|
|
1396
|
+
function validateValidationTarget(label, data, errors) {
|
|
1397
|
+
validateValidationTrigger(label, data.trigger, errors);
|
|
1398
|
+
const featureClassification = data.feature_classification;
|
|
1399
|
+
if (isRecord(featureClassification)) {
|
|
1400
|
+
for (const key of ["existential", "supporting", "later", "out_of_scope"]) {
|
|
1401
|
+
if (!(key in featureClassification)) {
|
|
1402
|
+
errors.push(`${label} feature_classification missing ${key}`);
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
validatePrototypeScope(label, data.prototype_scope, errors);
|
|
1407
|
+
validatePrototypeExperiment(label, data.prototype_experiment, errors);
|
|
1408
|
+
validateSignalSet(label, "observable_signals", data.observable_signals, ["pass", "fail", "ambiguous"], errors);
|
|
1409
|
+
validateSignalSet(label, "decision_rules", data.decision_rules, ["continue", "revise", "pivot", "stop", "needs_more_evidence"], errors);
|
|
1410
|
+
validateAgentReadinessGate(label, data.agent_readiness_gate, errors);
|
|
1411
|
+
}
|
|
1412
|
+
function validateValidationTrigger(label, value, errors) {
|
|
1413
|
+
if (!isRecord(value)) {
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
for (const key of ["mode", "requested_command", "reason"]) {
|
|
1417
|
+
if (!(key in value)) {
|
|
1418
|
+
errors.push(`${label} trigger missing ${key}`);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
const mode = String(value.mode ?? "");
|
|
1422
|
+
if (mode && !["user_explicit", "agent_auto"].includes(mode)) {
|
|
1423
|
+
errors.push(`${label} trigger.mode has invalid value ${mode}`);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
function validatePrototypeExperiment(label, value, errors) {
|
|
1427
|
+
if (!isRecord(value)) {
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
for (const key of ["scenario", "must_show", "must_not_show"]) {
|
|
1431
|
+
if (!(key in value)) {
|
|
1432
|
+
errors.push(`${label} prototype_experiment missing ${key}`);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
function validateSignalSet(label, field, value, keys, errors) {
|
|
1437
|
+
if (!isRecord(value)) {
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
for (const key of keys) {
|
|
1441
|
+
if (!(key in value)) {
|
|
1442
|
+
errors.push(`${label} ${field} missing ${key}`);
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
function validateAgentReadinessGate(label, value, errors) {
|
|
1447
|
+
if (!isRecord(value)) {
|
|
1448
|
+
return;
|
|
1449
|
+
}
|
|
1450
|
+
for (const key of ["status", "blockers", "warnings", "write_authority"]) {
|
|
1451
|
+
if (!(key in value)) {
|
|
1452
|
+
errors.push(`${label} agent_readiness_gate missing ${key}`);
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
const status = String(value.status ?? "");
|
|
1456
|
+
if (status && !["missing_validation", "thin_validation", "stale_validation", "ready_for_proto", "return_to_vision"].includes(status)) {
|
|
1457
|
+
errors.push(`${label} agent_readiness_gate.status has invalid value ${status}`);
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
function validatePrototypeScope(label, value, errors) {
|
|
1461
|
+
if (!isRecord(value)) {
|
|
1462
|
+
return;
|
|
1463
|
+
}
|
|
1464
|
+
for (const key of ["include", "exclude"]) {
|
|
1465
|
+
if (!(key in value)) {
|
|
1466
|
+
errors.push(`${label} prototype_scope missing ${key}`);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
function validatePrototypeTodo(label, value, errors) {
|
|
1471
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
1472
|
+
errors.push(`${label} must contain non-empty todo list`);
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
1475
|
+
const seen = new Set();
|
|
1476
|
+
value.forEach((item, index) => {
|
|
1477
|
+
if (!isRecord(item)) {
|
|
1478
|
+
errors.push(`${label} todo item ${index} is not a mapping`);
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
1481
|
+
const taskId = item.task_id;
|
|
1482
|
+
if (typeof taskId !== "string" || taskId.length === 0) {
|
|
1483
|
+
errors.push(`${label} todo item ${index} missing task_id`);
|
|
1484
|
+
return;
|
|
1485
|
+
}
|
|
1486
|
+
if (seen.has(taskId)) {
|
|
1487
|
+
errors.push(`${label} duplicate prototype task_id ${taskId}`);
|
|
1488
|
+
}
|
|
1489
|
+
seen.add(taskId);
|
|
1490
|
+
for (const key of ["title", "status", "acceptance"]) {
|
|
1491
|
+
if (!(key in item)) {
|
|
1492
|
+
errors.push(`${label} ${taskId} missing ${key}`);
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
1497
|
+
function validatePrototypeEvidence(root, label, data, errors) {
|
|
1498
|
+
if (!["image_prompt_pack", "visual", "interaction", "technical_feasibility", "3d_material", "workflow", "data_logic"].includes(String(data.prototype_mode))) {
|
|
1499
|
+
errors.push(`${label} has invalid prototype_mode ${String(data.prototype_mode)}`);
|
|
1500
|
+
}
|
|
1501
|
+
for (const key of ["reference_analysis", "concept_evidence", "implementation_evidence", "known_limits"]) {
|
|
1502
|
+
if (key in data && !Array.isArray(data[key])) {
|
|
1503
|
+
errors.push(`${label} ${key} must be an array`);
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
if ("visual_direction" in data && !isRecord(data.visual_direction)) {
|
|
1507
|
+
errors.push(`${label} visual_direction must be a mapping`);
|
|
1508
|
+
}
|
|
1509
|
+
if ("visual_concept_policy" in data) {
|
|
1510
|
+
validateVisualConceptPolicy(label, data, errors);
|
|
1511
|
+
}
|
|
1512
|
+
if ("verification" in data && !isRecord(data.verification)) {
|
|
1513
|
+
errors.push(`${label} verification must be a mapping`);
|
|
1514
|
+
}
|
|
1515
|
+
if ("self_critique" in data && !isRecord(data.self_critique)) {
|
|
1516
|
+
errors.push(`${label} self_critique must be a mapping`);
|
|
1517
|
+
}
|
|
1518
|
+
else {
|
|
1519
|
+
validateSelfCritique(label, data.self_critique, errors);
|
|
1520
|
+
}
|
|
1521
|
+
const prototypeArtifact = data.prototype_artifact;
|
|
1522
|
+
if (isRecord(prototypeArtifact)) {
|
|
1523
|
+
for (const key of ["path", "type"]) {
|
|
1524
|
+
if (!(key in prototypeArtifact)) {
|
|
1525
|
+
errors.push(`${label} prototype_artifact missing ${key}`);
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
validateLocalRef(root, label, "prototype_artifact.path", prototypeArtifact.path, errors);
|
|
1529
|
+
}
|
|
1530
|
+
validateEvidenceRefs(root, label, data, errors);
|
|
1531
|
+
if ("validation_input" in data && !isRecord(data.validation_input)) {
|
|
1532
|
+
errors.push(`${label} validation_input must be a mapping`);
|
|
1533
|
+
}
|
|
1534
|
+
else {
|
|
1535
|
+
validatePrototypeValidationInput(label, data.validation_input, errors);
|
|
1536
|
+
}
|
|
1537
|
+
if ("source" in data && !isRecord(data.source)) {
|
|
1538
|
+
errors.push(`${label} source must be a mapping`);
|
|
1539
|
+
}
|
|
1540
|
+
if ("negative_constraints" in data && !Array.isArray(data.negative_constraints)) {
|
|
1541
|
+
errors.push(`${label} negative_constraints must be an array`);
|
|
1542
|
+
}
|
|
1543
|
+
if ("review_plan" in data && !isRecord(data.review_plan)) {
|
|
1544
|
+
errors.push(`${label} review_plan must be a mapping`);
|
|
1545
|
+
}
|
|
1546
|
+
if ("directions" in data && !Array.isArray(data.directions)) {
|
|
1547
|
+
errors.push(`${label} directions must be an array`);
|
|
1548
|
+
}
|
|
1549
|
+
if ("screen_manifest" in data && !Array.isArray(data.screen_manifest)) {
|
|
1550
|
+
errors.push(`${label} screen_manifest must be an array`);
|
|
1551
|
+
}
|
|
1552
|
+
if ("screen_prompts" in data && !Array.isArray(data.screen_prompts)) {
|
|
1553
|
+
errors.push(`${label} screen_prompts must be an array`);
|
|
1554
|
+
}
|
|
1555
|
+
if ("prompt_pack_type" in data && !["strategic_proto_prompt_pack", "refined_proto_prompt_pack", "proto_review_evidence"].includes(String(data.prompt_pack_type))) {
|
|
1556
|
+
errors.push(`${label} has invalid prompt_pack_type ${String(data.prompt_pack_type)}`);
|
|
1557
|
+
}
|
|
1558
|
+
if (data.prompt_pack_type === "strategic_proto_prompt_pack") {
|
|
1559
|
+
validateStrategicPrototypePromptPack(label, data, errors);
|
|
1560
|
+
}
|
|
1561
|
+
if (data.prompt_pack_type === "refined_proto_prompt_pack") {
|
|
1562
|
+
validateRefinedPrototypePromptPack(label, data, errors);
|
|
1563
|
+
}
|
|
1564
|
+
if (!["pass", "fail", "unclear", "not_reviewed"].includes(String(data.result))) {
|
|
1565
|
+
errors.push(`${label} has invalid result ${String(data.result)}`);
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
function validateRefinedPrototypePromptPack(label, data, errors) {
|
|
1569
|
+
validateRequiredObjectFields(label, "tune_input", data.tune_input, ["baseline_source_type", "baseline_refs", "tune_request", "regeneration_scope"], errors);
|
|
1570
|
+
validateRefinedBaselineResolution(label, data.baseline_resolution, errors);
|
|
1571
|
+
validateRefinedCarryForward(label, data.carry_forward, errors);
|
|
1572
|
+
validateRefinedBaselineAudit(label, data.baseline_audit, errors);
|
|
1573
|
+
validateRequiredObjectFields(label, "product_system", data.product_system, REFINED_PRODUCT_SYSTEM_FIELDS, errors);
|
|
1574
|
+
validateRefinedDeltaRules(label, data.delta_rules, data.tune_input, errors);
|
|
1575
|
+
validateRefinedScreenDeltaMatrix(label, data.screen_delta_matrix, errors);
|
|
1576
|
+
const manifestIds = validateRefinedScreenManifest(label, data.screen_manifest, errors);
|
|
1577
|
+
validateRefinedScreenPrompts(label, data.screen_prompts, manifestIds, errors);
|
|
1578
|
+
if (!Array.isArray(data.generation_order) || data.generation_order.length === 0) {
|
|
1579
|
+
errors.push(`${label} generation_order must list target screen ids`);
|
|
1580
|
+
}
|
|
1581
|
+
if (!Array.isArray(data.acceptance_checklist) || data.acceptance_checklist.length === 0) {
|
|
1582
|
+
errors.push(`${label} acceptance_checklist must be non-empty`);
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
const REFINED_BASELINE_AUDIT_FIELDS = [
|
|
1586
|
+
"source_screen_id",
|
|
1587
|
+
"screen_name",
|
|
1588
|
+
"journey_stage",
|
|
1589
|
+
"user_goal",
|
|
1590
|
+
"system_state",
|
|
1591
|
+
"components",
|
|
1592
|
+
"must_preserve",
|
|
1593
|
+
];
|
|
1594
|
+
const REFINED_BASELINE_RESOLUTION_FIELDS = [
|
|
1595
|
+
"latest_approved_baseline_group_id",
|
|
1596
|
+
"latest_approved_baseline_ref",
|
|
1597
|
+
"baseline_lineage",
|
|
1598
|
+
"resolution_rule",
|
|
1599
|
+
"stale_source_guard",
|
|
1600
|
+
];
|
|
1601
|
+
const REFINED_CARRY_FORWARD_FIELDS = [
|
|
1602
|
+
"locked_screens",
|
|
1603
|
+
"locked_elements",
|
|
1604
|
+
"preserved_improvements",
|
|
1605
|
+
"explicit_unlocks",
|
|
1606
|
+
"cumulative_drift_guard",
|
|
1607
|
+
];
|
|
1608
|
+
const REFINED_PRODUCT_SYSTEM_FIELDS = [
|
|
1609
|
+
"product_thesis",
|
|
1610
|
+
"primary_loop",
|
|
1611
|
+
"component_vocabulary",
|
|
1612
|
+
"copywriting_style",
|
|
1613
|
+
"trust_and_boundary_system",
|
|
1614
|
+
"stable_constants",
|
|
1615
|
+
"adaptable_variables",
|
|
1616
|
+
];
|
|
1617
|
+
const REFINED_DELTA_RULE_KEYS = ["must_inherit", "must_add", "must_remove", "flexible_change"];
|
|
1618
|
+
const REFINED_SCREEN_DELTA_FIELDS = [
|
|
1619
|
+
"target_screen_id",
|
|
1620
|
+
"source_screen_ids",
|
|
1621
|
+
"preserve",
|
|
1622
|
+
"add",
|
|
1623
|
+
"remove",
|
|
1624
|
+
"transform",
|
|
1625
|
+
"flexible",
|
|
1626
|
+
"acceptance_criteria",
|
|
1627
|
+
];
|
|
1628
|
+
const REFINED_SCREEN_MANIFEST_FIELDS = ["target_screen_id", "source_screen_ids", "screen_name", "generation_scope"];
|
|
1629
|
+
const REFINED_SCREEN_PROMPT_FIELDS = ["prompt_id", "target_screen_id", "source_screen_ids", "screen_name", "prompt", "negative_prompt", "acceptance_criteria"];
|
|
1630
|
+
function validateRefinedBaselineAudit(label, value, errors) {
|
|
1631
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
1632
|
+
errors.push(`${label} baseline_audit must contain source screen audits`);
|
|
1633
|
+
return;
|
|
1634
|
+
}
|
|
1635
|
+
value.forEach((item, index) => {
|
|
1636
|
+
validateRequiredObjectFields(label, `baseline_audit[${index}]`, item, REFINED_BASELINE_AUDIT_FIELDS, errors);
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1639
|
+
function validateRefinedBaselineResolution(label, value, errors) {
|
|
1640
|
+
validateRequiredObjectFields(label, "baseline_resolution", value, REFINED_BASELINE_RESOLUTION_FIELDS, errors);
|
|
1641
|
+
if (!isRecord(value)) {
|
|
1642
|
+
return;
|
|
1643
|
+
}
|
|
1644
|
+
if (!Array.isArray(value.baseline_lineage)) {
|
|
1645
|
+
errors.push(`${label} baseline_resolution.baseline_lineage must be an array`);
|
|
1646
|
+
}
|
|
1647
|
+
if (String(value.stale_source_guard ?? "").trim().length === 0) {
|
|
1648
|
+
errors.push(`${label} baseline_resolution.stale_source_guard must forbid stale source-screen fallback`);
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
function validateRefinedCarryForward(label, value, errors) {
|
|
1652
|
+
if (!isRecord(value)) {
|
|
1653
|
+
errors.push(`${label} carry_forward must be a mapping`);
|
|
1654
|
+
return;
|
|
1655
|
+
}
|
|
1656
|
+
for (const key of REFINED_CARRY_FORWARD_FIELDS) {
|
|
1657
|
+
if (!Object.hasOwn(value, key)) {
|
|
1658
|
+
errors.push(`${label} carry_forward.${key} must be present`);
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
for (const key of ["locked_screens", "locked_elements", "preserved_improvements", "explicit_unlocks"]) {
|
|
1662
|
+
if (!Array.isArray(value[key])) {
|
|
1663
|
+
errors.push(`${label} carry_forward.${key} must be an array`);
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
if (String(value.cumulative_drift_guard ?? "").trim().length === 0) {
|
|
1667
|
+
errors.push(`${label} carry_forward.cumulative_drift_guard must forbid cumulative tune drift`);
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
function validateRefinedDeltaRules(label, value, tuneInput, errors) {
|
|
1671
|
+
if (!isRecord(value)) {
|
|
1672
|
+
errors.push(`${label} delta_rules must be a mapping`);
|
|
1673
|
+
return;
|
|
1674
|
+
}
|
|
1675
|
+
for (const key of REFINED_DELTA_RULE_KEYS) {
|
|
1676
|
+
if (!Array.isArray(value[key])) {
|
|
1677
|
+
errors.push(`${label} delta_rules.${key} must be an array`);
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
if (Array.isArray(value.must_inherit) && value.must_inherit.length === 0) {
|
|
1681
|
+
errors.push(`${label} delta_rules.must_inherit must preserve baseline product-system constants`);
|
|
1682
|
+
}
|
|
1683
|
+
const tuneRequest = isRecord(tuneInput) ? String(tuneInput.tune_request ?? "").toLowerCase() : "";
|
|
1684
|
+
if (/\b(remove|delete|drop|eliminate|hide)\b/.test(tuneRequest) && Array.isArray(value.must_remove) && value.must_remove.length === 0) {
|
|
1685
|
+
errors.push(`${label} delta_rules.must_remove must name requested removals from tune_input.tune_request`);
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
function validateRefinedScreenDeltaMatrix(label, value, errors) {
|
|
1689
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
1690
|
+
errors.push(`${label} screen_delta_matrix must contain target screen delta rows`);
|
|
1691
|
+
return;
|
|
1692
|
+
}
|
|
1693
|
+
value.forEach((item, index) => {
|
|
1694
|
+
validateRequiredObjectFields(label, `screen_delta_matrix[${index}]`, item, REFINED_SCREEN_DELTA_FIELDS, errors);
|
|
1695
|
+
});
|
|
1696
|
+
}
|
|
1697
|
+
function validateRefinedScreenManifest(label, value, errors) {
|
|
1698
|
+
const ids = new Set();
|
|
1699
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
1700
|
+
errors.push(`${label} screen_manifest must contain target screens`);
|
|
1701
|
+
return ids;
|
|
1702
|
+
}
|
|
1703
|
+
value.forEach((item, index) => {
|
|
1704
|
+
validateRequiredObjectFields(label, `screen_manifest[${index}]`, item, REFINED_SCREEN_MANIFEST_FIELDS, errors);
|
|
1705
|
+
if (isRecord(item) && nonEmptyString(item.target_screen_id)) {
|
|
1706
|
+
ids.add(String(item.target_screen_id));
|
|
1707
|
+
}
|
|
1708
|
+
});
|
|
1709
|
+
return ids;
|
|
1710
|
+
}
|
|
1711
|
+
function validateRefinedScreenPrompts(label, value, manifestIds, errors) {
|
|
1712
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
1713
|
+
errors.push(`${label} screen_prompts must contain screen-bound refined prompts`);
|
|
1714
|
+
return;
|
|
1715
|
+
}
|
|
1716
|
+
value.forEach((item, index) => {
|
|
1717
|
+
validateRequiredObjectFields(label, `screen_prompts[${index}]`, item, REFINED_SCREEN_PROMPT_FIELDS, errors);
|
|
1718
|
+
if (isRecord(item) && nonEmptyString(item.target_screen_id) && manifestIds.size > 0 && !manifestIds.has(String(item.target_screen_id))) {
|
|
1719
|
+
errors.push(`${label} screen_prompts[${index}].target_screen_id must exist in screen_manifest`);
|
|
1720
|
+
}
|
|
1721
|
+
});
|
|
1722
|
+
}
|
|
1723
|
+
const STRATEGIC_NORMALIZED_FIELDS = [
|
|
1724
|
+
"product_domain",
|
|
1725
|
+
"primary_user",
|
|
1726
|
+
"usage_context",
|
|
1727
|
+
"current_alternative",
|
|
1728
|
+
"core_pain",
|
|
1729
|
+
"desired_behavior_change",
|
|
1730
|
+
"strongest_success_signal",
|
|
1731
|
+
"core_differentiator",
|
|
1732
|
+
"emotional_value",
|
|
1733
|
+
"functional_value",
|
|
1734
|
+
"trust_requirements",
|
|
1735
|
+
"privacy_requirements",
|
|
1736
|
+
"non_goals",
|
|
1737
|
+
"future_opportunities",
|
|
1738
|
+
"validation_target",
|
|
1739
|
+
];
|
|
1740
|
+
const STRATEGIC_CORE_FIELDS = [
|
|
1741
|
+
"target_user",
|
|
1742
|
+
"behavior_change",
|
|
1743
|
+
"mechanism",
|
|
1744
|
+
"differentiator",
|
|
1745
|
+
"boundary_conditions",
|
|
1746
|
+
"central_uncertainty",
|
|
1747
|
+
];
|
|
1748
|
+
const PRODUCT_EXPERIENCE_MODEL_FIELDS = [
|
|
1749
|
+
"product_archetype",
|
|
1750
|
+
"primary_canvas",
|
|
1751
|
+
"information_architecture",
|
|
1752
|
+
"domain_object_model",
|
|
1753
|
+
"primary_task_loop",
|
|
1754
|
+
"interaction_state_model",
|
|
1755
|
+
"data_realism_requirements",
|
|
1756
|
+
"visual_language",
|
|
1757
|
+
"anti_generic_constraints",
|
|
1758
|
+
];
|
|
1759
|
+
const PROTOTYPE_SYSTEM_CONTRACT_FIELDS = [
|
|
1760
|
+
"stable_app_shell",
|
|
1761
|
+
"navigation_taxonomy",
|
|
1762
|
+
"data_vocabulary",
|
|
1763
|
+
"domain_object_anatomy",
|
|
1764
|
+
"object_detail_anatomy",
|
|
1765
|
+
"action_bar_contract",
|
|
1766
|
+
"audit_trust_pattern",
|
|
1767
|
+
"copy_tone",
|
|
1768
|
+
"allowed_screen_deltas",
|
|
1769
|
+
];
|
|
1770
|
+
const PROTOTYPE_REALITY_GATE_DIMENSIONS = [
|
|
1771
|
+
"product_category_fit",
|
|
1772
|
+
"primary_canvas_fit",
|
|
1773
|
+
"domain_object_realism",
|
|
1774
|
+
"task_loop_completeness",
|
|
1775
|
+
"interaction_state_coverage",
|
|
1776
|
+
"data_realism",
|
|
1777
|
+
"anti_generic_constraints",
|
|
1778
|
+
];
|
|
1779
|
+
const PROMPT_PACK_INTEGRITY_GATE_DIMENSIONS = [
|
|
1780
|
+
"direction_count_matches",
|
|
1781
|
+
"prompt_text_refs_resolve",
|
|
1782
|
+
"generated_image_refs_resolve",
|
|
1783
|
+
];
|
|
1784
|
+
const STRATEGIC_PROTOTYPE_BRIEF_FIELDS = [
|
|
1785
|
+
"product_name",
|
|
1786
|
+
"positioning",
|
|
1787
|
+
"target_user",
|
|
1788
|
+
"current_alternative",
|
|
1789
|
+
"core_idea",
|
|
1790
|
+
"primary_loop",
|
|
1791
|
+
"trust_boundaries",
|
|
1792
|
+
"non_goals",
|
|
1793
|
+
"desired_feeling",
|
|
1794
|
+
];
|
|
1795
|
+
const STRATEGIC_SCREEN_MANIFEST_FIELDS = [
|
|
1796
|
+
"target_screen_id",
|
|
1797
|
+
"screen_name",
|
|
1798
|
+
"journey_stage",
|
|
1799
|
+
"user_goal",
|
|
1800
|
+
"system_state",
|
|
1801
|
+
"required_components",
|
|
1802
|
+
"required_data_fields",
|
|
1803
|
+
"primary_actions",
|
|
1804
|
+
"trust_controls",
|
|
1805
|
+
"example_copy",
|
|
1806
|
+
"acceptance_criteria",
|
|
1807
|
+
];
|
|
1808
|
+
const STRATEGIC_GLOBAL_DESIGN_SYSTEM_PROMPT_FIELDS = [
|
|
1809
|
+
"visual_language",
|
|
1810
|
+
"layout_system",
|
|
1811
|
+
"component_vocabulary",
|
|
1812
|
+
"information_density",
|
|
1813
|
+
"copy_tone",
|
|
1814
|
+
"responsive_canvas_rules",
|
|
1815
|
+
"negative_visual_patterns",
|
|
1816
|
+
];
|
|
1817
|
+
const STRATEGIC_SCREEN_PROMPT_FIELDS = [
|
|
1818
|
+
"prompt_id",
|
|
1819
|
+
"target_screen_id",
|
|
1820
|
+
"screen_name",
|
|
1821
|
+
"image_role",
|
|
1822
|
+
"negative_prompt",
|
|
1823
|
+
"example_copy",
|
|
1824
|
+
"acceptance_criteria",
|
|
1825
|
+
];
|
|
1826
|
+
const STRATEGIC_QUALITY_RUBRIC_FIELDS = [
|
|
1827
|
+
"prompt_executability",
|
|
1828
|
+
"strategic_distinctness",
|
|
1829
|
+
"product_specificity",
|
|
1830
|
+
"state_coverage",
|
|
1831
|
+
"trust_boundary_coverage",
|
|
1832
|
+
];
|
|
1833
|
+
const STRATEGIC_DIRECTION_FIELDS = [
|
|
1834
|
+
"direction_id",
|
|
1835
|
+
"name",
|
|
1836
|
+
"strategic_hypothesis",
|
|
1837
|
+
"validates",
|
|
1838
|
+
"main_risk",
|
|
1839
|
+
"distinctness_rationale",
|
|
1840
|
+
"prototype_prompt",
|
|
1841
|
+
"screen_prompts",
|
|
1842
|
+
"pm_judgment",
|
|
1843
|
+
];
|
|
1844
|
+
const STRATEGIC_DISTINCTNESS_SIGNALS = [
|
|
1845
|
+
"product form",
|
|
1846
|
+
"trigger",
|
|
1847
|
+
"interaction model",
|
|
1848
|
+
"emotional driver",
|
|
1849
|
+
"retention mechanism",
|
|
1850
|
+
"metric",
|
|
1851
|
+
"main risk",
|
|
1852
|
+
"risk",
|
|
1853
|
+
"user behavior",
|
|
1854
|
+
"workflow",
|
|
1855
|
+
"trust",
|
|
1856
|
+
"privacy",
|
|
1857
|
+
];
|
|
1858
|
+
const STRATEGIC_FINGERPRINT_DIMENSIONS = [
|
|
1859
|
+
"product_form",
|
|
1860
|
+
"trigger",
|
|
1861
|
+
"interaction_model",
|
|
1862
|
+
"emotional_driver",
|
|
1863
|
+
"retention_mechanism",
|
|
1864
|
+
"metric",
|
|
1865
|
+
"main_risk",
|
|
1866
|
+
"trust_model",
|
|
1867
|
+
"privacy_model",
|
|
1868
|
+
];
|
|
1869
|
+
function validatePrototypeValidationInput(label, value, errors) {
|
|
1870
|
+
if (!isRecord(value)) {
|
|
1871
|
+
return;
|
|
1872
|
+
}
|
|
1873
|
+
const mode = String(value.mode ?? "");
|
|
1874
|
+
if (mode && !["validation_present", "agent_auto_generated"].includes(mode)) {
|
|
1875
|
+
errors.push(`${label} validation_input.mode has invalid value ${mode}`);
|
|
1876
|
+
}
|
|
1877
|
+
if (mode === "vision_only" || mode === "internally_derived") {
|
|
1878
|
+
errors.push(`${label} validation_input.mode must reference durable validation, not ${mode}`);
|
|
1879
|
+
}
|
|
1880
|
+
if (!Array.isArray(value.refs)) {
|
|
1881
|
+
errors.push(`${label} validation_input.refs must be an array`);
|
|
1882
|
+
}
|
|
1883
|
+
else if (mode && value.refs.length === 0) {
|
|
1884
|
+
errors.push(`${label} validation_input.refs must include durable validation artifact refs`);
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
function validateStrategicPrototypePromptPack(label, data, errors) {
|
|
1888
|
+
validatePreflightQualityGate(label, data.preflight_quality_gate, errors);
|
|
1889
|
+
validateInternalPipeline(label, data.internal_pipeline, errors);
|
|
1890
|
+
validateDirectionCountPolicy(label, data.direction_count_policy, errors);
|
|
1891
|
+
validateRequiredObjectFields(label, "normalized_input", data.normalized_input, STRATEGIC_NORMALIZED_FIELDS, errors);
|
|
1892
|
+
validateRequiredObjectFields(label, "strategic_core", data.strategic_core, STRATEGIC_CORE_FIELDS, errors);
|
|
1893
|
+
validateProductExperienceModel(label, data.product_experience_model, data, errors);
|
|
1894
|
+
validatePrototypeSystemContract(label, data.prototype_system_contract, data, errors);
|
|
1895
|
+
validatePrototypeRealityGate(label, data.prototype_reality_gate, data, errors);
|
|
1896
|
+
validatePromptPackIntegrityGate(label, data.prompt_pack_integrity_gate, data, errors);
|
|
1897
|
+
validateScreenBoundExecutability(label, data, errors);
|
|
1898
|
+
validateStrategicDirections(label, data.directions, data.direction_count_policy, errors);
|
|
1899
|
+
validateBuildRecommendation(label, data.build_recommendation, errors);
|
|
1900
|
+
validatePromptTextManifest(label, data.prompt_text_manifest, errors);
|
|
1901
|
+
validatePostValidate(label, data.post_validate, data.direction_count_policy, data.prompt_text_manifest, data.image_generation, data.directions, errors);
|
|
1902
|
+
validateImageGeneration(label, data.image_generation, errors);
|
|
1903
|
+
}
|
|
1904
|
+
function validatePreflightQualityGate(label, value, errors) {
|
|
1905
|
+
if (!isRecord(value)) {
|
|
1906
|
+
errors.push(`${label} preflight_quality_gate must be a mapping`);
|
|
1907
|
+
return;
|
|
1908
|
+
}
|
|
1909
|
+
for (const key of ["vision_status", "validation_status", "can_proceed", "blockers", "next_command_when_blocked"]) {
|
|
1910
|
+
if (!(key in value)) {
|
|
1911
|
+
errors.push(`${label} preflight_quality_gate missing ${key}`);
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
for (const key of ["vision_status", "validation_status"]) {
|
|
1915
|
+
const status = String(value[key] ?? "");
|
|
1916
|
+
if (status && !["missing", "thin", "ready"].includes(status)) {
|
|
1917
|
+
errors.push(`${label} preflight_quality_gate.${key} has invalid value ${status}`);
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
if (value.can_proceed !== true && value.next_command_when_blocked !== "/ow:vision") {
|
|
1921
|
+
errors.push(`${label} preflight_quality_gate must route blocked prototype work back to /ow:vision`);
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
function validateInternalPipeline(label, value, errors) {
|
|
1925
|
+
if (!isRecord(value)) {
|
|
1926
|
+
errors.push(`${label} internal_pipeline must be a mapping`);
|
|
1927
|
+
return;
|
|
1928
|
+
}
|
|
1929
|
+
if (value.orchestrator_command !== "/ow:proto" || value.user_visible_command !== "/ow:proto") {
|
|
1930
|
+
errors.push(`${label} internal_pipeline must keep /ow:proto as the user-visible orchestrator`);
|
|
1931
|
+
}
|
|
1932
|
+
const stages = value.stages;
|
|
1933
|
+
if (!Array.isArray(stages)) {
|
|
1934
|
+
errors.push(`${label} internal_pipeline.stages must be an array`);
|
|
1935
|
+
return;
|
|
1936
|
+
}
|
|
1937
|
+
const stageIds = new Set();
|
|
1938
|
+
for (const item of stages) {
|
|
1939
|
+
if (!isRecord(item)) {
|
|
1940
|
+
errors.push(`${label} internal_pipeline.stages entries must be mappings`);
|
|
1941
|
+
continue;
|
|
1942
|
+
}
|
|
1943
|
+
const stageId = String(item.stage_id ?? "");
|
|
1944
|
+
stageIds.add(stageId);
|
|
1945
|
+
for (const key of ["stage_id", "command", "visibility", "status", "outputs"]) {
|
|
1946
|
+
if (!(key in item)) {
|
|
1947
|
+
errors.push(`${label} internal_pipeline stage missing ${key}`);
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
if ((stageId === "vision2prompt" || stageId === "prompt2proto") && item.visibility !== "internal") {
|
|
1951
|
+
errors.push(`${label} internal pipeline stage ${stageId} must be internal`);
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
for (const required of ["proto-preflight", "vision2prompt", "prompt2proto"]) {
|
|
1955
|
+
if (!stageIds.has(required)) {
|
|
1956
|
+
errors.push(`${label} internal_pipeline missing stage ${required}`);
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
function validateDirectionCountPolicy(label, value, errors) {
|
|
1961
|
+
if (!isRecord(value)) {
|
|
1962
|
+
errors.push(`${label} direction_count_policy must be a mapping`);
|
|
1963
|
+
return;
|
|
1964
|
+
}
|
|
1965
|
+
const source = String(value.source ?? "");
|
|
1966
|
+
if (source && !["user_input", "agent_default_after_user_delegation"].includes(source)) {
|
|
1967
|
+
errors.push(`${label} direction_count_policy.source has invalid value ${source}`);
|
|
1968
|
+
}
|
|
1969
|
+
if (typeof value.resolved_count !== "number" || value.resolved_count < 1) {
|
|
1970
|
+
errors.push(`${label} direction_count_policy.resolved_count must be a positive number`);
|
|
1971
|
+
}
|
|
1972
|
+
if (source === "agent_default_after_user_delegation" && value.resolved_count !== 3) {
|
|
1973
|
+
errors.push(`${label} direction_count_policy delegated default must resolve to 3`);
|
|
1974
|
+
}
|
|
1975
|
+
if (value.ask_user_question_required === true && !nonEmptyString(value.ask_user_question)) {
|
|
1976
|
+
errors.push(`${label} direction_count_policy.ask_user_question must be set when askUserQuestion is required`);
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
function validateRequiredObjectFields(label, field, value, keys, errors) {
|
|
1980
|
+
if (!isRecord(value)) {
|
|
1981
|
+
errors.push(`${label} ${field} must be a mapping`);
|
|
1982
|
+
return;
|
|
1983
|
+
}
|
|
1984
|
+
for (const key of keys) {
|
|
1985
|
+
if (!hasUsefulValue(value[key])) {
|
|
1986
|
+
errors.push(`${label} ${field}.${key} must be non-empty`);
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
function validateProductExperienceModel(label, value, data, errors) {
|
|
1991
|
+
if (!strategicPromptPackRequiresProductExperienceModel(data)) {
|
|
1992
|
+
if ("product_experience_model" in data && !isRecord(value)) {
|
|
1993
|
+
errors.push(`${label} product_experience_model must be a mapping when present`);
|
|
1994
|
+
}
|
|
1995
|
+
return;
|
|
1996
|
+
}
|
|
1997
|
+
validateRequiredObjectFields(label, "product_experience_model", value, PRODUCT_EXPERIENCE_MODEL_FIELDS, errors);
|
|
1998
|
+
}
|
|
1999
|
+
function strategicPromptPackRequiresProductExperienceModel(data) {
|
|
2000
|
+
const promptTextManifest = isRecord(data.prompt_text_manifest) ? data.prompt_text_manifest : {};
|
|
2001
|
+
const imageGeneration = isRecord(data.image_generation) ? data.image_generation : {};
|
|
2002
|
+
return (data.status !== "draft" ||
|
|
2003
|
+
promptTextManifest.status === "ready_for_image_generation" ||
|
|
2004
|
+
promptTextManifest.status === "generated" ||
|
|
2005
|
+
(typeof imageGeneration.status === "string" && imageGeneration.status !== "not_started"));
|
|
2006
|
+
}
|
|
2007
|
+
function validatePrototypeSystemContract(label, value, data, errors) {
|
|
2008
|
+
const required = strategicPromptPackRequiresProductExperienceModel(data);
|
|
2009
|
+
if (!required) {
|
|
2010
|
+
if ("prototype_system_contract" in data && !isRecord(value)) {
|
|
2011
|
+
errors.push(`${label} prototype_system_contract must be a mapping when present`);
|
|
2012
|
+
}
|
|
2013
|
+
return;
|
|
2014
|
+
}
|
|
2015
|
+
validateRequiredObjectFields(label, "prototype_system_contract", value, PROTOTYPE_SYSTEM_CONTRACT_FIELDS, errors);
|
|
2016
|
+
if (!isRecord(value)) {
|
|
2017
|
+
return;
|
|
2018
|
+
}
|
|
2019
|
+
for (const key of PROTOTYPE_SYSTEM_CONTRACT_FIELDS) {
|
|
2020
|
+
if (key === "copy_tone") {
|
|
2021
|
+
if (!nonEmptyString(value[key])) {
|
|
2022
|
+
errors.push(`${label} prototype_system_contract.copy_tone must be a non-empty string`);
|
|
2023
|
+
}
|
|
2024
|
+
continue;
|
|
2025
|
+
}
|
|
2026
|
+
if (!Array.isArray(value[key]) || value[key].length === 0) {
|
|
2027
|
+
errors.push(`${label} prototype_system_contract.${key} must be a non-empty array`);
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
function validatePrototypeRealityGate(label, value, data, errors) {
|
|
2032
|
+
const required = strategicPromptPackRequiresProductExperienceModel(data);
|
|
2033
|
+
if (!required) {
|
|
2034
|
+
if ("prototype_reality_gate" in data && !isRecord(value)) {
|
|
2035
|
+
errors.push(`${label} prototype_reality_gate must be a mapping when present`);
|
|
2036
|
+
}
|
|
2037
|
+
return;
|
|
2038
|
+
}
|
|
2039
|
+
if (!isRecord(value)) {
|
|
2040
|
+
errors.push(`${label} prototype_reality_gate must be a mapping`);
|
|
2041
|
+
return;
|
|
2042
|
+
}
|
|
2043
|
+
for (const key of ["status", "trigger", "required_when_prompt_text_ready", "dimensions", "failures", "outcome_notes", "repair_route"]) {
|
|
2044
|
+
if (!(key in value)) {
|
|
2045
|
+
errors.push(`${label} prototype_reality_gate missing ${key}`);
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
const status = String(value.status ?? "");
|
|
2049
|
+
if (status && !["pending", "pass", "fail"].includes(status)) {
|
|
2050
|
+
errors.push(`${label} prototype_reality_gate.status has invalid value ${status}`);
|
|
2051
|
+
}
|
|
2052
|
+
if (value.trigger !== "before_image_generation") {
|
|
2053
|
+
errors.push(`${label} prototype_reality_gate.trigger must be before_image_generation`);
|
|
2054
|
+
}
|
|
2055
|
+
if (value.required_when_prompt_text_ready !== true) {
|
|
2056
|
+
errors.push(`${label} prototype_reality_gate.required_when_prompt_text_ready must be true`);
|
|
2057
|
+
}
|
|
2058
|
+
if (value.repair_route !== "/ow:vision2prompt") {
|
|
2059
|
+
errors.push(`${label} prototype_reality_gate.repair_route must be /ow:vision2prompt`);
|
|
2060
|
+
}
|
|
2061
|
+
if (!Array.isArray(value.dimensions)) {
|
|
2062
|
+
errors.push(`${label} prototype_reality_gate.dimensions must be an array`);
|
|
2063
|
+
}
|
|
2064
|
+
else {
|
|
2065
|
+
for (const dimension of PROTOTYPE_REALITY_GATE_DIMENSIONS) {
|
|
2066
|
+
if (!value.dimensions.includes(dimension)) {
|
|
2067
|
+
errors.push(`${label} prototype_reality_gate.dimensions missing ${dimension}`);
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
for (const key of ["failures", "outcome_notes"]) {
|
|
2072
|
+
if (!Array.isArray(value[key])) {
|
|
2073
|
+
errors.push(`${label} prototype_reality_gate.${key} must be an array`);
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
const promptTextManifest = isRecord(data.prompt_text_manifest) ? data.prompt_text_manifest : {};
|
|
2077
|
+
const imageGeneration = isRecord(data.image_generation) ? data.image_generation : {};
|
|
2078
|
+
const promptTextReady = promptTextManifest.status === "ready_for_image_generation" || promptTextManifest.status === "generated";
|
|
2079
|
+
if (promptTextReady && status !== "pass") {
|
|
2080
|
+
errors.push(`${label} prototype_reality_gate.status must be pass before image generation`);
|
|
2081
|
+
}
|
|
2082
|
+
if (status === "pass" && Array.isArray(value.failures) && value.failures.length > 0) {
|
|
2083
|
+
errors.push(`${label} prototype_reality_gate.failures must be empty when status is pass`);
|
|
2084
|
+
}
|
|
2085
|
+
if (status === "fail" && typeof imageGeneration.status === "string" && imageGeneration.status !== "not_started") {
|
|
2086
|
+
errors.push(`${label} prototype_reality_gate failed gates must not start image_generation`);
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
function validatePromptPackIntegrityGate(label, value, data, errors) {
|
|
2090
|
+
const required = strategicPromptPackRequiresProductExperienceModel(data);
|
|
2091
|
+
if (!required) {
|
|
2092
|
+
if ("prompt_pack_integrity_gate" in data && !isRecord(value)) {
|
|
2093
|
+
errors.push(`${label} prompt_pack_integrity_gate must be a mapping when present`);
|
|
2094
|
+
}
|
|
2095
|
+
return;
|
|
2096
|
+
}
|
|
2097
|
+
if (!isRecord(value)) {
|
|
2098
|
+
errors.push(`${label} prompt_pack_integrity_gate must be a mapping`);
|
|
2099
|
+
return;
|
|
2100
|
+
}
|
|
2101
|
+
for (const key of ["status", "trigger", "required_when_prompt_text_ready", "dimensions", "failures", "outcome_notes", "repair_route"]) {
|
|
2102
|
+
if (!(key in value)) {
|
|
2103
|
+
errors.push(`${label} prompt_pack_integrity_gate missing ${key}`);
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
const status = String(value.status ?? "");
|
|
2107
|
+
if (status && !["pending", "pass", "fail"].includes(status)) {
|
|
2108
|
+
errors.push(`${label} prompt_pack_integrity_gate.status has invalid value ${status}`);
|
|
2109
|
+
}
|
|
2110
|
+
if (value.trigger !== "before_image_generation") {
|
|
2111
|
+
errors.push(`${label} prompt_pack_integrity_gate.trigger must be before_image_generation`);
|
|
2112
|
+
}
|
|
2113
|
+
if (value.required_when_prompt_text_ready !== true) {
|
|
2114
|
+
errors.push(`${label} prompt_pack_integrity_gate.required_when_prompt_text_ready must be true`);
|
|
2115
|
+
}
|
|
2116
|
+
if (value.repair_route !== "/ow:vision2prompt") {
|
|
2117
|
+
errors.push(`${label} prompt_pack_integrity_gate.repair_route must be /ow:vision2prompt`);
|
|
2118
|
+
}
|
|
2119
|
+
if (!Array.isArray(value.dimensions)) {
|
|
2120
|
+
errors.push(`${label} prompt_pack_integrity_gate.dimensions must be an array`);
|
|
2121
|
+
}
|
|
2122
|
+
else {
|
|
2123
|
+
for (const dimension of PROMPT_PACK_INTEGRITY_GATE_DIMENSIONS) {
|
|
2124
|
+
if (!value.dimensions.includes(dimension)) {
|
|
2125
|
+
errors.push(`${label} prompt_pack_integrity_gate.dimensions missing ${dimension}`);
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
for (const key of ["failures", "outcome_notes"]) {
|
|
2130
|
+
if (!Array.isArray(value[key])) {
|
|
2131
|
+
errors.push(`${label} prompt_pack_integrity_gate.${key} must be an array`);
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
validatePromptPackIntegrity(label, data, errors);
|
|
2135
|
+
const promptTextManifest = isRecord(data.prompt_text_manifest) ? data.prompt_text_manifest : {};
|
|
2136
|
+
const imageGeneration = isRecord(data.image_generation) ? data.image_generation : {};
|
|
2137
|
+
const promptTextReady = promptTextManifest.status === "ready_for_image_generation" || promptTextManifest.status === "generated";
|
|
2138
|
+
if (promptTextReady && status !== "pass") {
|
|
2139
|
+
errors.push(`${label} prompt_pack_integrity_gate.status must be pass before image generation`);
|
|
2140
|
+
}
|
|
2141
|
+
if (status === "pass" && Array.isArray(value.failures) && value.failures.length > 0) {
|
|
2142
|
+
errors.push(`${label} prompt_pack_integrity_gate.failures must be empty when status is pass`);
|
|
2143
|
+
}
|
|
2144
|
+
if (status === "fail" && typeof imageGeneration.status === "string" && imageGeneration.status !== "not_started") {
|
|
2145
|
+
errors.push(`${label} prompt_pack_integrity_gate failed gates must not start image_generation`);
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
function validatePromptPackIntegrity(label, data, errors) {
|
|
2149
|
+
const directions = Array.isArray(data.directions) ? data.directions.filter(isRecord) : [];
|
|
2150
|
+
const directionIds = new Set();
|
|
2151
|
+
const promptIds = new Set();
|
|
2152
|
+
for (const direction of directions) {
|
|
2153
|
+
if (nonEmptyString(direction.direction_id)) {
|
|
2154
|
+
directionIds.add(String(direction.direction_id));
|
|
2155
|
+
}
|
|
2156
|
+
if (Array.isArray(direction.screen_prompts)) {
|
|
2157
|
+
for (const prompt of direction.screen_prompts) {
|
|
2158
|
+
if (isRecord(prompt) && nonEmptyString(prompt.prompt_id)) {
|
|
2159
|
+
promptIds.add(String(prompt.prompt_id));
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
const countPolicy = isRecord(data.direction_count_policy) ? data.direction_count_policy : {};
|
|
2165
|
+
const promptTextManifest = isRecord(data.prompt_text_manifest) ? data.prompt_text_manifest : {};
|
|
2166
|
+
const promptTextReady = promptTextManifest.status === "ready_for_image_generation" || promptTextManifest.status === "generated";
|
|
2167
|
+
const resolvedCount = typeof countPolicy.resolved_count === "number" ? countPolicy.resolved_count : null;
|
|
2168
|
+
if (promptTextReady && resolvedCount !== null && directions.length !== resolvedCount) {
|
|
2169
|
+
errors.push(`${label} directions length must equal direction_count_policy.resolved_count before image generation`);
|
|
2170
|
+
}
|
|
2171
|
+
if (promptTextReady && typeof promptTextManifest.direction_count !== "number") {
|
|
2172
|
+
errors.push(`${label} prompt_text_manifest.direction_count must be a number before image generation`);
|
|
2173
|
+
}
|
|
2174
|
+
if (typeof promptTextManifest.direction_count === "number" && directions.length > 0 && promptTextManifest.direction_count !== directions.length) {
|
|
2175
|
+
errors.push(`${label} prompt_text_manifest.direction_count must equal directions length`);
|
|
2176
|
+
}
|
|
2177
|
+
if (promptTextReady) {
|
|
2178
|
+
const refs = Array.isArray(promptTextManifest.prompt_text_refs) ? promptTextManifest.prompt_text_refs : [];
|
|
2179
|
+
if (refs.length === 0) {
|
|
2180
|
+
errors.push(`${label} prompt_text_manifest.prompt_text_refs must include prompt refs before image generation`);
|
|
2181
|
+
}
|
|
2182
|
+
refs.forEach((ref, index) => {
|
|
2183
|
+
if (!stringReferencesKnownPrompt(String(ref), directionIds, promptIds)) {
|
|
2184
|
+
errors.push(`${label} prompt_text_manifest.prompt_text_refs[${index}] must reference an existing direction_id or prompt_id`);
|
|
2185
|
+
}
|
|
2186
|
+
});
|
|
2187
|
+
}
|
|
2188
|
+
const imageGeneration = isRecord(data.image_generation) ? data.image_generation : {};
|
|
2189
|
+
const generatedImages = Array.isArray(imageGeneration.generated_images) ? imageGeneration.generated_images : [];
|
|
2190
|
+
generatedImages.forEach((image, index) => {
|
|
2191
|
+
if (!isRecord(image)) {
|
|
2192
|
+
return;
|
|
2193
|
+
}
|
|
2194
|
+
const directionId = String(image.direction_id ?? "");
|
|
2195
|
+
const promptId = String(image.prompt_id ?? "");
|
|
2196
|
+
if (directionId && !directionIds.has(directionId)) {
|
|
2197
|
+
errors.push(`${label} image_generation.generated_images[${index}].direction_id must exist in directions`);
|
|
2198
|
+
}
|
|
2199
|
+
if (promptId && !promptIds.has(promptId)) {
|
|
2200
|
+
errors.push(`${label} image_generation.generated_images[${index}].prompt_id must exist in directions[].screen_prompts`);
|
|
2201
|
+
}
|
|
2202
|
+
const metadata = isRecord(image.metadata) ? image.metadata : {};
|
|
2203
|
+
const sourcePromptRef = String(metadata.source_prompt_ref ?? "");
|
|
2204
|
+
if (sourcePromptRef && !stringReferencesKnownPrompt(sourcePromptRef, directionIds, promptIds)) {
|
|
2205
|
+
errors.push(`${label} image_generation.generated_images[${index}].metadata.source_prompt_ref must reference an existing direction_id or prompt_id`);
|
|
2206
|
+
}
|
|
2207
|
+
});
|
|
2208
|
+
}
|
|
2209
|
+
function validateScreenBoundExecutability(label, data, errors) {
|
|
2210
|
+
if (!strategicPromptPackRequiresScreenExecutability(data)) {
|
|
2211
|
+
return;
|
|
2212
|
+
}
|
|
2213
|
+
validateRequiredObjectFields(label, "prototype_brief", data.prototype_brief, STRATEGIC_PROTOTYPE_BRIEF_FIELDS, errors);
|
|
2214
|
+
validateRequiredObjectFields(label, "global_design_system_prompt", data.global_design_system_prompt, STRATEGIC_GLOBAL_DESIGN_SYSTEM_PROMPT_FIELDS, errors);
|
|
2215
|
+
validateRequiredObjectFields(label, "quality_rubric", data.quality_rubric, STRATEGIC_QUALITY_RUBRIC_FIELDS, errors);
|
|
2216
|
+
const manifestIds = validateStrategicScreenManifest(label, data.screen_manifest, errors);
|
|
2217
|
+
validateStrategicDirectionScreenPrompts(label, data.directions, manifestIds, errors);
|
|
2218
|
+
}
|
|
2219
|
+
function strategicPromptPackRequiresScreenExecutability(data) {
|
|
2220
|
+
const promptTextManifest = isRecord(data.prompt_text_manifest) ? data.prompt_text_manifest : {};
|
|
2221
|
+
const imageGeneration = isRecord(data.image_generation) ? data.image_generation : {};
|
|
2222
|
+
return (promptTextManifest.status === "ready_for_image_generation" ||
|
|
2223
|
+
promptTextManifest.status === "generated" ||
|
|
2224
|
+
(typeof imageGeneration.status === "string" && imageGeneration.status !== "not_started"));
|
|
2225
|
+
}
|
|
2226
|
+
function validateStrategicScreenManifest(label, value, errors) {
|
|
2227
|
+
const ids = new Set();
|
|
2228
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
2229
|
+
errors.push(`${label} screen_manifest must contain screen-bound product states before image generation`);
|
|
2230
|
+
return ids;
|
|
2231
|
+
}
|
|
2232
|
+
value.forEach((item, index) => {
|
|
2233
|
+
validateRequiredObjectFields(label, `screen_manifest[${index}]`, item, STRATEGIC_SCREEN_MANIFEST_FIELDS, errors);
|
|
2234
|
+
if (!isRecord(item)) {
|
|
2235
|
+
return;
|
|
363
2236
|
}
|
|
2237
|
+
if (nonEmptyString(item.target_screen_id)) {
|
|
2238
|
+
ids.add(String(item.target_screen_id));
|
|
2239
|
+
}
|
|
2240
|
+
if (!hasUsefulValue(item.ai_behavior) && !hasUsefulValue(item.non_ai_rationale)) {
|
|
2241
|
+
errors.push(`${label} screen_manifest[${index}] must include ai_behavior or non_ai_rationale`);
|
|
2242
|
+
}
|
|
2243
|
+
});
|
|
2244
|
+
return ids;
|
|
2245
|
+
}
|
|
2246
|
+
function validateStrategicDirectionScreenPrompts(label, directions, manifestIds, errors) {
|
|
2247
|
+
if (!Array.isArray(directions)) {
|
|
2248
|
+
return;
|
|
364
2249
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
2250
|
+
directions.forEach((direction, directionIndex) => {
|
|
2251
|
+
if (!isRecord(direction)) {
|
|
2252
|
+
return;
|
|
2253
|
+
}
|
|
2254
|
+
const screenPrompts = direction.screen_prompts;
|
|
2255
|
+
if (!Array.isArray(screenPrompts) || screenPrompts.length === 0) {
|
|
2256
|
+
errors.push(`${label} directions[${directionIndex}].screen_prompts must contain screen-bound prompt text before image generation`);
|
|
2257
|
+
return;
|
|
2258
|
+
}
|
|
2259
|
+
screenPrompts.forEach((prompt, promptIndex) => {
|
|
2260
|
+
const fieldLabel = `directions[${directionIndex}].screen_prompts[${promptIndex}]`;
|
|
2261
|
+
validateRequiredObjectFields(label, fieldLabel, prompt, STRATEGIC_SCREEN_PROMPT_FIELDS, errors);
|
|
2262
|
+
if (!isRecord(prompt)) {
|
|
2263
|
+
return;
|
|
2264
|
+
}
|
|
2265
|
+
if (!hasUsefulValue(prompt.prompt) && !hasUsefulValue(prompt.standalone_prompt)) {
|
|
2266
|
+
errors.push(`${label} ${fieldLabel} must include prompt or standalone_prompt`);
|
|
373
2267
|
}
|
|
2268
|
+
if (nonEmptyString(prompt.target_screen_id) && manifestIds.size > 0 && !manifestIds.has(String(prompt.target_screen_id))) {
|
|
2269
|
+
errors.push(`${label} ${fieldLabel}.target_screen_id must exist in screen_manifest`);
|
|
2270
|
+
}
|
|
2271
|
+
});
|
|
2272
|
+
});
|
|
2273
|
+
}
|
|
2274
|
+
function stringReferencesKnownPrompt(value, directionIds, promptIds) {
|
|
2275
|
+
const normalized = normalizePromptRef(value);
|
|
2276
|
+
if (normalized.length === 0) {
|
|
2277
|
+
return false;
|
|
2278
|
+
}
|
|
2279
|
+
for (const id of [...directionIds, ...promptIds]) {
|
|
2280
|
+
const token = normalizePromptRef(id);
|
|
2281
|
+
if (token.length > 0 && normalized.includes(token)) {
|
|
2282
|
+
return true;
|
|
374
2283
|
}
|
|
375
2284
|
}
|
|
376
|
-
|
|
377
|
-
errors.push(`${label} artifact ${index} active_pointer must be a mapping`);
|
|
378
|
-
}
|
|
2285
|
+
return false;
|
|
379
2286
|
}
|
|
380
|
-
function
|
|
381
|
-
|
|
2287
|
+
function normalizePromptRef(value) {
|
|
2288
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
2289
|
+
}
|
|
2290
|
+
function validateStrategicDirections(label, value, countPolicy, errors) {
|
|
2291
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
2292
|
+
errors.push(`${label} directions must contain strategic prompt directions`);
|
|
382
2293
|
return;
|
|
383
2294
|
}
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
errors.push(`${label} must contain disclosure levels 0 through 4`);
|
|
388
|
-
return;
|
|
2295
|
+
const resolvedCount = isRecord(countPolicy) && typeof countPolicy.resolved_count === "number" ? countPolicy.resolved_count : null;
|
|
2296
|
+
if (resolvedCount !== null && value.length < resolvedCount) {
|
|
2297
|
+
errors.push(`${label} directions must include at least direction_count_policy.resolved_count items`);
|
|
389
2298
|
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
errors.push(`${label} level ${index} is not a mapping`);
|
|
2299
|
+
value.forEach((item, index) => {
|
|
2300
|
+
if (!isRecord(item)) {
|
|
2301
|
+
errors.push(`${label} directions[${index}] must be a mapping`);
|
|
394
2302
|
return;
|
|
395
2303
|
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
for (const key of ["name", "default_for_agents", "purpose", "examples"]) {
|
|
400
|
-
if (!(key in level)) {
|
|
401
|
-
errors.push(`${label} level ${index} missing ${key}`);
|
|
2304
|
+
for (const key of STRATEGIC_DIRECTION_FIELDS) {
|
|
2305
|
+
if (!hasUsefulValue(item[key])) {
|
|
2306
|
+
errors.push(`${label} directions[${index}].${key} must be non-empty`);
|
|
402
2307
|
}
|
|
403
2308
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
errors.push(`${label} missing disclosure level ${level}`);
|
|
2309
|
+
const screenPrompts = item.screen_prompts;
|
|
2310
|
+
if (Array.isArray(screenPrompts) && screenPrompts.length < 2) {
|
|
2311
|
+
errors.push(`${label} directions[${index}].screen_prompts must include multi-image prompt text`);
|
|
408
2312
|
}
|
|
409
|
-
|
|
2313
|
+
const distinctness = String(item.distinctness_rationale ?? "").toLowerCase();
|
|
2314
|
+
if (!STRATEGIC_DISTINCTNESS_SIGNALS.some((signal) => distinctness.includes(signal))) {
|
|
2315
|
+
errors.push(`${label} directions[${index}].distinctness_rationale must name a strategic difference, not only visual style`);
|
|
2316
|
+
}
|
|
2317
|
+
});
|
|
410
2318
|
}
|
|
411
|
-
function
|
|
412
|
-
|
|
2319
|
+
function validateBuildRecommendation(label, value, errors) {
|
|
2320
|
+
validateRequiredObjectFields(label, "build_recommendation", value, ["first_direction_id", "why_first", "success_signals", "failure_signals", "next_test_if_it_works"], errors);
|
|
2321
|
+
}
|
|
2322
|
+
function validatePromptTextManifest(label, value, errors) {
|
|
2323
|
+
if (!isRecord(value)) {
|
|
2324
|
+
errors.push(`${label} prompt_text_manifest must be a mapping`);
|
|
413
2325
|
return;
|
|
414
2326
|
}
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
2327
|
+
const status = String(value.status ?? "");
|
|
2328
|
+
if (status && !["draft", "ready_for_image_generation", "generated"].includes(status)) {
|
|
2329
|
+
errors.push(`${label} prompt_text_manifest.status has invalid value ${status}`);
|
|
2330
|
+
}
|
|
2331
|
+
if (status !== "draft" && value.directions_ready !== true) {
|
|
2332
|
+
errors.push(`${label} prompt_text_manifest.directions_ready must be true before image generation`);
|
|
2333
|
+
}
|
|
2334
|
+
if (!Array.isArray(value.prompt_text_refs)) {
|
|
2335
|
+
errors.push(`${label} prompt_text_manifest.prompt_text_refs must be an array`);
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
function validatePostValidate(label, value, countPolicy, promptTextManifest, imageGeneration, directions, errors) {
|
|
2339
|
+
if (!isRecord(value)) {
|
|
2340
|
+
errors.push(`${label} post_validate must be a mapping`);
|
|
419
2341
|
return;
|
|
420
2342
|
}
|
|
421
|
-
for (const key of
|
|
422
|
-
if (!(key in
|
|
423
|
-
errors.push(`${label} missing
|
|
2343
|
+
for (const key of ["status", "trigger", "required_when_direction_count_gte", "skip_when_resolved_count", "threshold_policy", "fingerprint_dimensions", "comparisons", "failures", "outcome_notes", "repair_route"]) {
|
|
2344
|
+
if (!(key in value)) {
|
|
2345
|
+
errors.push(`${label} post_validate missing ${key}`);
|
|
424
2346
|
}
|
|
425
2347
|
}
|
|
426
|
-
|
|
427
|
-
|
|
2348
|
+
const status = String(value.status ?? "");
|
|
2349
|
+
if (status && !["pending", "pass", "fail", "skipped"].includes(status)) {
|
|
2350
|
+
errors.push(`${label} post_validate.status has invalid value ${status}`);
|
|
428
2351
|
}
|
|
429
|
-
|
|
430
|
-
|
|
2352
|
+
if (value.trigger !== "after_prompt_assets_ready") {
|
|
2353
|
+
errors.push(`${label} post_validate.trigger must be after_prompt_assets_ready`);
|
|
431
2354
|
}
|
|
432
|
-
|
|
433
|
-
|
|
2355
|
+
if (value.required_when_direction_count_gte !== 2) {
|
|
2356
|
+
errors.push(`${label} post_validate.required_when_direction_count_gte must be 2`);
|
|
434
2357
|
}
|
|
435
|
-
|
|
436
|
-
|
|
2358
|
+
if (value.skip_when_resolved_count !== 1) {
|
|
2359
|
+
errors.push(`${label} post_validate.skip_when_resolved_count must be 1`);
|
|
437
2360
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
"verification",
|
|
464
|
-
"self_critique",
|
|
465
|
-
"known_limits",
|
|
466
|
-
"result",
|
|
467
|
-
"handoff",
|
|
468
|
-
],
|
|
469
|
-
decision_record: [
|
|
470
|
-
"reviewed_evidence",
|
|
471
|
-
"outcome",
|
|
472
|
-
"rationale",
|
|
473
|
-
"accepted_scope",
|
|
474
|
-
"rejected_scope",
|
|
475
|
-
"revision_scope",
|
|
476
|
-
"next_command",
|
|
477
|
-
"follow_up_questions",
|
|
478
|
-
],
|
|
479
|
-
product_design: [
|
|
480
|
-
"accepted_prototype_evidence",
|
|
481
|
-
"personas",
|
|
482
|
-
"journey_map",
|
|
483
|
-
"user_stories",
|
|
484
|
-
"feature_matrix",
|
|
485
|
-
"kano_classification",
|
|
486
|
-
"behavior_model",
|
|
487
|
-
"ux_states",
|
|
488
|
-
"scope",
|
|
489
|
-
"open_questions",
|
|
490
|
-
"conditional_packets",
|
|
491
|
-
"spec_readiness",
|
|
492
|
-
],
|
|
493
|
-
};
|
|
494
|
-
return requiredByType[artifactType] ?? null;
|
|
495
|
-
}
|
|
496
|
-
function validateValidationTarget(label, data, errors) {
|
|
497
|
-
const featureClassification = data.feature_classification;
|
|
498
|
-
if (isRecord(featureClassification)) {
|
|
499
|
-
for (const key of ["existential", "supporting", "later", "out_of_scope"]) {
|
|
500
|
-
if (!(key in featureClassification)) {
|
|
501
|
-
errors.push(`${label} feature_classification missing ${key}`);
|
|
2361
|
+
if (value.repair_route !== "/ow:vision2prompt") {
|
|
2362
|
+
errors.push(`${label} post_validate.repair_route must be /ow:vision2prompt`);
|
|
2363
|
+
}
|
|
2364
|
+
const thresholdPolicy = value.threshold_policy;
|
|
2365
|
+
if (!isRecord(thresholdPolicy)) {
|
|
2366
|
+
errors.push(`${label} post_validate.threshold_policy must be a mapping`);
|
|
2367
|
+
}
|
|
2368
|
+
else {
|
|
2369
|
+
if (thresholdPolicy.method !== "strategic_fingerprint_similarity") {
|
|
2370
|
+
errors.push(`${label} post_validate.threshold_policy.method must be strategic_fingerprint_similarity`);
|
|
2371
|
+
}
|
|
2372
|
+
if (thresholdPolicy.comparison !== "pairwise") {
|
|
2373
|
+
errors.push(`${label} post_validate.threshold_policy.comparison must be pairwise`);
|
|
2374
|
+
}
|
|
2375
|
+
if (typeof thresholdPolicy.max_pairwise_similarity !== "number" || thresholdPolicy.max_pairwise_similarity <= 0 || thresholdPolicy.max_pairwise_similarity >= 1) {
|
|
2376
|
+
errors.push(`${label} post_validate.threshold_policy.max_pairwise_similarity must be between 0 and 1`);
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
if (!Array.isArray(value.fingerprint_dimensions)) {
|
|
2380
|
+
errors.push(`${label} post_validate.fingerprint_dimensions must be an array`);
|
|
2381
|
+
}
|
|
2382
|
+
else {
|
|
2383
|
+
for (const dimension of STRATEGIC_FINGERPRINT_DIMENSIONS) {
|
|
2384
|
+
if (!value.fingerprint_dimensions.includes(dimension)) {
|
|
2385
|
+
errors.push(`${label} post_validate.fingerprint_dimensions missing ${dimension}`);
|
|
502
2386
|
}
|
|
503
2387
|
}
|
|
504
2388
|
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
return;
|
|
2389
|
+
for (const key of ["comparisons", "failures", "outcome_notes"]) {
|
|
2390
|
+
if (!Array.isArray(value[key])) {
|
|
2391
|
+
errors.push(`${label} post_validate.${key} must be an array`);
|
|
2392
|
+
}
|
|
510
2393
|
}
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
2394
|
+
const resolvedCount = isRecord(countPolicy) && typeof countPolicy.resolved_count === "number" ? countPolicy.resolved_count : null;
|
|
2395
|
+
const promptStatus = isRecord(promptTextManifest) ? String(promptTextManifest.status ?? "") : "";
|
|
2396
|
+
const promptReady = promptStatus === "ready_for_image_generation" || promptStatus === "generated";
|
|
2397
|
+
if (resolvedCount === 1 && status !== "skipped") {
|
|
2398
|
+
errors.push(`${label} post_validate.status must be skipped when direction_count_policy.resolved_count is 1`);
|
|
2399
|
+
}
|
|
2400
|
+
if (promptReady && resolvedCount !== null && resolvedCount >= 2 && !["pass", "fail"].includes(status)) {
|
|
2401
|
+
errors.push(`${label} post_validate.status must be pass or fail before /ow:prompt2proto when resolved_count is 2 or more`);
|
|
2402
|
+
}
|
|
2403
|
+
if (status === "skipped" && resolvedCount !== 1) {
|
|
2404
|
+
errors.push(`${label} post_validate.status can be skipped only when resolved_count is 1`);
|
|
2405
|
+
}
|
|
2406
|
+
if (status === "fail") {
|
|
2407
|
+
const imageStatus = isRecord(imageGeneration) ? String(imageGeneration.status ?? "") : "";
|
|
2408
|
+
if (["queued", "in_progress", "complete"].includes(imageStatus)) {
|
|
2409
|
+
errors.push(`${label} post_validate failed gates must not start image_generation`);
|
|
514
2410
|
}
|
|
515
2411
|
}
|
|
2412
|
+
if (status === "pass" && promptReady && resolvedCount !== null && resolvedCount >= 2) {
|
|
2413
|
+
validateStrategicFingerprintSimilarity(label, directions, value.fingerprint_dimensions, thresholdPolicy, errors);
|
|
2414
|
+
}
|
|
516
2415
|
}
|
|
517
|
-
function
|
|
518
|
-
if (!Array.isArray(
|
|
519
|
-
errors.push(`${label} must contain non-empty todo list`);
|
|
2416
|
+
function validateStrategicFingerprintSimilarity(label, directions, dimensions, thresholdPolicy, errors) {
|
|
2417
|
+
if (!Array.isArray(directions) || !Array.isArray(dimensions) || !isRecord(thresholdPolicy)) {
|
|
520
2418
|
return;
|
|
521
2419
|
}
|
|
522
|
-
const
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
if (seen.has(taskId)) {
|
|
534
|
-
errors.push(`${label} duplicate prototype task_id ${taskId}`);
|
|
2420
|
+
const threshold = typeof thresholdPolicy.max_pairwise_similarity === "number" ? thresholdPolicy.max_pairwise_similarity : null;
|
|
2421
|
+
if (threshold === null) {
|
|
2422
|
+
return;
|
|
2423
|
+
}
|
|
2424
|
+
const records = directions
|
|
2425
|
+
.filter(isRecord)
|
|
2426
|
+
.map((direction, index) => {
|
|
2427
|
+
const directionId = nonEmptyString(direction.direction_id) ? String(direction.direction_id) : `index-${index}`;
|
|
2428
|
+
const fingerprint = isRecord(direction.strategic_fingerprint) ? direction.strategic_fingerprint : null;
|
|
2429
|
+
if (fingerprint === null) {
|
|
2430
|
+
errors.push(`${label} directions[${index}].strategic_fingerprint must be set when post_validate.status is pass`);
|
|
535
2431
|
}
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
2432
|
+
return { directionId, index, fingerprint };
|
|
2433
|
+
});
|
|
2434
|
+
for (let left = 0; left < records.length; left += 1) {
|
|
2435
|
+
for (let right = left + 1; right < records.length; right += 1) {
|
|
2436
|
+
const leftRecord = records[left];
|
|
2437
|
+
const rightRecord = records[right];
|
|
2438
|
+
if (!leftRecord?.fingerprint || !rightRecord?.fingerprint) {
|
|
2439
|
+
continue;
|
|
2440
|
+
}
|
|
2441
|
+
const result = compareStrategicFingerprints(leftRecord.fingerprint, rightRecord.fingerprint, dimensions);
|
|
2442
|
+
if (result.comparedCount === 0) {
|
|
2443
|
+
errors.push(`${label} post_validate cannot compare ${leftRecord.directionId} and ${rightRecord.directionId}: no populated strategic_fingerprint dimensions`);
|
|
2444
|
+
continue;
|
|
2445
|
+
}
|
|
2446
|
+
if (result.score > threshold) {
|
|
2447
|
+
errors.push(`${label} post_validate pair ${leftRecord.directionId}/${rightRecord.directionId} exceeds strategic fingerprint similarity threshold ${threshold}: score ${formatSimilarity(result.score)} shared dimensions ${result.sharedDimensions.join(", ")}`);
|
|
540
2448
|
}
|
|
541
2449
|
}
|
|
542
|
-
}
|
|
2450
|
+
}
|
|
543
2451
|
}
|
|
544
|
-
function
|
|
545
|
-
|
|
546
|
-
|
|
2452
|
+
function compareStrategicFingerprints(left, right, dimensions) {
|
|
2453
|
+
let total = 0;
|
|
2454
|
+
let comparedCount = 0;
|
|
2455
|
+
const sharedDimensions = [];
|
|
2456
|
+
for (const rawDimension of dimensions) {
|
|
2457
|
+
const dimension = String(rawDimension);
|
|
2458
|
+
const leftTokens = fingerprintTokens(left[dimension]);
|
|
2459
|
+
const rightTokens = fingerprintTokens(right[dimension]);
|
|
2460
|
+
if (leftTokens.size === 0 || rightTokens.size === 0) {
|
|
2461
|
+
continue;
|
|
2462
|
+
}
|
|
2463
|
+
const similarity = jaccardSimilarity(leftTokens, rightTokens);
|
|
2464
|
+
total += similarity;
|
|
2465
|
+
comparedCount += 1;
|
|
2466
|
+
if (similarity >= 0.8) {
|
|
2467
|
+
sharedDimensions.push(dimension);
|
|
2468
|
+
}
|
|
547
2469
|
}
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
2470
|
+
return {
|
|
2471
|
+
score: comparedCount === 0 ? 0 : total / comparedCount,
|
|
2472
|
+
comparedCount,
|
|
2473
|
+
sharedDimensions,
|
|
2474
|
+
};
|
|
2475
|
+
}
|
|
2476
|
+
function fingerprintTokens(value) {
|
|
2477
|
+
const raw = Array.isArray(value)
|
|
2478
|
+
? value.join(" ")
|
|
2479
|
+
: isRecord(value)
|
|
2480
|
+
? Object.values(value).join(" ")
|
|
2481
|
+
: String(value ?? "");
|
|
2482
|
+
return new Set(raw.toLowerCase().split(/[^a-z0-9]+/).filter((token) => token.length > 1));
|
|
2483
|
+
}
|
|
2484
|
+
function jaccardSimilarity(left, right) {
|
|
2485
|
+
let intersection = 0;
|
|
2486
|
+
for (const token of left) {
|
|
2487
|
+
if (right.has(token)) {
|
|
2488
|
+
intersection += 1;
|
|
551
2489
|
}
|
|
552
2490
|
}
|
|
553
|
-
|
|
554
|
-
|
|
2491
|
+
const union = left.size + right.size - intersection;
|
|
2492
|
+
return union === 0 ? 0 : intersection / union;
|
|
2493
|
+
}
|
|
2494
|
+
function formatSimilarity(value) {
|
|
2495
|
+
return value.toFixed(2);
|
|
2496
|
+
}
|
|
2497
|
+
function validateImageGeneration(label, value, errors) {
|
|
2498
|
+
if (!isRecord(value)) {
|
|
2499
|
+
errors.push(`${label} image_generation must be a mapping`);
|
|
2500
|
+
return;
|
|
555
2501
|
}
|
|
556
|
-
|
|
557
|
-
if ("
|
|
558
|
-
errors.push(`${label}
|
|
2502
|
+
const status = String(value.status ?? "");
|
|
2503
|
+
if (status && !["not_started", "queued", "in_progress", "complete", "blocked"].includes(status)) {
|
|
2504
|
+
errors.push(`${label} image_generation.status has invalid value ${status}`);
|
|
559
2505
|
}
|
|
560
|
-
if (
|
|
561
|
-
errors.push(`${label}
|
|
2506
|
+
if (!nonEmptyString(value.batch_strategy)) {
|
|
2507
|
+
errors.push(`${label} image_generation.batch_strategy must be non-empty`);
|
|
2508
|
+
}
|
|
2509
|
+
if (!Array.isArray(value.generated_images)) {
|
|
2510
|
+
errors.push(`${label} image_generation.generated_images must be an array`);
|
|
562
2511
|
}
|
|
563
2512
|
else {
|
|
564
|
-
|
|
2513
|
+
value.generated_images.forEach((item, index) => validateGeneratedImageMetadata(label, item, index, errors));
|
|
565
2514
|
}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
2515
|
+
if (!Array.isArray(value.collection_notes)) {
|
|
2516
|
+
errors.push(`${label} image_generation.collection_notes must be an array`);
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
function validateGeneratedImageMetadata(label, value, index, errors) {
|
|
2520
|
+
if (!isRecord(value)) {
|
|
2521
|
+
errors.push(`${label} image_generation.generated_images[${index}] must be a mapping`);
|
|
2522
|
+
return;
|
|
2523
|
+
}
|
|
2524
|
+
for (const key of ["image_id", "direction_id", "prompt_id", "screen_name", "path", "metadata"]) {
|
|
2525
|
+
if (!hasUsefulValue(value[key])) {
|
|
2526
|
+
errors.push(`${label} image_generation.generated_images[${index}].${key} must be non-empty`);
|
|
572
2527
|
}
|
|
573
|
-
validateLocalRef(root, label, "prototype_artifact.path", prototypeArtifact.path, errors);
|
|
574
2528
|
}
|
|
575
|
-
|
|
576
|
-
if (!
|
|
577
|
-
errors.push(`${label}
|
|
2529
|
+
const metadata = value.metadata;
|
|
2530
|
+
if (!isRecord(metadata)) {
|
|
2531
|
+
errors.push(`${label} image_generation.generated_images[${index}].metadata must be a mapping`);
|
|
2532
|
+
return;
|
|
2533
|
+
}
|
|
2534
|
+
for (const key of ["source_prompt_ref", "generated_at", "generator", "generation_status", "review_status"]) {
|
|
2535
|
+
if (!hasUsefulValue(metadata[key])) {
|
|
2536
|
+
errors.push(`${label} image_generation.generated_images[${index}].metadata.${key} must be non-empty`);
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
function hasUsefulValue(value) {
|
|
2541
|
+
if (Array.isArray(value)) {
|
|
2542
|
+
return value.length > 0;
|
|
2543
|
+
}
|
|
2544
|
+
if (isRecord(value)) {
|
|
2545
|
+
return Object.keys(value).length > 0;
|
|
578
2546
|
}
|
|
2547
|
+
return nonEmptyString(value);
|
|
579
2548
|
}
|
|
580
2549
|
function validateVisualConceptPolicy(label, data, errors) {
|
|
581
2550
|
const policy = data.visual_concept_policy;
|
|
@@ -638,20 +2607,23 @@ function validateEvidenceRefs(root, label, data, errors) {
|
|
|
638
2607
|
}
|
|
639
2608
|
}
|
|
640
2609
|
function validateLocalRef(root, label, field, value, errors) {
|
|
641
|
-
if (typeof value !== "string"
|
|
2610
|
+
if (typeof value !== "string") {
|
|
642
2611
|
return;
|
|
643
2612
|
}
|
|
644
|
-
const
|
|
645
|
-
if (
|
|
2613
|
+
const ref = resolveLocalReference(root, value, { exists: existsSyncSafe });
|
|
2614
|
+
if (ref.kind === "empty" || ref.kind === "external") {
|
|
2615
|
+
return;
|
|
2616
|
+
}
|
|
2617
|
+
if (ref.kind === "outside-root") {
|
|
646
2618
|
errors.push(`${label} ${field} references path outside root: ${value}`);
|
|
647
2619
|
return;
|
|
648
2620
|
}
|
|
649
|
-
if (!
|
|
2621
|
+
if (!ref.exists) {
|
|
650
2622
|
errors.push(`${label} ${field} references missing path ${value}`);
|
|
651
2623
|
}
|
|
652
2624
|
}
|
|
653
2625
|
function isExternalRef(value) {
|
|
654
|
-
return
|
|
2626
|
+
return isExternalReference(value);
|
|
655
2627
|
}
|
|
656
2628
|
function nonEmptyString(value) {
|
|
657
2629
|
return typeof value === "string" && value.trim().length > 0;
|
|
@@ -671,6 +2643,52 @@ function validateProductDesign(label, data, errors) {
|
|
|
671
2643
|
}
|
|
672
2644
|
}
|
|
673
2645
|
}
|
|
2646
|
+
function validateProductionSpec(label, data, errors) {
|
|
2647
|
+
for (const key of ["scope", "requirements", "interfaces", "verification", "change_readiness"]) {
|
|
2648
|
+
if (key in data && !isRecord(data[key])) {
|
|
2649
|
+
errors.push(`${label} ${key} must be a mapping`);
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
const changeReadiness = data.change_readiness;
|
|
2653
|
+
if (isRecord(changeReadiness)) {
|
|
2654
|
+
for (const key of ["ready", "next_command"]) {
|
|
2655
|
+
if (!(key in changeReadiness)) {
|
|
2656
|
+
errors.push(`${label} change_readiness missing ${key}`);
|
|
2657
|
+
}
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
function validateProductionChange(label, data, errors) {
|
|
2662
|
+
for (const key of ["goals", "non_goals", "affected_paths", "acceptance", "validation", "risks"]) {
|
|
2663
|
+
if (key in data && !Array.isArray(data[key])) {
|
|
2664
|
+
errors.push(`${label} ${key} must be an array`);
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
const runtimeReadiness = data.runtime_readiness;
|
|
2668
|
+
if (isRecord(runtimeReadiness)) {
|
|
2669
|
+
for (const key of ["ready", "next_command"]) {
|
|
2670
|
+
if (!(key in runtimeReadiness)) {
|
|
2671
|
+
errors.push(`${label} runtime_readiness missing ${key}`);
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2676
|
+
function validateTeamRuntime(label, data, errors) {
|
|
2677
|
+
if (typeof data.execution_mode === "string" && !["single_agent", "agent_team", "reconcile", "qa_fix"].includes(data.execution_mode)) {
|
|
2678
|
+
errors.push(`${label} has invalid execution_mode ${data.execution_mode}`);
|
|
2679
|
+
}
|
|
2680
|
+
for (const key of ["work_queue", "agents", "issues", "checkpoints"]) {
|
|
2681
|
+
if (key in data && !Array.isArray(data[key])) {
|
|
2682
|
+
errors.push(`${label} ${key} must be an array`);
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
if ("verification" in data && !isRecord(data.verification)) {
|
|
2686
|
+
errors.push(`${label} verification must be a mapping`);
|
|
2687
|
+
}
|
|
2688
|
+
if ("handoff" in data && !isRecord(data.handoff)) {
|
|
2689
|
+
errors.push(`${label} handoff must be a mapping`);
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
674
2692
|
function validateActivePointer(root, path, data, errors) {
|
|
675
2693
|
const rule = [
|
|
676
2694
|
pointerRule("VISION_CONTRACT.yaml", "current_session", "sessions", "session_id", "path"),
|
|
@@ -678,6 +2696,9 @@ function validateActivePointer(root, path, data, errors) {
|
|
|
678
2696
|
pointerRule("PROTOTYPE_INDEX.yaml", "current_prototype", "prototypes", "prototype_id", "path"),
|
|
679
2697
|
pointerRule("DECISION_INDEX.yaml", "current_decision", "decisions", "decision_id", "path"),
|
|
680
2698
|
pointerRule("DESIGN_INDEX.yaml", "current_design", "designs", "design_id", "path"),
|
|
2699
|
+
pointerRule("SPEC_INDEX.yaml", "current_spec", "specs", "spec_id", "path"),
|
|
2700
|
+
pointerRule("CHANGE_INDEX.yaml", "current_change", "changes", "change_id", "path"),
|
|
2701
|
+
pointerRule("RUNTIME_INDEX.yaml", "current_run", "runs", "run_id", "path"),
|
|
681
2702
|
].find((item) => basename(path) === item.fileName);
|
|
682
2703
|
if (!rule) {
|
|
683
2704
|
return;
|