@tea-agent/loop-agent 0.42.0-next.1 → 0.42.0-next.3
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/CHANGELOG.md +260 -1
- package/dist/application/dag/generate-task-dag.js +75 -19
- package/dist/application/task-lifecycle/advance.js +24 -5
- package/dist/application/task-lifecycle/observe.js +171 -17
- package/dist/application/task-lifecycle/plan-transitions.js +42 -7
- package/dist/application/task-lifecycle/recommendations.js +13 -2
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +7 -0
- package/dist/cli/program.js +5 -0
- package/dist/commands/client-recovery.js +3 -0
- package/dist/commands/dag-follow-up.js +138 -0
- package/dist/commands/init.js +27 -1
- package/dist/commands/task-advance.js +19 -0
- package/dist/executors/dag-pi-executor.js +3784 -231
- package/dist/executors/pi-executor.js +15 -4
- package/dist/executors/pi-read-budget-policy.js +239 -0
- package/dist/executors/shell-executor.js +942 -187
- package/dist/executors/shell-write-guard.js +7 -0
- package/dist/infrastructure/console/operation-store.js +226 -6
- package/dist/shared/dag-failure-category.js +12 -0
- package/dist/shared/openspec-spec.js +70 -4
- package/dist/shared/operator/capabilities.js +7 -0
- package/dist/task/config-types.js +105 -5
- package/dist/task/contract/adopt.js +4 -0
- package/dist/task/contract/import-revision.js +4 -0
- package/dist/task/contract/project.js +3 -0
- package/dist/task/contract/schema.js +2 -1
- package/dist/task/frontend-project-capability.js +203 -20
- package/dist/task/runtime.js +5 -2
- package/dist/task/source-prepare/build-draft.js +3 -3
- package/dist/task/source-prepare/fragment-inventory.js +15 -9
- package/dist/task/source-prepare/prepare.js +89 -1
- package/dist/task/source-prepare/semantic-intake.js +6 -2
- package/dist/task/source-references.js +22 -1
- package/dist/worker/console/chat/chat-ui-policy.js +4 -2
- package/dist/worker/console/chat/pi-runtime.js +173 -7
- package/dist/worker/console/chat/repo-browser.js +10 -3
- package/dist/worker/console/chat/routes.js +30 -19
- package/dist/worker/console/chat/session-store.js +117 -70
- package/dist/worker/console/chat/shortcuts.js +1 -1
- package/dist/worker/console/chat/turn-process.js +111 -56
- package/dist/worker/console/frontend-human-decision-adapter.js +19 -0
- package/dist/worker/console/frontend-split-operation-adapter.js +20 -0
- package/dist/worker/console/index.js +3 -0
- package/dist/worker/console/operator-actions.js +169 -7
- package/dist/worker/console/server.js +3 -0
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DZZ8m3PO.js → abnfDiagram-N423BO3Z-CeznJ4Iw.js} +1 -1
- package/dist/worker/console/static/assets/{arc-D6PvaVd-.js → arc-B4u9u7SX.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-B_OTOiI8.js → architectureDiagram-T3A2C74G-oghpT81p.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-Bv6rqHBg.js → blockDiagram-VBNYF7ZC-BPJ5Kvcj.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B8eHr0oz.js → c4Diagram-5PPSVZJV-CoIu69pd.js} +1 -1
- package/dist/worker/console/static/assets/channel-DmIXqJ6B.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-DYwR0im2.js → chunk-2GRJ4B5K-DtgiAsSc.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-D2WPGqXt.js → chunk-2Q5K7J3B-YSu_EVSD.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-CQISJ_I7.js → chunk-5RXB4S5H-DaRO-NB6.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-C0o2Du1e.js → chunk-5VM5RSS4-Dh0ZKiGA.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-4f8kr-U7.js → chunk-6Q2QTUOP-DOrvbR-H.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-D_OWvXzX.js → chunk-GF5L2VYU-C3b22LN2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CasdPz5X.js → chunk-JWPE2WC7-CXlqO8l6.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-Cj8lRrla.js → chunk-KBJHAD2P-BblTBrDu.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CUvD1FSd.js → chunk-RYQCIY6F-DswqPALe.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CqLw_eqB.js → chunk-XXDRQBXY-CfTwVOlc.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-MFV5Qxb5.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-MFV5Qxb5.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-ku-WqJPx.js → cose-bilkent-JH36ORCC-bLD8jVFo.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-Bq_PYDhl.js → cynefin-VYW2F7L2-BvoG1fh5.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BiGZV641.js → cynefinDiagram-MW4NZA55-DEPAdJu3.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-C4JRlglF.js → dagre-VZM6K2ZE-DmrNvBLy.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-B0dNeWYG.js → diagram-7IWD3JNH-CdwzIwvs.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Cj35YvYM.js → diagram-B4RE2ZJO-CJ3hj-md.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-uzQoJ2-8.js → diagram-LBJQPF4R-C6YJT2a4.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D1a-Buoz.js → diagram-Q27KOJAE-C_LPfjhX.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-XjRrLRSs.js → diagram-UB23O5K3-xvsSb5Ie.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Da_O24cW.js → ebnfDiagram-BXEA7PRR-Rxp2tKcD.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BLJ8jrYU.js → erDiagram-JOGREHBK-DRyLYmZh.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-B9GrLjM0.js → flowDiagram-UKHOOZJN-TLSU_D2B.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CeJ0TqiK.js → ganttDiagram-PKOTCBZU-DGqeTvHC.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Bm2eNNFX.js → gitGraphDiagram-DS77QQ5N-BZxyH2wh.js} +1 -1
- package/dist/worker/console/static/assets/index-C8V8K9b4.js +451 -0
- package/dist/worker/console/static/assets/index-Cnf_KxTQ.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DB26i3d8.js → infoDiagram-6WML65LV-BsYtzx01.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BOGRyeZT.js → ishikawaDiagram-WSZJBQD7-Qr-ezGv-.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-CZRoBO_6.js → journeyDiagram-NVQOT4AX-DYbB3ibo.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-qlWhJyoB.js → kanban-definition-27J2QSJJ-BHJHewZO.js} +1 -1
- package/dist/worker/console/static/assets/{linear-CfUiDDB3.js → linear-CmPIPmCG.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-BQe6fpqj.js → mermaid.core-F--C1uV0.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-zFzWHw64.js → mindmap-definition-FAOFIHXS-YTXfL1st.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DFGUqjZO.js → pegDiagram-VL7TDLO6-DdDMYXP9.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BB5i0l8Z.js → pieDiagram-7S7Q4E2Y-C3nH4KVd.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-obbVZ_X5.js → quadrantDiagram-CIZ2JOQS-kXyxjG7Y.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BwdaNzc1.js → railroadDiagram-AXF67PYL-KNzbl-7w.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CkTJaugh.js → requirementDiagram-LRYGKXZP-CSwI6dG0.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-BwJ-hIgq.js → sankeyDiagram-W5VNT64P-Bl1AyuXy.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BzSgmm7w.js → sequenceDiagram-SI44F4Z6-2y4EmXQa.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-Bn3oj5Q1.js → sizeCapture-X5ZJPWSS-E8v8YrMg.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-dxhGtL8K.js → stateDiagram-OKZ733FA-eGGlR856.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8e4J3at.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-BLpEInya.js → swimlanes-SLNWSIFB-B7g7xc07.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Do88I1Nb.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-ChJGYcXy.js → timeline-definition-Z64GVDOM-DR3VCHBG.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DK-qjRer.js → vennDiagram-T6HMQDX7-LUvapZIy.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-3qeaWAg-.js → wardleyDiagram-T6FBY63Y-C7GDcaXq.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CootlyP9.js → xychartDiagram-ELKLHX3M-CbNAqoGK.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/active-run-badge.js +17 -0
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +19 -8
- package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +15 -7
- package/dist/worker/console/workspace-context.js +111 -1
- package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
- package/dist/worker/observe/static/dag-node-purpose.js +5 -0
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +99 -4
- package/dist/workflows/dag/backend-test-markdown-workflow.js +13 -1
- package/dist/workflows/dag/backend-test-writer-completeness.js +45 -3
- package/dist/workflows/dag/contract-validator-registrations.js +1 -2
- package/dist/workflows/dag/dag-retry-schema.js +138 -0
- package/dist/workflows/dag/frontend-closeout.js +221 -0
- package/dist/workflows/dag/frontend-design-policy.js +400 -0
- package/dist/workflows/dag/frontend-human-decision.js +182 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +907 -162
- package/dist/workflows/dag/frontend-plan-render.js +2 -1
- package/dist/workflows/dag/frontend-prewrite-gate.js +255 -349
- package/dist/workflows/dag/frontend-provider-capability-matrix.js +159 -0
- package/dist/workflows/dag/frontend-recovery-capsule.js +455 -0
- package/dist/workflows/dag/frontend-recovery-controller.js +202 -0
- package/dist/workflows/dag/frontend-recovery-lineage.js +178 -0
- package/dist/workflows/dag/frontend-recovery-plan.js +17 -10
- package/dist/workflows/dag/frontend-recovery-run.js +33 -20
- package/dist/workflows/dag/frontend-repair.js +1 -432
- package/dist/workflows/dag/frontend-review-context.js +261 -15
- package/dist/workflows/dag/frontend-review-findings.js +270 -0
- package/dist/workflows/dag/frontend-shadow-dual-write.js +914 -0
- package/dist/workflows/dag/frontend-shape-capsule-store.js +191 -0
- package/dist/workflows/dag/frontend-shape-facts.js +419 -0
- package/dist/workflows/dag/frontend-shape.js +435 -0
- package/dist/workflows/dag/frontend-source-fidelity-ledger.js +108 -0
- package/dist/workflows/dag/frontend-split-application-service.js +203 -0
- package/dist/workflows/dag/frontend-split-orchestrator.js +899 -0
- package/dist/workflows/dag/frontend-typed-event-store.js +452 -0
- package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
- package/dist/workflows/dag/frontend-verification-trace.js +249 -24
- package/dist/workflows/dag/frontend-worktree-diff.js +250 -17
- package/dist/workflows/dag/frontend-writer-admission.js +285 -0
- package/dist/workflows/dag/frontend-writer-status.js +256 -0
- package/dist/workflows/dag/init-hybrid.js +777 -525
- package/dist/workflows/dag/interrupt-request.js +7 -0
- package/dist/workflows/dag/node-execution.js +752 -4
- package/dist/workflows/dag/prompt.js +34 -1
- package/dist/workflows/dag/recovery-lease.js +80 -0
- package/dist/workflows/dag/report.js +37 -1
- package/dist/workflows/dag/rerun-feedback.js +256 -1
- package/dist/workflows/dag/rerun-plan.js +17 -6
- package/dist/workflows/dag/rerun-run.js +23 -0
- package/dist/workflows/dag/rerun-task.js +29 -0
- package/dist/workflows/dag/retry-policy.js +219 -104
- package/dist/workflows/dag/runner.js +433 -126
- package/dist/workflows/dag/scheduler.js +114 -16
- package/dist/workflows/dag/types.js +239 -12
- package/dist/workflows/dag/validate.js +28 -12
- package/docs/examples/README.md +5 -0
- package/docs/init-surface.manifest.json +30 -12
- package/docs/skills/vetted-skill-registry.md +4 -2
- package/docs/templates/README.md +2 -0
- package/docs/templates/agent-dag-report.schema.json +8 -2
- package/docs/templates/agent-dag.schema.json +1 -1
- package/docs/templates/backend-test-dag.json +18 -18
- package/docs/templates/frontend-implementation-contract.schema.json +4 -1
- package/docs/templates/frontend-implementation-dag.json +89 -0
- package/docs/templates/spec-registry.schema.json +45 -0
- package/harness.json +1 -1
- package/package.json +1 -1
- package/skills/frontend-bounded-implement/SKILL.md +15 -14
- package/skills/frontend-bounded-implement/references/code-standards.md +19 -0
- package/skills/frontend-contract/SKILL.md +23 -0
- package/skills/frontend-contract/references/contract-protocol.md +34 -0
- package/skills/frontend-design-review/SKILL.md +22 -41
- package/skills/frontend-plan/SKILL.md +26 -0
- package/skills/frontend-plan/references/decision-contract.md +37 -0
- package/skills/frontend-plan/references/design-decisions.md +17 -0
- package/skills/frontend-review/SKILL.md +20 -15
- package/skills/frontend-review/references/review-findings.md +6 -7
- package/skills/frontend-scout/SKILL.md +25 -0
- package/skills/frontend-scout/references/design-evidence.md +16 -0
- package/skills/frontend-scout/references/scout-evidence.md +23 -0
- package/skills/frontend-verification/SKILL.md +1 -1
- package/skills/loop-agent/references/command-reference.md +1 -0
- package/skills/loop-agent/references/hybrid-dag.md +2 -2
- package/dist/worker/console/static/assets/channel-BU5gOilw.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DIzKJGHr.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DIzKJGHr.js +0 -1
- package/dist/worker/console/static/assets/index-H9rFJiGL.css +0 -1
- package/dist/worker/console/static/assets/index-xwu9GxEc.js +0 -451
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Cld2qK9v.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-TIHpiT7w.js +0 -8
- package/skills/frontend-implementation/SKILL.md +0 -52
- package/skills/frontend-implementation/references/code-standards.md +0 -33
- package/skills/frontend-implementation/references/design-spec.md +0 -56
- package/skills/frontend-implementation/references/node-contracts.md +0 -31
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export { buildFrontendShapeTransitionCapsule, computeFrontendShapeSourceDigest, evaluateFrontendShapeEscalation, FRONTEND_SHAPE_NODE_COUNTS, frontendShapeTransitionCapsuleSchema, frontendShapeTransitionReshapeCapsuleSchema, frontendShapeTransitionSplitCapsuleSchema, parseFrontendShapeTransitionCapsule, resolveFrontendTaskShape, } from "./frontend-shape.js";
|
|
3
|
+
/**
|
|
4
|
+
* Deterministic frontend closeout facts + renderer (M7).
|
|
5
|
+
*
|
|
6
|
+
* The functions here are pure (no I/O, no model, no side effects). The closeout
|
|
7
|
+
* renderer reads only committed facts, never model summaries or node prose, so
|
|
8
|
+
* a weak conclusion (failed / not-run / baseline-debt / mock-backed / pending)
|
|
9
|
+
* can never be rewritten into a stronger one (passed / real-integrated).
|
|
10
|
+
*
|
|
11
|
+
* Canonical shape routing and transition capsules live in `frontend-shape.ts`
|
|
12
|
+
* and are re-exported here for callers that also consume closeout contracts.
|
|
13
|
+
*/
|
|
14
|
+
export const FRONTEND_CLOSEOUT_FACTS_SCHEMA_ID = "frontend-closeout-facts-v1";
|
|
15
|
+
/* ------------------------------------------------------------------ *
|
|
16
|
+
* Enums + facts schema *
|
|
17
|
+
* ------------------------------------------------------------------ */
|
|
18
|
+
export const frontendCoverageStatusSchema = z.enum([
|
|
19
|
+
"passed",
|
|
20
|
+
"failed",
|
|
21
|
+
"not-run",
|
|
22
|
+
"blocked",
|
|
23
|
+
"unavailable",
|
|
24
|
+
]);
|
|
25
|
+
export const frontendLintStatusSchema = z.enum([
|
|
26
|
+
"passed",
|
|
27
|
+
"baseline-debt",
|
|
28
|
+
"failed",
|
|
29
|
+
"unavailable",
|
|
30
|
+
]);
|
|
31
|
+
export const frontendIntegrationFactsSchema = z.enum([
|
|
32
|
+
"real-api",
|
|
33
|
+
"mock-backed",
|
|
34
|
+
"local-no-remote",
|
|
35
|
+
"unavailable",
|
|
36
|
+
]);
|
|
37
|
+
export const frontendCloseoutFactsSchema = z
|
|
38
|
+
.object({
|
|
39
|
+
schemaVersion: z.literal(1),
|
|
40
|
+
schemaId: z.literal(FRONTEND_CLOSEOUT_FACTS_SCHEMA_ID),
|
|
41
|
+
coverageMatrix: z.array(z
|
|
42
|
+
.object({
|
|
43
|
+
verificationTargetId: z.string().min(1),
|
|
44
|
+
status: frontendCoverageStatusSchema,
|
|
45
|
+
})
|
|
46
|
+
.strict()),
|
|
47
|
+
lintStatus: frontendLintStatusSchema,
|
|
48
|
+
integrationFacts: frontendIntegrationFactsSchema,
|
|
49
|
+
reviewApproval: z
|
|
50
|
+
.object({
|
|
51
|
+
typedVerdict: z
|
|
52
|
+
.enum(["approve_review", "request_review_changes"])
|
|
53
|
+
.nullable(),
|
|
54
|
+
legacyVerdict: z.enum(["pass", "request-revision"]).nullable(),
|
|
55
|
+
match: z.boolean(),
|
|
56
|
+
})
|
|
57
|
+
.strict(),
|
|
58
|
+
cumulativeDiff: z
|
|
59
|
+
.object({
|
|
60
|
+
cumulativeDigest: z.string().min(1),
|
|
61
|
+
empty: z.boolean(),
|
|
62
|
+
})
|
|
63
|
+
.strict(),
|
|
64
|
+
notRun: z
|
|
65
|
+
.object({
|
|
66
|
+
browser: z.literal("not-run"),
|
|
67
|
+
visual: z.literal("not-run"),
|
|
68
|
+
})
|
|
69
|
+
.strict(),
|
|
70
|
+
pendingRealIntegration: z.boolean(),
|
|
71
|
+
residualRisks: z.array(z.string().min(1)).optional(),
|
|
72
|
+
evidenceGaps: z.array(z.string().min(1)).optional(),
|
|
73
|
+
})
|
|
74
|
+
.strict();
|
|
75
|
+
/* ------------------------------------------------------------------ *
|
|
76
|
+
* FrontendTestHandoff (A+B AC-010) *
|
|
77
|
+
* ------------------------------------------------------------------ */
|
|
78
|
+
export const FRONTEND_TEST_HANDOFF_SCHEMA_ID = "frontend-test-handoff-v1";
|
|
79
|
+
/**
|
|
80
|
+
* A+B (AC-010): deterministic frontend-test handoff. Bound to the task id,
|
|
81
|
+
* source digest, and candidate hash; browser/visual/runtime-a11y are always
|
|
82
|
+
* `not-run` and the handoff is always `deferred-to-frontend-test`, so a weak
|
|
83
|
+
* closeout can never block implementation and can never be rewritten as
|
|
84
|
+
* passed.
|
|
85
|
+
*/
|
|
86
|
+
export const frontendTestHandoffSchema = z
|
|
87
|
+
.object({
|
|
88
|
+
schemaVersion: z.literal(1),
|
|
89
|
+
schemaId: z.literal(FRONTEND_TEST_HANDOFF_SCHEMA_ID),
|
|
90
|
+
taskId: z.string().min(1),
|
|
91
|
+
sourceDigest: z.string().regex(/^[a-f0-9]{64}$/),
|
|
92
|
+
candidateHash: z.string().regex(/^[a-f0-9]{64}$/),
|
|
93
|
+
executionStatus: z.literal("not-run"),
|
|
94
|
+
handoffStatus: z.literal("deferred-to-frontend-test"),
|
|
95
|
+
nonBlocking: z.literal(true),
|
|
96
|
+
})
|
|
97
|
+
.strict();
|
|
98
|
+
export function buildFrontendTestHandoff(input) {
|
|
99
|
+
return frontendTestHandoffSchema.parse({
|
|
100
|
+
schemaVersion: 1,
|
|
101
|
+
schemaId: FRONTEND_TEST_HANDOFF_SCHEMA_ID,
|
|
102
|
+
taskId: input.taskId,
|
|
103
|
+
sourceDigest: input.sourceDigest,
|
|
104
|
+
candidateHash: input.candidateHash,
|
|
105
|
+
executionStatus: "not-run",
|
|
106
|
+
handoffStatus: "deferred-to-frontend-test",
|
|
107
|
+
nonBlocking: true,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Deterministic, fail-closed mapping from structured evidence to integration
|
|
112
|
+
* facts. Never reads model text. `real-api` is the only value that may report
|
|
113
|
+
* real integration, and it requires both real passed evidence and satisfied
|
|
114
|
+
* required verification evidence. Anything else downgrades to pending.
|
|
115
|
+
*/
|
|
116
|
+
export function deriveFrontendIntegrationFacts(input) {
|
|
117
|
+
const requiredEvidenceSatisfied = input.verificationTargets.length > 0 &&
|
|
118
|
+
input.verificationTargets.every((target) => target.status === "ok");
|
|
119
|
+
if (input.realApiEvidence.present &&
|
|
120
|
+
input.realApiEvidence.passed &&
|
|
121
|
+
requiredEvidenceSatisfied) {
|
|
122
|
+
return "real-api";
|
|
123
|
+
}
|
|
124
|
+
// Required evidence not satisfied → never report real-integrated or
|
|
125
|
+
// mock/locally-validated; evidence is unavailable.
|
|
126
|
+
if (!requiredEvidenceSatisfied) {
|
|
127
|
+
return "unavailable";
|
|
128
|
+
}
|
|
129
|
+
if (input.mockStrategy !== "not-needed" && input.mockEvidenceBound) {
|
|
130
|
+
return "mock-backed";
|
|
131
|
+
}
|
|
132
|
+
if (input.mockStrategy === "not-needed" && !input.realApiEvidence.present) {
|
|
133
|
+
return "local-no-remote";
|
|
134
|
+
}
|
|
135
|
+
return "unavailable";
|
|
136
|
+
}
|
|
137
|
+
/* ------------------------------------------------------------------ *
|
|
138
|
+
* Deterministic closeout renderer *
|
|
139
|
+
* ------------------------------------------------------------------ */
|
|
140
|
+
function bulletList(items) {
|
|
141
|
+
if (!items || items.length === 0)
|
|
142
|
+
return "";
|
|
143
|
+
return items.map((item) => `- ${item}`).join("\n");
|
|
144
|
+
}
|
|
145
|
+
function coverageLines(facts) {
|
|
146
|
+
const lines = facts.coverageMatrix.map((entry) => `- ${entry.verificationTargetId}: ${entry.status}`);
|
|
147
|
+
return lines.length > 0 ? lines.join("\n") : "_(none)_";
|
|
148
|
+
}
|
|
149
|
+
function frontendStatus(facts) {
|
|
150
|
+
switch (facts.integrationFacts) {
|
|
151
|
+
case "real-api":
|
|
152
|
+
return "Frontend status: real-integrated";
|
|
153
|
+
case "mock-backed":
|
|
154
|
+
return "Frontend status: mock-validated";
|
|
155
|
+
case "local-no-remote":
|
|
156
|
+
return "Frontend status: locally-validated";
|
|
157
|
+
case "unavailable":
|
|
158
|
+
return "Frontend status: unverified";
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function integrationStatus(facts) {
|
|
162
|
+
return facts.integrationFacts === "real-api"
|
|
163
|
+
? "Real integration: validated"
|
|
164
|
+
: `Real integration: pending (${facts.integrationFacts})`;
|
|
165
|
+
}
|
|
166
|
+
function reviewApprovalLines(facts) {
|
|
167
|
+
const { typedVerdict, legacyVerdict, match } = facts.reviewApproval;
|
|
168
|
+
return [
|
|
169
|
+
`typed: ${typedVerdict ?? "none"}`,
|
|
170
|
+
`legacy: ${legacyVerdict ?? "none"}`,
|
|
171
|
+
`match: ${match}`,
|
|
172
|
+
].join("\n");
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Render a deterministic closeout Markdown document from committed facts only.
|
|
176
|
+
* The same facts always render byte-identical output; weak statuses are
|
|
177
|
+
* emitted verbatim and never promoted to `passed` / real-integrated.
|
|
178
|
+
*/
|
|
179
|
+
export function renderFrontendCloseout(facts) {
|
|
180
|
+
const lintLine = facts.lintStatus === "baseline-debt"
|
|
181
|
+
? `${facts.lintStatus} (explicit debt; not passed)`
|
|
182
|
+
: facts.lintStatus;
|
|
183
|
+
return [
|
|
184
|
+
"# Frontend Closeout",
|
|
185
|
+
"",
|
|
186
|
+
"## Coverage Matrix",
|
|
187
|
+
coverageLines(facts),
|
|
188
|
+
"",
|
|
189
|
+
"## Lint Status",
|
|
190
|
+
lintLine,
|
|
191
|
+
"",
|
|
192
|
+
"## Integration Facts",
|
|
193
|
+
facts.integrationFacts,
|
|
194
|
+
"",
|
|
195
|
+
"## Frontend Status",
|
|
196
|
+
frontendStatus(facts),
|
|
197
|
+
"",
|
|
198
|
+
"## Real Integration Status",
|
|
199
|
+
integrationStatus(facts),
|
|
200
|
+
"",
|
|
201
|
+
"## Real Integration Pending",
|
|
202
|
+
facts.pendingRealIntegration ? "yes" : "no",
|
|
203
|
+
"",
|
|
204
|
+
"## Review Approval",
|
|
205
|
+
reviewApprovalLines(facts),
|
|
206
|
+
"",
|
|
207
|
+
"## Cumulative Diff",
|
|
208
|
+
`digest: ${facts.cumulativeDiff.cumulativeDigest}`,
|
|
209
|
+
`empty: ${facts.cumulativeDiff.empty}`,
|
|
210
|
+
"",
|
|
211
|
+
"## Browser / Visual",
|
|
212
|
+
`Browser: ${facts.notRun.browser}`,
|
|
213
|
+
`Visual: ${facts.notRun.visual}`,
|
|
214
|
+
"",
|
|
215
|
+
"## Residual Risks",
|
|
216
|
+
bulletList(facts.residualRisks) || "_(none)_",
|
|
217
|
+
"",
|
|
218
|
+
"## Evidence Gaps",
|
|
219
|
+
bulletList(facts.evidenceGaps) || "_(none)_",
|
|
220
|
+
].join("\n") + "\n";
|
|
221
|
+
}
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import { deterministicSha256, } from "./frontend-implementation-contract.js";
|
|
2
|
+
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
3
|
+
const SHA256_HEX = /^[a-f0-9]{64}$/;
|
|
4
|
+
const NPM_PACKAGE_NAME = /^(?:@[a-zA-Z0-9_.-]+\/)?[a-zA-Z0-9][a-zA-Z0-9_.-]*$/;
|
|
5
|
+
/** Max concrete implementation files a single writeSet may carry before the
|
|
6
|
+
* plan is asked to split. Kept at 15: beyond this, a single implement node's
|
|
7
|
+
* read/edit loop inflates provider context toward overflow. */
|
|
8
|
+
const MAX_WRITE_SET_FILES = 15;
|
|
9
|
+
function normalizeRelativePath(value) {
|
|
10
|
+
return value.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
11
|
+
}
|
|
12
|
+
function sortFindings(findings) {
|
|
13
|
+
return [...findings].sort((a, b) => {
|
|
14
|
+
if (a.code !== b.code)
|
|
15
|
+
return a.code < b.code ? -1 : 1;
|
|
16
|
+
if (a.message !== b.message)
|
|
17
|
+
return a.message < b.message ? -1 : 1;
|
|
18
|
+
const pathA = a.path ?? "";
|
|
19
|
+
const pathB = b.path ?? "";
|
|
20
|
+
if (pathA !== pathB)
|
|
21
|
+
return pathA < pathB ? -1 : 1;
|
|
22
|
+
return 0;
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
/** 1. UI-visible requirements must map to a design decision. Every applicable
|
|
26
|
+
* UI state and every interaction must be covered by an exact `purpose` match in
|
|
27
|
+
* `uiComponentChoices`; a present `stylingStrategy` additionally covers the
|
|
28
|
+
* visual UI-state dimension (it cannot cover behavioural interactions). */
|
|
29
|
+
export function checkUiDesignCoverage(contract) {
|
|
30
|
+
const findings = [];
|
|
31
|
+
const purposes = new Set((contract.uiComponentChoices ?? []).map((choice) => choice.purpose));
|
|
32
|
+
const hasStyling = Boolean(contract.stylingStrategy?.trim());
|
|
33
|
+
// Reuse of an existing component/pattern covers behavioural interactions
|
|
34
|
+
// without forcing one uiComponentChoices row per interaction: the model
|
|
35
|
+
// should not enumerate every click/hover on a reused Button. Only
|
|
36
|
+
// interactions with no reuse and no explicit purpose are flagged.
|
|
37
|
+
const hasReuseChoice = (contract.uiComponentChoices ?? []).some((choice) => choice.decision === "reuse-existing");
|
|
38
|
+
for (const state of contract.uiStates) {
|
|
39
|
+
if (!state.applicable)
|
|
40
|
+
continue;
|
|
41
|
+
const covered = purposes.has(state.name) || hasStyling;
|
|
42
|
+
if (!covered) {
|
|
43
|
+
findings.push({
|
|
44
|
+
code: "ui-design-coverage-missing",
|
|
45
|
+
message: `applicable UI state "${state.name}" has no matching uiComponentChoices.purpose or stylingStrategy`,
|
|
46
|
+
path: state.name,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
for (const interaction of contract.interactions) {
|
|
51
|
+
if (purposes.has(interaction.name) || hasReuseChoice)
|
|
52
|
+
continue;
|
|
53
|
+
findings.push({
|
|
54
|
+
code: "ui-design-coverage-missing",
|
|
55
|
+
message: `interaction "${interaction.name}" has no matching uiComponentChoices.purpose and no reuse-existing choice covering it`,
|
|
56
|
+
path: interaction.name,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return findings;
|
|
60
|
+
}
|
|
61
|
+
/** 2. Source freshness: every injected entry must be fresh with a valid hash;
|
|
62
|
+
* every design-evidence path must appear in the freshness set with a single,
|
|
63
|
+
* self-consistent sha256; any declared conflict is blocking. */
|
|
64
|
+
export function checkSourceFreshness(contract, sourceFreshness) {
|
|
65
|
+
const findings = [];
|
|
66
|
+
const hashesByPath = new Map();
|
|
67
|
+
for (const entry of sourceFreshness) {
|
|
68
|
+
if (!entry.fresh) {
|
|
69
|
+
findings.push({
|
|
70
|
+
code: "source-stale",
|
|
71
|
+
message: `source is stale: ${entry.path}`,
|
|
72
|
+
path: entry.path,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
if (!SHA256_HEX.test(entry.sha256)) {
|
|
76
|
+
findings.push({
|
|
77
|
+
code: "source-hash-invalid",
|
|
78
|
+
message: `source sha256 is not a valid 64-hex digest: ${entry.path}`,
|
|
79
|
+
path: entry.path,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
const hashes = hashesByPath.get(entry.path) ?? [];
|
|
83
|
+
hashes.push(entry.sha256);
|
|
84
|
+
hashesByPath.set(entry.path, hashes);
|
|
85
|
+
}
|
|
86
|
+
for (const [path, hashes] of hashesByPath) {
|
|
87
|
+
if (new Set(hashes).size > 1) {
|
|
88
|
+
findings.push({
|
|
89
|
+
code: "source-hash-conflict",
|
|
90
|
+
message: `source freshness entries disagree on sha256: ${path}`,
|
|
91
|
+
path,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
for (const evidencePath of contract.designEvidence.paths) {
|
|
96
|
+
if (!hashesByPath.has(evidencePath)) {
|
|
97
|
+
findings.push({
|
|
98
|
+
code: "design-evidence-path-missing-freshness",
|
|
99
|
+
message: `design evidence path is absent from the freshness set: ${evidencePath}`,
|
|
100
|
+
path: evidencePath,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// `designEvidence.conflicts` is the plan's honest disclosure of design
|
|
105
|
+
// deviations (e.g. task text demands a test form the frozen constraints
|
|
106
|
+
// make infeasible). Whether a disclosed conflict is acceptable is decided
|
|
107
|
+
// by the design reviewer via `approve_design` / `request_design_changes`,
|
|
108
|
+
// which `checkDesignApproval` enforces separately. Treating any conflict
|
|
109
|
+
// as blocking here would strand every honest plan that reports one — the
|
|
110
|
+
// model cannot "fix" a conflict it correctly identified. Conflicts must be
|
|
111
|
+
// disclosed (schema-required array), never silently dropped, but disclosure
|
|
112
|
+
// itself is not a policy violation.
|
|
113
|
+
return findings;
|
|
114
|
+
}
|
|
115
|
+
/** 3. Mock/API strategy must be allowed; non-`not-needed` strategies require
|
|
116
|
+
* at least one endpoint and every endpoint must carry fixture + consumer. */
|
|
117
|
+
export function checkMockStrategy(contract, allowedMockStrategies) {
|
|
118
|
+
const findings = [];
|
|
119
|
+
if (!allowedMockStrategies.includes(contract.mockApi.strategy)) {
|
|
120
|
+
findings.push({
|
|
121
|
+
code: "mock-strategy-not-allowed",
|
|
122
|
+
message: `mockApi.strategy "${contract.mockApi.strategy}" is not in allowedMockStrategies`,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
if (contract.mockApi.strategy !== "not-needed") {
|
|
126
|
+
if (contract.mockApi.endpoints.length === 0) {
|
|
127
|
+
findings.push({
|
|
128
|
+
code: "mock-endpoints-empty",
|
|
129
|
+
message: `mockApi.strategy "${contract.mockApi.strategy}" requires at least one endpoint`,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
contract.mockApi.endpoints.forEach((endpoint, index) => {
|
|
133
|
+
if (!endpoint.fixture) {
|
|
134
|
+
findings.push({
|
|
135
|
+
code: "mock-endpoint-missing-fixture",
|
|
136
|
+
message: `mock endpoint ${index} is missing a fixture path`,
|
|
137
|
+
path: endpoint.path,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
if (!endpoint.consumer) {
|
|
141
|
+
findings.push({
|
|
142
|
+
code: "mock-endpoint-missing-consumer",
|
|
143
|
+
message: `mock endpoint ${index} is missing a consumer path`,
|
|
144
|
+
path: endpoint.path,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
return findings;
|
|
150
|
+
}
|
|
151
|
+
/** 4. Component choice must be precisely evidence-referenced: `specified`
|
|
152
|
+
* requires a specReference whose normalized path is a candidate and a non-empty
|
|
153
|
+
* section; `new` requires a task-source reference with a non-empty section;
|
|
154
|
+
* `reuse-existing` may omit it. The
|
|
155
|
+
* candidate set is the full generation-frozen openspec candidate list (the
|
|
156
|
+
* shell passes openspecCandidatePaths with componentSpecCandidatePaths as the
|
|
157
|
+
* fallback), so directory-organized component libraries are never rejected.
|
|
158
|
+
* A component specReference must still not point into clearly non-component
|
|
159
|
+
* spec areas — templates and the ai_workspace governance subtree cannot be the
|
|
160
|
+
* authoritative component definition. */
|
|
161
|
+
export function checkComponentChoice(contract, componentSpecCandidatePaths) {
|
|
162
|
+
const findings = [];
|
|
163
|
+
const candidates = new Set(componentSpecCandidatePaths.map(normalizeRelativePath));
|
|
164
|
+
const isNonComponentSpecArea = (path) => {
|
|
165
|
+
const normalized = normalizeRelativePath(path);
|
|
166
|
+
return (normalized.startsWith("openspec/project-specs/templates/") ||
|
|
167
|
+
normalized.startsWith("ai_workspace/") ||
|
|
168
|
+
normalized === "openspec/project-specs/templates");
|
|
169
|
+
};
|
|
170
|
+
(contract.uiComponentChoices ?? []).forEach((choice, index) => {
|
|
171
|
+
if (choice.decision === "specified") {
|
|
172
|
+
if (!choice.specReference) {
|
|
173
|
+
findings.push({
|
|
174
|
+
code: "component-spec-reference-missing",
|
|
175
|
+
message: `specified component choice #${index} is missing specReference`,
|
|
176
|
+
path: choice.purpose,
|
|
177
|
+
});
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (!choice.specReference.section.trim()) {
|
|
181
|
+
findings.push({
|
|
182
|
+
code: "component-spec-reference-missing",
|
|
183
|
+
message: `specified component choice #${index} has an empty section`,
|
|
184
|
+
path: choice.purpose,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
const normalized = normalizeRelativePath(choice.specReference.path);
|
|
188
|
+
if (!candidates.has(normalized)) {
|
|
189
|
+
findings.push({
|
|
190
|
+
code: "component-spec-reference-path-outside-candidates",
|
|
191
|
+
message: `specified component choice #${index} specReference.path "${choice.specReference.path}" is not a candidate spec path`,
|
|
192
|
+
path: choice.specReference.path,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
else if (isNonComponentSpecArea(normalized)) {
|
|
196
|
+
findings.push({
|
|
197
|
+
code: "component-spec-reference-path-outside-candidates",
|
|
198
|
+
message: `specified component choice #${index} specReference.path "${choice.specReference.path}" is a candidate but lives in a non-component spec area (templates or ai_workspace governance cannot be the authoritative component definition)`,
|
|
199
|
+
path: choice.specReference.path,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
else if (choice.decision === "new") {
|
|
204
|
+
if (!choice.specReference || !choice.specReference.path) {
|
|
205
|
+
findings.push({
|
|
206
|
+
code: "component-new-source-reference-missing",
|
|
207
|
+
message: `new component choice #${index} is missing its task-source specReference`,
|
|
208
|
+
path: choice.purpose,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
else if (!choice.specReference.section.trim()) {
|
|
212
|
+
findings.push({
|
|
213
|
+
code: "component-new-source-reference-missing",
|
|
214
|
+
message: `new component choice #${index} has an empty task-source reference section`,
|
|
215
|
+
path: choice.purpose,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
return findings;
|
|
221
|
+
}
|
|
222
|
+
/** 5. Every implementation target and every non-static verification target
|
|
223
|
+
* must fall inside the declared writeSet patterns. */
|
|
224
|
+
export function checkTargetPaths(contract, writeSetPatterns) {
|
|
225
|
+
const findings = [];
|
|
226
|
+
// No frozen writeSet patterns (legacy / unconfigured run) → nothing to
|
|
227
|
+
// check; an empty pattern set must not report every file as out of scope.
|
|
228
|
+
if (writeSetPatterns.length === 0)
|
|
229
|
+
return findings;
|
|
230
|
+
const matches = (file) => writeSetPatterns.some((pattern) => pathMatchesPattern(file, pattern));
|
|
231
|
+
// A writeSet this large forces a single implement node to read/edit many
|
|
232
|
+
// files, inflating the provider context until it overflows (400
|
|
233
|
+
// request-too-large). Flag it so the plan is asked to split (the contract
|
|
234
|
+
// node already has record_split_proposal) instead of letting the writer
|
|
235
|
+
// blow up mid-run.
|
|
236
|
+
if (contract.targets.files.length > MAX_WRITE_SET_FILES) {
|
|
237
|
+
findings.push({
|
|
238
|
+
code: "write-set-too-large",
|
|
239
|
+
message: `implementation writeSet has ${contract.targets.files.length} files (limit ${MAX_WRITE_SET_FILES}); split the task (record_split_proposal) so no single implement node exceeds the bound`,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
for (const file of contract.targets.files) {
|
|
243
|
+
if (!matches(file)) {
|
|
244
|
+
findings.push({
|
|
245
|
+
code: "target-path-outside-write-set",
|
|
246
|
+
message: `implementation target is outside the writeSet patterns: ${file}`,
|
|
247
|
+
path: file,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
for (const target of contract.verificationTargets) {
|
|
252
|
+
if (target.type === "static")
|
|
253
|
+
continue;
|
|
254
|
+
if (!matches(target.file)) {
|
|
255
|
+
findings.push({
|
|
256
|
+
code: "target-path-outside-write-set",
|
|
257
|
+
message: `verification target "${target.id}" file is outside the writeSet patterns: ${target.file}`,
|
|
258
|
+
path: target.file,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return findings;
|
|
263
|
+
}
|
|
264
|
+
/** 6. Any declared dependency identifier must be authorized. A declarative
|
|
265
|
+
* "no new dependencies" policy declares nothing; package-name-like tokens are
|
|
266
|
+
* matched exactly against `allowedDependencies`.
|
|
267
|
+
*
|
|
268
|
+
* The dependency policy is a natural-language statement, not a machine list.
|
|
269
|
+
* Three rules keep this check deterministic and aligned with the plan facts:
|
|
270
|
+
*
|
|
271
|
+
* 1. An empty allowedDependencies set skips the check entirely — with no
|
|
272
|
+
* allowlist there is no ground truth to reject against. The DAG generator
|
|
273
|
+
* normally fills this set from the project package.json, so an empty set
|
|
274
|
+
* means the manifest could not be read (no authorization is configured).
|
|
275
|
+
* 2. A declarative "no new dependencies" intent (零新增/不新增/无新增/不引入/
|
|
276
|
+
* no new…) satisfies the check: the model explicitly claims it adds
|
|
277
|
+
* nothing, so prose mentioning the existing toolchain ("DOM",
|
|
278
|
+
* "devDependencies", already-declared deps) must not be misread as new
|
|
279
|
+
* dependency claims.
|
|
280
|
+
* 3. Otherwise, every package-name-like token must match the allowlist —
|
|
281
|
+
* an unauthorized new dependency (a library absent from the project
|
|
282
|
+
* manifest) still fails closed.
|
|
283
|
+
*/
|
|
284
|
+
export function checkDependencies(contract, allowedDependencies) {
|
|
285
|
+
const findings = [];
|
|
286
|
+
const policy = contract.dependencyPolicy?.trim();
|
|
287
|
+
if (!policy)
|
|
288
|
+
return findings;
|
|
289
|
+
// No explicit allowlist: nothing can be judged unauthorized.
|
|
290
|
+
if (allowedDependencies.length === 0)
|
|
291
|
+
return findings;
|
|
292
|
+
// Declarative no-new-dependencies intent: trust the claim, do not parse
|
|
293
|
+
// prose tokens (they mention the already-declared toolchain). The allowlist
|
|
294
|
+
// below already passes any already-declared dependency by exact match, so
|
|
295
|
+
// this exemption is deliberately narrow: only explicit negatives escape the
|
|
296
|
+
// token scan, never neutral descriptions that could conceal a new dep.
|
|
297
|
+
if (/零新增|不新增|无新增|不引入|不添加|不依赖|no\s*new\s*deps?|no\s*new\s*dependencies/i.test(policy)) {
|
|
298
|
+
return findings;
|
|
299
|
+
}
|
|
300
|
+
const allowed = new Set(allowedDependencies);
|
|
301
|
+
const tokens = policy
|
|
302
|
+
.split(/[,;\s]+/)
|
|
303
|
+
.filter((token) => token.length > 0);
|
|
304
|
+
for (const token of tokens) {
|
|
305
|
+
if (!NPM_PACKAGE_NAME.test(token))
|
|
306
|
+
continue;
|
|
307
|
+
// Already-declared dependencies are authorized by definition.
|
|
308
|
+
if (allowed.has(token))
|
|
309
|
+
continue;
|
|
310
|
+
// Versioned/scoped variants: pkg@1.2.3 or @scope/pkg@1 → match bare name.
|
|
311
|
+
const bareName = token.startsWith("@")
|
|
312
|
+
? token.split("@").slice(0, 2).join("@")
|
|
313
|
+
: token.split("@")[0];
|
|
314
|
+
if (allowed.has(bareName))
|
|
315
|
+
continue;
|
|
316
|
+
findings.push({
|
|
317
|
+
code: "dependency-not-allowed",
|
|
318
|
+
message: `declared dependency "${token}" is not in allowedDependencies`,
|
|
319
|
+
path: token,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
return findings;
|
|
323
|
+
}
|
|
324
|
+
/** 7. Every declared UI state must carry a complete attribution:
|
|
325
|
+
* applicable → expectedBehavior, non-applicable → notApplicableReason.
|
|
326
|
+
*
|
|
327
|
+
* The check validates the states the contract DECLARES, not a hardcoded list
|
|
328
|
+
* of canonical names: the plan prompts say "Do not invent UI states", so a
|
|
329
|
+
* contract that reasonably covers loading/empty/error/permission without a
|
|
330
|
+
* `disabled` entry must not be blocked for missing a state the model had no
|
|
331
|
+
* reason to declare — the model cannot fix that without inventing one, which
|
|
332
|
+
* the ladder then retries to exhaustion.
|
|
333
|
+
*
|
|
334
|
+
* Pure-logic contracts (no user-visible UI states AND no interactions) are
|
|
335
|
+
* exempt entirely: the schema and prompts explicitly allow `uiStates: []`.
|
|
336
|
+
* Once any state or interaction is declared, every declared state must carry
|
|
337
|
+
* complete attribution.
|
|
338
|
+
*/
|
|
339
|
+
export function checkUiStateAttribution(contract) {
|
|
340
|
+
if (contract.uiStates.length === 0 && contract.interactions.length === 0)
|
|
341
|
+
return [];
|
|
342
|
+
const findings = [];
|
|
343
|
+
for (const state of contract.uiStates) {
|
|
344
|
+
if (state.applicable && !state.expectedBehavior) {
|
|
345
|
+
findings.push({
|
|
346
|
+
code: "ui-state-attribution-incomplete",
|
|
347
|
+
message: `applicable UI state "${state.name}" is missing expectedBehavior`,
|
|
348
|
+
path: state.name,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
if (!state.applicable && !state.notApplicableReason) {
|
|
352
|
+
findings.push({
|
|
353
|
+
code: "ui-state-attribution-incomplete",
|
|
354
|
+
message: `non-applicable UI state "${state.name}" is missing notApplicableReason`,
|
|
355
|
+
path: state.name,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return findings;
|
|
360
|
+
}
|
|
361
|
+
/** Design approval: a `request-revision` verdict from the design reviewer is
|
|
362
|
+
* blocking. This is injected as structured input, never parsed from model text. */
|
|
363
|
+
export function checkDesignApproval(designApproval) {
|
|
364
|
+
if (designApproval.verdict === "request-revision") {
|
|
365
|
+
return [
|
|
366
|
+
{
|
|
367
|
+
code: "design-approval-request-revision",
|
|
368
|
+
message: "design approval verdict is request-revision",
|
|
369
|
+
},
|
|
370
|
+
];
|
|
371
|
+
}
|
|
372
|
+
return [];
|
|
373
|
+
}
|
|
374
|
+
/** Aggregate the deterministic design-policy checks into a single
|
|
375
|
+
* deterministic result. Any finding blocks; an empty finding list approves. */
|
|
376
|
+
export function evaluateFrontendDesignPolicy(input) {
|
|
377
|
+
const findings = sortFindings([
|
|
378
|
+
...(input.designApproval ? checkDesignApproval(input.designApproval) : []),
|
|
379
|
+
...checkUiDesignCoverage(input.contract),
|
|
380
|
+
...checkSourceFreshness(input.contract, input.sourceFreshness),
|
|
381
|
+
...checkMockStrategy(input.contract, input.allowedMockStrategies),
|
|
382
|
+
...checkComponentChoice(input.contract, input.componentSpecCandidatePaths),
|
|
383
|
+
...checkTargetPaths(input.contract, input.writeSetPatterns),
|
|
384
|
+
...checkDependencies(input.contract, input.allowedDependencies),
|
|
385
|
+
...checkUiStateAttribution(input.contract),
|
|
386
|
+
]);
|
|
387
|
+
const classification = findings.length > 0 ? "blocked" : "approved";
|
|
388
|
+
const result = {
|
|
389
|
+
schemaId: "frontend-design-policy-v1",
|
|
390
|
+
classification,
|
|
391
|
+
findings,
|
|
392
|
+
digest: "",
|
|
393
|
+
};
|
|
394
|
+
result.digest = deterministicSha256({
|
|
395
|
+
schemaId: result.schemaId,
|
|
396
|
+
classification: result.classification,
|
|
397
|
+
findings: result.findings,
|
|
398
|
+
});
|
|
399
|
+
return result;
|
|
400
|
+
}
|