@tea-agent/loop-agent 0.42.0-next.1 → 0.42.0-next.11
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/AGENTS.md +1 -1
- package/CHANGELOG.md +321 -1
- package/dist/application/dag/generate-task-dag.js +75 -19
- package/dist/application/dag/run-dag.js +41 -0
- 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 +6 -1
- package/dist/commands/client-recovery.js +3 -0
- package/dist/commands/dag-follow-up.js +138 -0
- package/dist/commands/dag-rerun-task.js +2 -0
- package/dist/commands/init.js +27 -1
- package/dist/commands/task-advance.js +19 -0
- package/dist/executors/dag-pi-executor.js +4010 -78
- package/dist/executors/pi-executor.js +15 -4
- package/dist/executors/pi-extension-resolver.js +14 -2
- package/dist/executors/pi-read-budget-policy.js +239 -0
- package/dist/executors/shell-executor.js +978 -188
- 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 +64 -25
- package/dist/task/source-prepare/prepare.js +126 -21
- 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/model-resolver.js +7 -4
- package/dist/worker/console/chat/pi-runtime.js +258 -11
- package/dist/worker/console/chat/repo-browser.js +10 -3
- package/dist/worker/console/chat/routes.js +165 -71
- package/dist/worker/console/chat/sdd-data-alignment.js +222 -0
- package/dist/worker/console/chat/session-catalog.js +2 -0
- package/dist/worker/console/chat/session-store.js +159 -70
- package/dist/worker/console/chat/shortcuts.js +19 -3
- 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/pi-readiness.js +3 -2
- package/dist/worker/console/server.js +23 -1
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DZZ8m3PO.js → abnfDiagram-N423BO3Z-CcS17TBr.js} +1 -1
- package/dist/worker/console/static/assets/{arc-D6PvaVd-.js → arc-COptKq2S.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-B_OTOiI8.js → architectureDiagram-T3A2C74G-h4LKMHjP.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-Bv6rqHBg.js → blockDiagram-VBNYF7ZC-COA1MOH0.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B8eHr0oz.js → c4Diagram-5PPSVZJV-DJUf0QPm.js} +1 -1
- package/dist/worker/console/static/assets/channel-DdBCaOJ6.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-DYwR0im2.js → chunk-2GRJ4B5K-mtWfKrUX.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-D2WPGqXt.js → chunk-2Q5K7J3B-B8pXsxDQ.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-CQISJ_I7.js → chunk-5RXB4S5H-ipKzByl1.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-C0o2Du1e.js → chunk-5VM5RSS4-DYi3Ald_.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-4f8kr-U7.js → chunk-6Q2QTUOP-DQtGYoty.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-D_OWvXzX.js → chunk-GF5L2VYU-BU0qS2YV.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CasdPz5X.js → chunk-JWPE2WC7-DVH9dIGE.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-Cj8lRrla.js → chunk-KBJHAD2P-Dt60SmgM.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CUvD1FSd.js → chunk-RYQCIY6F-BoaUGuEY.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CqLw_eqB.js → chunk-XXDRQBXY-_WHFDTp2.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DZFra1GO.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DZFra1GO.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-ku-WqJPx.js → cose-bilkent-JH36ORCC-BxkbTIRd.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-Bq_PYDhl.js → cynefin-VYW2F7L2-Bf2UVnoG.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BiGZV641.js → cynefinDiagram-MW4NZA55-Bo23q1J_.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-C4JRlglF.js → dagre-VZM6K2ZE-Dd_UU49i.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-B0dNeWYG.js → diagram-7IWD3JNH-ebUa1a9y.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Cj35YvYM.js → diagram-B4RE2ZJO-BucthU8r.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-uzQoJ2-8.js → diagram-LBJQPF4R-BgqNpAkm.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D1a-Buoz.js → diagram-Q27KOJAE-DC4q5NGa.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-XjRrLRSs.js → diagram-UB23O5K3-CBqCaMeb.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Da_O24cW.js → ebnfDiagram-BXEA7PRR-CGtnbQ3-.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BLJ8jrYU.js → erDiagram-JOGREHBK-CG3LUao5.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-B9GrLjM0.js → flowDiagram-UKHOOZJN-D2ZVgoFS.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CeJ0TqiK.js → ganttDiagram-PKOTCBZU-DoYFZiKt.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Bm2eNNFX.js → gitGraphDiagram-DS77QQ5N-CKoP1s6j.js} +1 -1
- package/dist/worker/console/static/assets/index-CAZ2fC_X.css +1 -0
- package/dist/worker/console/static/assets/index-vbTcFnFs.js +449 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DB26i3d8.js → infoDiagram-6WML65LV-Duofv8p2.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BOGRyeZT.js → ishikawaDiagram-WSZJBQD7-D2nlCkA1.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-CZRoBO_6.js → journeyDiagram-NVQOT4AX-Dd4IHdus.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-qlWhJyoB.js → kanban-definition-27J2QSJJ-Bi20AOdt.js} +1 -1
- package/dist/worker/console/static/assets/{linear-CfUiDDB3.js → linear-D59YJ9kB.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-BQe6fpqj.js → mermaid.core-Bl13LpNa.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-zFzWHw64.js → mindmap-definition-FAOFIHXS-C6DLP5eY.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DFGUqjZO.js → pegDiagram-VL7TDLO6-BjO4cy64.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BB5i0l8Z.js → pieDiagram-7S7Q4E2Y-D7i2qZTG.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-obbVZ_X5.js → quadrantDiagram-CIZ2JOQS-Dx8o_h0y.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BwdaNzc1.js → railroadDiagram-AXF67PYL-B55orI_t.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CkTJaugh.js → requirementDiagram-LRYGKXZP-zWaehp6f.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-BwJ-hIgq.js → sankeyDiagram-W5VNT64P-CcA-pjvD.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BzSgmm7w.js → sequenceDiagram-SI44F4Z6-BOFbzNHI.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-Bn3oj5Q1.js → sizeCapture-X5ZJPWSS-BjAejah1.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-dxhGtL8K.js → stateDiagram-OKZ733FA-DXYwxJwZ.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Clg3V9t1.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-BLpEInya.js → swimlanes-SLNWSIFB-OYiOch8n.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DX1dxAAW.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-ChJGYcXy.js → timeline-definition-Z64GVDOM-cZfH3nmU.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DK-qjRer.js → vennDiagram-T6HMQDX7-BDE7b3E1.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-3qeaWAg-.js → wardleyDiagram-T6FBY63Y-kyyGy9WJ.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CootlyP9.js → xychartDiagram-ELKLHX3M-CJj6VTog.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/app/useRecoveryConsole.js +5 -5
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +19 -8
- package/dist/worker/console/static-src/operator-chat/input-history.js +8 -6
- package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +15 -7
- package/dist/worker/console/static-src/operator-chat/runtime-snapshot-store.js +10 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +258 -23
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +112 -4
- package/dist/worker/console/static-src/operator-chat/useComposer.js +13 -4
- package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +29 -6
- package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +8 -2
- package/dist/worker/console/static-src/shell/console-update-reload.js +36 -9
- package/dist/worker/console/static-src/shell/workspace-route.js +11 -0
- package/dist/worker/console/workspace-context.js +115 -1
- package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
- package/dist/worker/observe/routes.js +4 -0
- package/dist/worker/observe/static/constants.js +22 -22
- package/dist/worker/observe/static/dag-context-reason-labels.js +19 -0
- package/dist/worker/observe/static/dag-helpers.js +4 -2
- package/dist/worker/observe/static/dag-history-labels.js +1 -0
- package/dist/worker/observe/static/dag-inspector-humanize.d.ts +16 -0
- package/dist/worker/observe/static/dag-inspector-humanize.js +339 -0
- package/dist/worker/observe/static/dag-node-purpose.js +13 -8
- package/dist/worker/observe/static/index.html +4 -4
- package/dist/worker/observe/static/inspect-workspace.js +23 -0
- package/dist/worker/observe/static/kpi.js +2 -2
- package/dist/worker/observe/static/operator-chrome.d.ts +10 -2
- package/dist/worker/observe/static/operator-chrome.js +37 -23
- package/dist/worker/observe/static/prompt-restart-candidates.js +4 -2
- package/dist/worker/observe/static/relations.js +7 -7
- package/dist/worker/observe/static/router.d.ts +12 -1
- package/dist/worker/observe/static/router.js +48 -6
- package/dist/worker/observe/static/run-processing.js +4 -2
- package/dist/worker/observe/static/shell-chrome.js +2 -2
- package/dist/worker/observe/static/state.js +5 -0
- package/dist/worker/observe/static/styles.css +231 -0
- package/dist/worker/observe/static/views/dag-graph.js +1 -1
- package/dist/worker/observe/static/views/dag-inspector.js +336 -181
- package/dist/worker/observe/static/views/dag.js +26 -14
- package/dist/worker/observe/static/views/dags.js +3 -1
- package/dist/worker/observe/static/views/dashboard.js +14 -6
- package/dist/worker/observe/static/views/pool.js +7 -2
- package/dist/worker/observe/static/views/run.js +12 -3
- package/dist/worker/observe/static/views/session-timeline.js +74 -13
- package/dist/worker/observe/static/views/task.js +7 -2
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +215 -28
- package/dist/workflows/dag/backend-test-markdown-workflow.js +13 -1
- package/dist/workflows/dag/backend-test-plan-protocol.js +210 -0
- package/dist/workflows/dag/backend-test-scenario-param.js +172 -25
- package/dist/workflows/dag/backend-test-scenario-partitions.js +62 -1
- package/dist/workflows/dag/backend-test-writer-completeness.js +104 -12
- 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 +1237 -192
- package/dist/workflows/dag/frontend-plan-render.js +2 -1
- package/dist/workflows/dag/frontend-prewrite-gate.js +256 -350
- 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 +226 -0
- package/dist/workflows/dag/frontend-recovery-lineage.js +178 -0
- package/dist/workflows/dag/frontend-recovery-plan.js +21 -10
- package/dist/workflows/dag/frontend-recovery-run.js +166 -34
- 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 +941 -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 +252 -25
- package/dist/workflows/dag/frontend-worktree-diff.js +250 -17
- package/dist/workflows/dag/frontend-writer-admission.js +319 -0
- package/dist/workflows/dag/frontend-writer-status.js +256 -0
- package/dist/workflows/dag/init-hybrid.js +1031 -564
- package/dist/workflows/dag/interrupt-request.js +7 -0
- package/dist/workflows/dag/node-execution.js +854 -4
- package/dist/workflows/dag/prompt.js +70 -3
- package/dist/workflows/dag/recovery-lease.js +170 -0
- package/dist/workflows/dag/report.js +37 -1
- package/dist/workflows/dag/rerun-feedback.js +315 -1
- package/dist/workflows/dag/rerun-plan.js +17 -6
- package/dist/workflows/dag/rerun-run.js +44 -6
- package/dist/workflows/dag/rerun-task.js +251 -13
- package/dist/workflows/dag/retry-policy.js +230 -104
- package/dist/workflows/dag/runner.js +580 -124
- package/dist/workflows/dag/scheduler.js +133 -20
- package/dist/workflows/dag/types.js +244 -16
- 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 +25 -20
- 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 +4 -3
- 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
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
* Strong evidence only: package.json deps, config files, source imports.
|
|
4
4
|
* Lockfile-only or directory-name-only must NOT activate adapters.
|
|
5
5
|
*/
|
|
6
|
-
import { readFile, readdir, stat } from "node:fs/promises";
|
|
6
|
+
import { lstat, readFile, readdir, realpath, stat } from "node:fs/promises";
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import { z } from "zod";
|
|
9
|
-
import { DEFAULT_OPENSPEC_GOVERNANCE_ROOT, MAX_OPENSPEC_CANDIDATE_PATHS, OPENSPEC_SPEC_DIRS, OPENSPEC_SPEC_EXT_RE, isGovernanceSubtreePath,
|
|
9
|
+
import { DEFAULT_OPENSPEC_GOVERNANCE_ROOT, MAX_OPENSPEC_CANDIDATE_PATHS, OPENSPEC_SPEC_DIRS, OPENSPEC_SPEC_EXT_RE, isGovernanceSubtreePath, isValidFrontendSpecRoot, } from "../shared/openspec-spec.js";
|
|
10
10
|
const classifiedPathSchema = z
|
|
11
11
|
.string()
|
|
12
|
-
.refine((candidate) =>
|
|
12
|
+
.refine((candidate) => OPENSPEC_SPEC_EXT_RE.test(candidate), "classified path must be a repo-relative supported spec file");
|
|
13
13
|
/**
|
|
14
14
|
* Stable semantic partition of `designEvidence.normativePaths`. Keys are a
|
|
15
15
|
* generic directory convention (not project-specific file names); the same
|
|
@@ -81,6 +81,105 @@ async function exists(filePath) {
|
|
|
81
81
|
return false;
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
|
+
export const FRONTEND_SPEC_REGISTRY_PATH = "ai_workspace/loop-agent/spec-registry.json";
|
|
85
|
+
const SPEC_SIGNAL_RE = /(?:规范|规约|标准|约定|guideline|standard|convention|design[- ]?system|rule|token|component|style|theme)/i;
|
|
86
|
+
const SPEC_EXCLUDE_RE = /(?:^|\/)(?:archive|history|changelog|changes|proposals?|release[-_ ]notes?)(?:\/|$)/i;
|
|
87
|
+
/** Stable, bounded heuristic used only for candidate ordering/classification. */
|
|
88
|
+
export function scoreFrontendSpecCandidate(filePath, contentPreview = "", options = {}) {
|
|
89
|
+
const normalized = filePath.replaceAll("\\", "/");
|
|
90
|
+
const lower = normalized.toLowerCase();
|
|
91
|
+
const reasons = [];
|
|
92
|
+
let score = 0;
|
|
93
|
+
if (options.registryMode === "normative") {
|
|
94
|
+
score += 100;
|
|
95
|
+
reasons.push("registry-normative");
|
|
96
|
+
}
|
|
97
|
+
else if (options.registryMode === "advisory") {
|
|
98
|
+
score += 30;
|
|
99
|
+
reasons.push("registry-advisory");
|
|
100
|
+
}
|
|
101
|
+
if (options.taskRelated) {
|
|
102
|
+
score += 25;
|
|
103
|
+
reasons.push("task-related");
|
|
104
|
+
}
|
|
105
|
+
if (/(?:design-system|guidelines?|standards?|conventions?|rules?|specs?)(?:\/|$)/i.test(lower)) {
|
|
106
|
+
score += 20;
|
|
107
|
+
reasons.push("spec-directory");
|
|
108
|
+
}
|
|
109
|
+
if (/(?:guidelines?|standards?|conventions?|rules?|tokens?|components?|theme|style)/i.test(path.basename(lower))) {
|
|
110
|
+
score += 15;
|
|
111
|
+
reasons.push("spec-filename");
|
|
112
|
+
}
|
|
113
|
+
if (SPEC_SIGNAL_RE.test(contentPreview)) {
|
|
114
|
+
score += 20;
|
|
115
|
+
reasons.push("spec-content");
|
|
116
|
+
}
|
|
117
|
+
if (/(?:^|\n)\s*(?:#{1,6}\s*)?(?:rules?|do|don't|必须|禁止|约定|规范)\b/im.test(contentPreview)) {
|
|
118
|
+
score += 15;
|
|
119
|
+
reasons.push("normative-structure");
|
|
120
|
+
}
|
|
121
|
+
if (SPEC_EXCLUDE_RE.test(lower)) {
|
|
122
|
+
score -= 80;
|
|
123
|
+
reasons.push("excluded-history-path");
|
|
124
|
+
}
|
|
125
|
+
const kind = options.registryMode === "normative" || score >= 80
|
|
126
|
+
? "normative"
|
|
127
|
+
: score >= 35
|
|
128
|
+
? "advisory"
|
|
129
|
+
: "unknown";
|
|
130
|
+
return { path: normalized, kind, score, reasons, source: options.registryMode ? "registry" : "auto-discovered" };
|
|
131
|
+
}
|
|
132
|
+
/** Read the optional project-level registry without making it mandatory. */
|
|
133
|
+
export async function readFrontendSpecRegistry(repoRoot) {
|
|
134
|
+
const raw = await readJson(path.join(repoRoot, FRONTEND_SPEC_REGISTRY_PATH));
|
|
135
|
+
if (!raw || typeof raw !== "object")
|
|
136
|
+
return null;
|
|
137
|
+
const value = raw;
|
|
138
|
+
if (value.version !== 1 || !Array.isArray(value.roots))
|
|
139
|
+
return null;
|
|
140
|
+
const roots = value.roots
|
|
141
|
+
.filter((item) => Boolean(item) && typeof item === "object")
|
|
142
|
+
.map((item) => ({
|
|
143
|
+
path: typeof item.path === "string" ? item.path.replaceAll("\\", "/").replace(/\/+$/, "") : "",
|
|
144
|
+
scope: Array.isArray(item.scope) ? item.scope.filter((v) => typeof v === "string") : undefined,
|
|
145
|
+
mode: (item.mode === "normative" || item.mode === "advisory" ? item.mode : undefined),
|
|
146
|
+
description: typeof item.description === "string" ? item.description : undefined,
|
|
147
|
+
}))
|
|
148
|
+
.filter((item) => isValidFrontendSpecRoot(item.path));
|
|
149
|
+
return {
|
|
150
|
+
version: 1,
|
|
151
|
+
roots,
|
|
152
|
+
discovery: value.discovery && typeof value.discovery === "object" ? value.discovery : undefined,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Resolve only unambiguous symlink-root aliases. The physical target is never
|
|
157
|
+
* emitted as a candidate path: it merely lets task-source wording such as
|
|
158
|
+
* `ai_doc/rules.md` resolve to the stable repo-local path `ai_workspace/rules.md`.
|
|
159
|
+
*/
|
|
160
|
+
export async function resolveFrontendSpecRootAliases(repoRoot, specRoots) {
|
|
161
|
+
const candidates = [];
|
|
162
|
+
for (const logicalRoot of specRoots) {
|
|
163
|
+
const full = path.join(repoRoot, logicalRoot);
|
|
164
|
+
try {
|
|
165
|
+
if (!(await lstat(full)).isSymbolicLink())
|
|
166
|
+
continue;
|
|
167
|
+
const alias = path.basename(await realpath(full));
|
|
168
|
+
if (alias && alias !== path.basename(logicalRoot)) {
|
|
169
|
+
candidates.push({ alias, logicalRoot });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
// Missing/broken links are handled by normal candidate discovery drift.
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const counts = new Map();
|
|
177
|
+
for (const item of candidates)
|
|
178
|
+
counts.set(item.alias, (counts.get(item.alias) ?? 0) + 1);
|
|
179
|
+
return candidates
|
|
180
|
+
.filter((item) => counts.get(item.alias) === 1)
|
|
181
|
+
.sort((a, b) => a.alias.localeCompare(b.alias));
|
|
182
|
+
}
|
|
84
183
|
/**
|
|
85
184
|
* Resolve the governance subtree root from harness.json.governanceRoot.
|
|
86
185
|
* Falls back to DEFAULT_OPENSPEC_GOVERNANCE_ROOT when the manifest is missing,
|
|
@@ -105,28 +204,54 @@ export async function listOpenspecDiscovery(repoRoot, options = {}) {
|
|
|
105
204
|
.filter(Boolean);
|
|
106
205
|
const collected = [];
|
|
107
206
|
let excludedGovernanceCount = 0;
|
|
207
|
+
let scannedFiles = 0;
|
|
208
|
+
let maxFilesHit = false;
|
|
209
|
+
let scannedBytes = 0;
|
|
210
|
+
const warnings = [];
|
|
211
|
+
const maxDepth = options.maxDepth ?? 4;
|
|
212
|
+
const maxFiles = options.maxFiles ?? MAX_OPENSPEC_CANDIDATE_PATHS;
|
|
213
|
+
const maxBytes = options.maxBytes ?? 1024 * 1024;
|
|
214
|
+
const timeoutMs = options.timeoutMs ?? 2000;
|
|
215
|
+
const startedAt = Date.now();
|
|
216
|
+
// Metadata / bookkeeping directories that are not normative design specs:
|
|
217
|
+
// changelogs, ADR/history archives, proposals under review, and scratch
|
|
218
|
+
// state. Reading them inflates the candidate list (and the plan prompt)
|
|
219
|
+
// with files no component selection should cite. A task that genuinely
|
|
220
|
+
// needs one can still reference it explicitly — the prewrite gate
|
|
221
|
+
// candidates come from this discovery, but explicit task-source citations
|
|
222
|
+
// flow through openspecMandatoryPaths independently.
|
|
223
|
+
const DEFAULT_EXCLUDED_SPEC_PREFIXES = [
|
|
224
|
+
"openspec/changes",
|
|
225
|
+
"openspec/project-specs/changes",
|
|
226
|
+
"openspec/project-specs/changelog",
|
|
227
|
+
"openspec/project-specs/history",
|
|
228
|
+
"openspec/project-specs/archive",
|
|
229
|
+
"openspec/project-specs/decisions",
|
|
230
|
+
"openspec/project-specs/proposals",
|
|
231
|
+
];
|
|
108
232
|
const isExcluded = (rel) => {
|
|
109
233
|
if (isGovernanceSubtreePath(rel, governanceRoot))
|
|
110
234
|
return true;
|
|
235
|
+
if (DEFAULT_EXCLUDED_SPEC_PREFIXES.some((prefix) => rel === prefix || rel.startsWith(prefix + "/"))) {
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
111
238
|
return extraExcludedPrefixes.some((prefix) => rel === prefix || rel.startsWith(prefix + "/"));
|
|
112
239
|
};
|
|
113
|
-
for (const specDir of OPENSPEC_SPEC_DIRS) {
|
|
240
|
+
for (const specDir of options.specRoots ?? [...OPENSPEC_SPEC_DIRS]) {
|
|
114
241
|
const root = path.join(repoRoot, specDir);
|
|
115
242
|
if (!(await exists(root)))
|
|
116
243
|
continue;
|
|
117
244
|
await walk(root, 0);
|
|
118
245
|
}
|
|
119
|
-
//
|
|
120
|
-
//
|
|
246
|
+
// Keep the established default-root priority for deterministic compatibility;
|
|
247
|
+
// configured extra roots are appended in lexical order.
|
|
121
248
|
const unique = [...new Set(collected)];
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
.
|
|
125
|
-
const aiWorkspacePaths = unique
|
|
126
|
-
.filter((candidate) => candidate.startsWith("ai_workspace/"))
|
|
249
|
+
const defaultPaths = OPENSPEC_SPEC_DIRS.flatMap((root) => unique.filter((candidate) => candidate.startsWith(root + "/")).sort());
|
|
250
|
+
const customPaths = unique
|
|
251
|
+
.filter((candidate) => !defaultPaths.includes(candidate))
|
|
127
252
|
.sort();
|
|
128
|
-
const deduped = [...
|
|
129
|
-
const truncated = deduped.length > MAX_OPENSPEC_CANDIDATE_PATHS;
|
|
253
|
+
const deduped = [...defaultPaths, ...customPaths];
|
|
254
|
+
const truncated = maxFilesHit || deduped.length > MAX_OPENSPEC_CANDIDATE_PATHS;
|
|
130
255
|
const paths = truncated
|
|
131
256
|
? deduped.slice(0, MAX_OPENSPEC_CANDIDATE_PATHS)
|
|
132
257
|
: deduped;
|
|
@@ -136,10 +261,19 @@ export async function listOpenspecDiscovery(repoRoot, options = {}) {
|
|
|
136
261
|
excludedGovernanceCount,
|
|
137
262
|
truncated,
|
|
138
263
|
maxCandidates: MAX_OPENSPEC_CANDIDATE_PATHS,
|
|
264
|
+
warnings,
|
|
139
265
|
};
|
|
140
266
|
async function walk(dir, depth) {
|
|
141
|
-
if (
|
|
267
|
+
if (Date.now() - startedAt >= timeoutMs) {
|
|
268
|
+
if (!warnings.includes("timeout-exceeded"))
|
|
269
|
+
warnings.push("timeout-exceeded");
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (depth > maxDepth) {
|
|
273
|
+
if (!warnings.includes("max-depth-exceeded"))
|
|
274
|
+
warnings.push("max-depth-exceeded");
|
|
142
275
|
return;
|
|
276
|
+
}
|
|
143
277
|
let entries = [];
|
|
144
278
|
try {
|
|
145
279
|
entries = (await readdir(dir)).sort();
|
|
@@ -148,12 +282,31 @@ export async function listOpenspecDiscovery(repoRoot, options = {}) {
|
|
|
148
282
|
return;
|
|
149
283
|
}
|
|
150
284
|
for (const name of entries) {
|
|
285
|
+
if (Date.now() - startedAt >= timeoutMs) {
|
|
286
|
+
if (!warnings.includes("timeout-exceeded"))
|
|
287
|
+
warnings.push("timeout-exceeded");
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (scannedFiles >= maxFiles) {
|
|
291
|
+
if (!warnings.includes("max-files-exceeded"))
|
|
292
|
+
warnings.push("max-files-exceeded");
|
|
293
|
+
maxFilesHit = true;
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
151
296
|
const full = path.join(dir, name);
|
|
152
297
|
try {
|
|
153
298
|
const info = await stat(full);
|
|
154
299
|
if (info.isDirectory())
|
|
155
300
|
await walk(full, depth + 1);
|
|
156
301
|
else if (OPENSPEC_SPEC_EXT_RE.test(name)) {
|
|
302
|
+
const size = info.size;
|
|
303
|
+
if (scannedBytes + size > maxBytes) {
|
|
304
|
+
if (!warnings.includes("max-bytes-exceeded"))
|
|
305
|
+
warnings.push("max-bytes-exceeded");
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
scannedFiles++;
|
|
309
|
+
scannedBytes += size;
|
|
157
310
|
const rel = path.relative(repoRoot, full).replace(/\\/g, "/");
|
|
158
311
|
if (isExcluded(rel)) {
|
|
159
312
|
excludedGovernanceCount++;
|
|
@@ -241,6 +394,14 @@ function emptyClassified() {
|
|
|
241
394
|
* project-specific file names. Basename stems use word-boundary matching so
|
|
242
395
|
* e.g. `*api*` (case-insensitive) maps to `rule.api` without hardcoding names.
|
|
243
396
|
*/
|
|
397
|
+
/** True when any path segment (other than the file itself) carries a
|
|
398
|
+
* component/theme kind marker. Directory-organized libraries such as
|
|
399
|
+
* `ui/ucp-components-md/AdvancedSearch.md` rely on this; templates/ and
|
|
400
|
+
* rules/ are handled by their own branches before this fallback runs. */
|
|
401
|
+
function segmentMatchesComponentKind(segments) {
|
|
402
|
+
const dirSegments = segments.slice(0, -1);
|
|
403
|
+
return dirSegments.some((segment) => /\bcomponents?\b/.test(segment.toLowerCase()));
|
|
404
|
+
}
|
|
244
405
|
function classifyOpenspecPaths(paths) {
|
|
245
406
|
const classified = emptyClassified();
|
|
246
407
|
const ruleStem = (basename) => {
|
|
@@ -260,7 +421,7 @@ function classifyOpenspecPaths(paths) {
|
|
|
260
421
|
return "other";
|
|
261
422
|
};
|
|
262
423
|
for (const candidate of paths) {
|
|
263
|
-
if (!
|
|
424
|
+
if (!OPENSPEC_SPEC_EXT_RE.test(candidate))
|
|
264
425
|
continue;
|
|
265
426
|
const segments = candidate.split("/");
|
|
266
427
|
const basename = segments[segments.length - 1] ?? "";
|
|
@@ -279,8 +440,17 @@ function classifyOpenspecPaths(paths) {
|
|
|
279
440
|
classified.theme.push(candidate);
|
|
280
441
|
else if (/\bcomponents?\b/.test(lower))
|
|
281
442
|
classified.component.push(candidate);
|
|
282
|
-
else
|
|
443
|
+
else if (segmentMatchesComponentKind(segments)) {
|
|
444
|
+
// Directory-organized component libraries (e.g.
|
|
445
|
+
// ui/ucp-components-md/AdvancedSearch.md) carry the kind in the
|
|
446
|
+
// parent directory name, not the file name. The parent-dir rule
|
|
447
|
+
// is a fallback so such layouts still land in the component
|
|
448
|
+
// bucket instead of being dropped into uiOther.
|
|
449
|
+
classified.component.push(candidate);
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
283
452
|
classified.uiOther.push(candidate);
|
|
453
|
+
}
|
|
284
454
|
}
|
|
285
455
|
else {
|
|
286
456
|
classified.advisoryOther.push(candidate);
|
|
@@ -325,6 +495,7 @@ function toOpenspecDiscovery(discovery, designEvidence) {
|
|
|
325
495
|
excludedGovernanceCount: discovery.excludedGovernanceCount,
|
|
326
496
|
truncated: discovery.truncated,
|
|
327
497
|
maxCandidates: discovery.maxCandidates,
|
|
498
|
+
warnings: discovery.warnings,
|
|
328
499
|
};
|
|
329
500
|
}
|
|
330
501
|
export function buildAdapterGuidance(capability) {
|
|
@@ -344,7 +515,7 @@ export function buildAdapterGuidance(capability) {
|
|
|
344
515
|
lines.push("Vue adapter: prefer Composition API if project already uses it; reuse Pinia/Vue Router/Vue Query only when evidenced; Vitest + Vue Test Utils when present.");
|
|
345
516
|
}
|
|
346
517
|
else {
|
|
347
|
-
lines.push("Unknown framework: use generic frontend
|
|
518
|
+
lines.push("Unknown framework: use only the node-scoped generic frontend guidance; do not assume React/Vue/Next APIs.");
|
|
348
519
|
}
|
|
349
520
|
if (capability.a11y.status === "present") {
|
|
350
521
|
lines.push(`A11y tools present: ${capability.a11y.tools.join(", ")}. Report static/component-level evidence only; Browser a11y remains not-run.`);
|
|
@@ -361,13 +532,25 @@ export function buildAdapterGuidance(capability) {
|
|
|
361
532
|
* Discover frontend project capability from repo root.
|
|
362
533
|
* Strong evidence: package.json direct deps + config/bootstrap files.
|
|
363
534
|
*/
|
|
364
|
-
export async function discoverFrontendProjectCapability(repoRoot) {
|
|
535
|
+
export async function discoverFrontendProjectCapability(repoRoot, options = {}) {
|
|
536
|
+
const registry = await readFrontendSpecRegistry(repoRoot);
|
|
537
|
+
const registryRoots = registry?.roots
|
|
538
|
+
.filter((root) => !root.scope || root.scope.includes("frontend"))
|
|
539
|
+
.map((root) => root.path) ?? [];
|
|
540
|
+
const discoveryOptions = {
|
|
541
|
+
...options,
|
|
542
|
+
specRoots: [...new Set([...OPENSPEC_SPEC_DIRS, ...(options.specRoots ?? []), ...registryRoots])],
|
|
543
|
+
maxDepth: registry?.discovery?.maxDepth ?? options.maxDepth,
|
|
544
|
+
maxFiles: registry?.discovery?.maxFiles ?? options.maxFiles,
|
|
545
|
+
maxBytes: registry?.discovery?.maxBytes ?? options.maxBytes,
|
|
546
|
+
timeoutMs: registry?.discovery?.timeoutMs ?? options.timeoutMs,
|
|
547
|
+
};
|
|
365
548
|
const evidencePaths = [];
|
|
366
549
|
const reasons = [];
|
|
367
550
|
const pkgPath = path.join(repoRoot, "package.json");
|
|
368
551
|
const pkgRaw = await readJson(pkgPath);
|
|
369
552
|
if (!pkgRaw || typeof pkgRaw !== "object") {
|
|
370
|
-
const discovery = await listOpenspecDiscovery(repoRoot);
|
|
553
|
+
const discovery = await listOpenspecDiscovery(repoRoot, discoveryOptions);
|
|
371
554
|
const designEvidence = buildDesignEvidence(discovery.paths);
|
|
372
555
|
const base = {
|
|
373
556
|
schemaVersion: 1,
|
|
@@ -540,7 +723,7 @@ export async function discoverFrontendProjectCapability(repoRoot) {
|
|
|
540
723
|
router = router ?? "next-router";
|
|
541
724
|
if (hasDep(deps, "vue-router"))
|
|
542
725
|
router = "vue-router";
|
|
543
|
-
const discovery = await listOpenspecDiscovery(repoRoot);
|
|
726
|
+
const discovery = await listOpenspecDiscovery(repoRoot, discoveryOptions);
|
|
544
727
|
const designEvidence = buildDesignEvidence(discovery.paths);
|
|
545
728
|
if (discovery.paths.length)
|
|
546
729
|
evidencePaths.push(...discovery.paths.slice(0, 5));
|
package/dist/task/runtime.js
CHANGED
|
@@ -72,7 +72,7 @@ export function getTaskPaths(repoRoot, taskId) {
|
|
|
72
72
|
dagDraftPath: path.join(taskDir, 'dag.json'),
|
|
73
73
|
};
|
|
74
74
|
}
|
|
75
|
-
export async function createTask(repoRoot, taskId, title) {
|
|
75
|
+
export async function createTask(repoRoot, taskId, title, options) {
|
|
76
76
|
const validation = validateNewTaskId(taskId);
|
|
77
77
|
for (const warning of validation.warnings) {
|
|
78
78
|
console.warn(`[new-task] warning: ${warning}`);
|
|
@@ -97,7 +97,10 @@ export async function createTask(repoRoot, taskId, title) {
|
|
|
97
97
|
taskId,
|
|
98
98
|
title: title ?? taskId,
|
|
99
99
|
sourceFiles: [],
|
|
100
|
-
taskKind: 'standard',
|
|
100
|
+
taskKind: options?.taskKind ?? 'standard',
|
|
101
|
+
...(options?.taskKindExplicit !== undefined
|
|
102
|
+
? { taskKindExplicit: options.taskKindExplicit }
|
|
103
|
+
: {}),
|
|
101
104
|
referenceRepos: [],
|
|
102
105
|
referenceDocs: [],
|
|
103
106
|
allowedPaths: [],
|
|
@@ -17,10 +17,10 @@ function resolveTaskKind(flags, baseDraft, existing) {
|
|
|
17
17
|
const parsed = taskKindSchema.safeParse(candidate);
|
|
18
18
|
return parsed.success ? parsed.data : String(candidate);
|
|
19
19
|
}
|
|
20
|
-
function resolveTaskKindExplicit(flags, baseDraft) {
|
|
20
|
+
function resolveTaskKindExplicit(flags, baseDraft, existing) {
|
|
21
21
|
if (flags.taskKindExplicit !== undefined)
|
|
22
22
|
return flags.taskKindExplicit;
|
|
23
|
-
return baseDraft?.taskKindExplicit ?? false;
|
|
23
|
+
return (baseDraft?.taskKindExplicit ?? existing?.taskKindExplicit ?? false);
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
26
26
|
* Resolve managed backend-test layout config.
|
|
@@ -122,7 +122,7 @@ export function buildPrepareDraft(input) {
|
|
|
122
122
|
const { taskId, existingTaskConfig, baseDraft, facts, flags, references } = input;
|
|
123
123
|
const title = resolveTitle(taskId, flags, facts, baseDraft, existingTaskConfig);
|
|
124
124
|
const taskKind = resolveTaskKind(flags, baseDraft, existingTaskConfig);
|
|
125
|
-
const taskKindExplicit = resolveTaskKindExplicit(flags, baseDraft);
|
|
125
|
+
const taskKindExplicit = resolveTaskKindExplicit(flags, baseDraft, existingTaskConfig);
|
|
126
126
|
const featureId = trimNonEmpty(flags.featureId) ??
|
|
127
127
|
trimNonEmpty(baseDraft?.featureId) ??
|
|
128
128
|
trimNonEmpty(existingTaskConfig?.featureId) ??
|
|
@@ -16,17 +16,15 @@ const NON_GOAL_HEADING_RE = /(?:^|\s)(?:非目标|out of scope|non-goals|non goa
|
|
|
16
16
|
const OBJECTIVE_HEADING_RE = /(?:^|\s)(?:目标|objective|问题与目标|业务目标|需求概述)(?:\s|$)/iu;
|
|
17
17
|
/** 可执行片段启发式(AC 风格/祈使语气):命中则不可按 context-only 降级。 */
|
|
18
18
|
const EXECUTABLE_HINT_RE = /(?:必须|应当|应支持|需要|需|不得|should|must|shall|will|require|support|implement|provide|ensure|verify|当.*?时|when.*?then)/iu;
|
|
19
|
-
/** 显式
|
|
20
|
-
* 与 checklist
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
*
|
|
27
|
-
|
|
28
|
-
* 捕获组 1 = AC id,捕获组 2 = 标题余下文本。 */
|
|
29
|
-
const EXPLICIT_AC_HEADING_RE = /^\s*(AC(?:[-_](?:FE|BE|UI|API))?[-_][A-Z0-9]+(?:[-_][A-Z0-9]+)*)\s*[::]?\s*(.*)$/iu;
|
|
19
|
+
/** 显式 requirement ID:AC-001 / BR-001 / REQ-SRC-001 等(允许 bullet 标记后空格
|
|
20
|
+
* 与 checklist 勾选框)。AC/BR/REQ 都是 DAG 的合法 canonical requirement 身份;
|
|
21
|
+
* 之前这里只识别 AC,导致已进入 canonicalRequirements 的 BR 片段无法绑定来源。
|
|
22
|
+
* 捕获组 1 = requirement id,捕获组 2 = 余下文本。 */
|
|
23
|
+
const EXPLICIT_REQUIREMENT_ID_RE = /^\s*(?:(?:[-*+]|\d+[.)])\s+(?:\[[ xX]\]\s+)?)?((?:REQ|BR|AC)(?:[-_][A-Z0-9]+)+)\s*[::]?\s*(.*)$/iu;
|
|
24
|
+
/** 显式 requirement 标题式:`#### AC-FE-001 Save nickname` / `### BR-001:正文`。
|
|
25
|
+
* 普通单词(Acceptance Criteria / Acceptance)永不匹配,按章节分类生成稳定
|
|
26
|
+
* REQ-SRC-* 片段。捕获组 1 = requirement id,捕获组 2 = 标题余下文本。 */
|
|
27
|
+
const EXPLICIT_REQUIREMENT_HEADING_RE = /^\s*((?:REQ|BR|AC)(?:[-_][A-Z0-9]+)+)\s*[::]?\s*(.*)$/iu;
|
|
30
28
|
/** Markdown 表格数据行(非分隔行)。 */
|
|
31
29
|
const TABLE_DATA_ROW_RE = /^\s*\|.*\|\s*$/;
|
|
32
30
|
const TABLE_SEPARATOR_ROW_RE = /^\s*\|[\s:|-]+\|\s*$/;
|
|
@@ -35,11 +33,11 @@ function isTableHeaderRow(line, nextLine) {
|
|
|
35
33
|
return isTableDataRow(line) && TABLE_SEPARATOR_ROW_RE.test(nextLine.trim());
|
|
36
34
|
}
|
|
37
35
|
/** 表格行首格显式 AC ID:`| AC-001 | 订单列表加载完成 |` → id=AC-001, rest=订单列表加载完成。 */
|
|
38
|
-
function
|
|
36
|
+
function matchTableRowRequirementId(line) {
|
|
39
37
|
const trimmed = line.trim().replace(/^\|/, "");
|
|
40
38
|
const cells = trimmed.split("|");
|
|
41
39
|
const first = cells[0]?.trim() ?? "";
|
|
42
|
-
const idMatch =
|
|
40
|
+
const idMatch = EXPLICIT_REQUIREMENT_HEADING_RE.exec(first);
|
|
43
41
|
if (!idMatch)
|
|
44
42
|
return null;
|
|
45
43
|
const id = (idMatch[1] ?? "").trim().toUpperCase().replace(/_/g, "-");
|
|
@@ -52,6 +50,14 @@ function matchTableRowAcId(line) {
|
|
|
52
50
|
.trim();
|
|
53
51
|
return { id, rest: rest || id };
|
|
54
52
|
}
|
|
53
|
+
/** The splitter may use only AC ids as fragment identities; BR/REQ ids are
|
|
54
|
+
* mapped later while keeping the generated REQ-SRC fragment identity. */
|
|
55
|
+
function matchTableRowAcId(line) {
|
|
56
|
+
const match = matchTableRowRequirementId(line);
|
|
57
|
+
if (!match || !/^AC(?:-[A-Z0-9]+)+$/iu.test(match.id))
|
|
58
|
+
return null;
|
|
59
|
+
return match;
|
|
60
|
+
}
|
|
55
61
|
export function sha256Text(text) {
|
|
56
62
|
return createHash("sha256").update(text, "utf8").digest("hex");
|
|
57
63
|
}
|
|
@@ -66,7 +72,20 @@ export function normalizeFragmentText(text) {
|
|
|
66
72
|
.trim();
|
|
67
73
|
}
|
|
68
74
|
export function matchExplicitAcId(line) {
|
|
69
|
-
const match =
|
|
75
|
+
const match = EXPLICIT_REQUIREMENT_ID_RE.exec(line.trim());
|
|
76
|
+
if (!match)
|
|
77
|
+
return null;
|
|
78
|
+
const rawId = match[1] ?? "";
|
|
79
|
+
if (!/^AC(?:[-_][A-Z0-9]+)+$/iu.test(rawId))
|
|
80
|
+
return null;
|
|
81
|
+
const id = rawId.trim().toUpperCase().replace(/_/g, "-");
|
|
82
|
+
if (!id)
|
|
83
|
+
return null;
|
|
84
|
+
return { id, rest: (match[2] ?? "").trim() };
|
|
85
|
+
}
|
|
86
|
+
/** Match an explicit AC/BR/REQ id in a list item or a paragraph fragment. */
|
|
87
|
+
export function matchExplicitRequirementId(line) {
|
|
88
|
+
const match = EXPLICIT_REQUIREMENT_ID_RE.exec(line.trim());
|
|
70
89
|
if (!match)
|
|
71
90
|
return null;
|
|
72
91
|
const rawId = match[1] ?? "";
|
|
@@ -77,7 +96,20 @@ export function matchExplicitAcId(line) {
|
|
|
77
96
|
}
|
|
78
97
|
/** V4 显式 AC 标题式匹配:`#### AC-FE-001 Save nickname` → id=AC-FE-001, rest=Save nickname。 */
|
|
79
98
|
export function matchExplicitAcHeading(headingText) {
|
|
80
|
-
const match =
|
|
99
|
+
const match = EXPLICIT_REQUIREMENT_HEADING_RE.exec(headingText.trim());
|
|
100
|
+
if (!match)
|
|
101
|
+
return null;
|
|
102
|
+
const rawId = match[1] ?? "";
|
|
103
|
+
if (!/^AC(?:[-_][A-Z0-9]+)+$/iu.test(rawId))
|
|
104
|
+
return null;
|
|
105
|
+
const id = rawId.trim().toUpperCase().replace(/_/g, "-");
|
|
106
|
+
if (!id)
|
|
107
|
+
return null;
|
|
108
|
+
return { id, rest: (match[2] ?? "").trim() };
|
|
109
|
+
}
|
|
110
|
+
/** Match an explicit AC/BR/REQ id in a heading. */
|
|
111
|
+
export function matchExplicitRequirementHeading(headingText) {
|
|
112
|
+
const match = EXPLICIT_REQUIREMENT_HEADING_RE.exec(headingText.trim());
|
|
81
113
|
if (!match)
|
|
82
114
|
return null;
|
|
83
115
|
const rawId = match[1] ?? "";
|
|
@@ -175,8 +207,11 @@ export function splitDocumentIntoFragments(doc) {
|
|
|
175
207
|
const explicitSectionAc = matchExplicitAcHeading(lastHeadingSegment);
|
|
176
208
|
const sectionKind = classifySectionHeading(section.headingPath);
|
|
177
209
|
const effectiveKind = explicitSectionAc ? "acceptance" : sectionKind;
|
|
210
|
+
// V4 显式 AC 标题行已在下方入口 emit(explicitSectionAc 分支);正文循环
|
|
211
|
+
// 必须从标题行的下一行开始,否则同一标题行会被正文循环的 heading 分支
|
|
212
|
+
// 再次 emit,产生结构性 duplicate(每个 AC 标题式片段出现两次)。
|
|
178
213
|
const bodyStart = section.headingPath
|
|
179
|
-
? section.startLine +
|
|
214
|
+
? section.startLine + 1
|
|
180
215
|
: section.startLine;
|
|
181
216
|
const bodyEnd = section.endLine;
|
|
182
217
|
const bodyLines = lines.slice(Math.max(0, bodyStart - 1), Math.min(bodyEnd, lines.length));
|
|
@@ -386,7 +421,8 @@ export function buildFragmentInventory(input) {
|
|
|
386
421
|
: null;
|
|
387
422
|
const explicit = explicitIdFromId
|
|
388
423
|
? { id: explicitIdFromId, rest: fragment.text }
|
|
389
|
-
:
|
|
424
|
+
: matchExplicitRequirementId(fragment.text) ??
|
|
425
|
+
matchTableRowRequirementId(fragment.text);
|
|
390
426
|
// V4 标题/条目下正文继承的显式 AC ID(不生成取代它的 REQ-SRC-*)。
|
|
391
427
|
const inherited = !explicit && fragment.explicitAcId
|
|
392
428
|
? { id: fragment.explicitAcId, rest: fragment.text }
|
|
@@ -438,14 +474,17 @@ export function buildFragmentInventory(input) {
|
|
|
438
474
|
disposition = "non-goal";
|
|
439
475
|
}
|
|
440
476
|
else {
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
477
|
+
// Non-goal/constraint/acceptance/objective sections are classified
|
|
478
|
+
// above. An "other" section (背景 / 概述 / 说明 …) is contextual
|
|
479
|
+
// description, not an executable requirement: even when it mentions
|
|
480
|
+
// implementation verbs ("需要", "渲染", "组件"), it must not be
|
|
481
|
+
// promoted to a high-risk unresolved executable fragment — that
|
|
482
|
+
// blocks PRDs whose 背景 prose happens to use such words (merge
|
|
483
|
+
// regression: "需要一个小组件" was flagged executable via 需要).
|
|
484
|
+
// Explicit AC ids are already handled by the branches above, so
|
|
485
|
+
// this branch is always contextual.
|
|
486
|
+
disposition = "context-only";
|
|
487
|
+
reason = "context-only fragment; excluded from canonical requirement coverage";
|
|
449
488
|
}
|
|
450
489
|
const priorId = seenText.get(text);
|
|
451
490
|
if (priorId) {
|