@opengsd/gsd-core 1.12.0 → 1.13.0
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.opencode/plugins/gsd-core.js +12 -0
- package/agents/gsd-executor.md +63 -35
- package/agents/gsd-plan-checker.md +76 -57
- package/agents/gsd-planner.md +14 -0
- package/agents/gsd-ui-checker.md +19 -3
- package/agents/gsd-ui-researcher.md +29 -0
- package/agents/gsd-verifier.md +23 -1
- package/bin/install.js +239 -67
- package/commands/gsd/execute-phase.md +1 -1
- package/commands/gsd/ns-workflow.md +2 -1
- package/commands/gsd/phase.md +1 -1
- package/commands/gsd/quick-batch.md +105 -0
- package/commands/gsd/surface.md +18 -8
- package/gsd-core/bin/gsd-tools.cjs +195 -50
- package/gsd-core/bin/lib/capability-activation.cjs +27 -0
- package/gsd-core/bin/lib/capability-registry.cjs +514 -114
- package/gsd-core/bin/lib/capability-state.cjs +7 -1
- package/gsd-core/bin/lib/capability-validator.cjs +120 -4
- package/gsd-core/bin/lib/capability-writer.cjs +14 -4
- package/gsd-core/bin/lib/check-command-router.cjs +85 -2
- package/gsd-core/bin/lib/claude-orchestration.cjs +10 -25
- package/gsd-core/bin/lib/clusters.cjs +1 -0
- package/gsd-core/bin/lib/command-aliases.cjs +16 -0
- package/gsd-core/bin/lib/commands.cjs +337 -13
- package/gsd-core/bin/lib/config-loader.cjs +3 -0
- package/gsd-core/bin/lib/core-utils.cjs +34 -7
- package/gsd-core/bin/lib/decisions.cjs +213 -1
- package/gsd-core/bin/lib/edge-probe.cjs +14 -1
- package/gsd-core/bin/lib/file-overlap-partitioner.cjs +74 -0
- package/gsd-core/bin/lib/frontmatter.cjs +137 -23
- package/gsd-core/bin/lib/gap-checker.cjs +22 -13
- package/gsd-core/bin/lib/git-base-branch.cjs +10 -2
- package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +8 -2
- package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +54 -11
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +75 -22
- package/gsd-core/bin/lib/host-integration.cjs +57 -5
- package/gsd-core/bin/lib/init-command-router.cjs +14 -0
- package/gsd-core/bin/lib/init.cjs +132 -15
- package/gsd-core/bin/lib/install-engine.cjs +184 -12
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +45 -0
- package/gsd-core/bin/lib/install-profiles.cjs +22 -14
- package/gsd-core/bin/lib/installer-migration-report.cjs +1 -0
- package/gsd-core/bin/lib/io.cjs +35 -0
- package/gsd-core/bin/lib/loop-resolver.cjs +14 -8
- package/gsd-core/bin/lib/markdown-table.cjs +123 -0
- package/gsd-core/bin/lib/milestone.cjs +22 -2
- package/gsd-core/bin/lib/phase-command-router.cjs +13 -6
- package/gsd-core/bin/lib/phase-id.cjs +251 -9
- package/gsd-core/bin/lib/phase.cjs +774 -35
- package/gsd-core/bin/lib/plan-document.cjs +10 -0
- package/gsd-core/bin/lib/planning-snapshot.cjs +147 -20
- package/gsd-core/bin/lib/planning-workspace.cjs +103 -28
- package/gsd-core/bin/lib/quick-batch-command-router.cjs +285 -0
- package/gsd-core/bin/lib/quick-batch-dispatch.cjs +250 -0
- package/gsd-core/bin/lib/quick-batch.cjs +840 -0
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +53 -5
- package/gsd-core/bin/lib/review-lane-invocation.cjs +73 -1
- package/gsd-core/bin/lib/review-lane-runner.cjs +136 -10
- package/gsd-core/bin/lib/roadmap-parser.cjs +499 -26
- package/gsd-core/bin/lib/roadmap.cjs +187 -58
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +233 -33
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +16 -17
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +286 -108
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +215 -43
- package/gsd-core/bin/lib/shell-command-projection.cjs +4 -0
- package/gsd-core/bin/lib/smart-entry.cjs +7 -9
- package/gsd-core/bin/lib/state-document.cjs +30 -5
- package/gsd-core/bin/lib/state-md-schema.cjs +23 -13
- package/gsd-core/bin/lib/state-transition.cjs +333 -44
- package/gsd-core/bin/lib/state.cjs +684 -125
- package/gsd-core/bin/lib/surface.cjs +23 -8
- package/gsd-core/bin/lib/tdd-red-evidence.cjs +133 -0
- package/gsd-core/bin/lib/uat.cjs +1419 -515
- package/gsd-core/bin/lib/update-context.cjs +6 -2
- package/gsd-core/bin/lib/validate.cjs +230 -12
- package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
- package/gsd-core/bin/lib/verification.cjs +273 -12
- package/gsd-core/bin/lib/verify-command-router.cjs +1 -0
- package/gsd-core/bin/lib/verify.cjs +346 -16
- package/gsd-core/bin/lib/workstream-inventory.cjs +20 -2
- package/gsd-core/bin/lib/worktree-safety.cjs +8 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +8 -0
- package/gsd-core/bin/verify-reapply-patches.cjs +70 -3
- package/gsd-core/references/agent-contracts.md +3 -3
- package/gsd-core/references/edge-probe.md +17 -13
- package/gsd-core/references/execute-mvp-tdd.md +18 -16
- package/gsd-core/references/execute-phase-response-language.md +6 -0
- package/gsd-core/references/executor-examples.md +42 -0
- package/gsd-core/references/few-shot-examples/plan-checker.md +15 -15
- package/gsd-core/references/mvp-concepts.md +2 -2
- package/gsd-core/references/plan-checker-examples.md +41 -0
- package/gsd-core/references/planner-antipatterns.md +25 -0
- package/gsd-core/references/planner-chunked.md +5 -1
- package/gsd-core/references/planner-coupling.md +42 -0
- package/gsd-core/references/planner-quick-batch.md +71 -0
- package/gsd-core/references/planner-reviews.md +47 -0
- package/gsd-core/references/planner-revision.md +75 -2
- package/gsd-core/references/planning-config.md +2 -1
- package/gsd-core/references/response-language-directive.md +9 -0
- package/gsd-core/references/revision-loop.md +118 -11
- package/gsd-core/references/tdd.md +14 -9
- package/gsd-core/references/verifier-evidence-gate.md +160 -0
- package/gsd-core/templates/phase-prompt.md +4 -0
- package/gsd-core/templates/verification-report.md +5 -0
- package/gsd-core/workflows/add-backlog.md +2 -0
- package/gsd-core/workflows/add-phase.md +2 -0
- package/gsd-core/workflows/add-tests.md +1 -1
- package/gsd-core/workflows/add-todo.md +1 -1
- package/gsd-core/workflows/ai-integration-phase.md +1 -1
- package/gsd-core/workflows/analyze-dependencies.md +2 -0
- package/gsd-core/workflows/audit-fix.md +2 -0
- package/gsd-core/workflows/audit-milestone.md +2 -0
- package/gsd-core/workflows/audit-uat.md +2 -0
- package/gsd-core/workflows/autonomous.md +2 -0
- package/gsd-core/workflows/check-todos.md +1 -1
- package/gsd-core/workflows/cleanup.md +1 -1
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +15 -13
- package/gsd-core/workflows/code-review-fix.md +2 -0
- package/gsd-core/workflows/code-review.md +73 -31
- package/gsd-core/workflows/complete-milestone.md +13 -4
- package/gsd-core/workflows/debug.md +1 -1
- package/gsd-core/workflows/diagnose-issues.md +5 -1
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/all.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/analyze.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/auto.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/batch.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/chain.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/default.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/power.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/text.md +2 -0
- package/gsd-core/workflows/discuss-phase/templates/context.md +2 -0
- package/gsd-core/workflows/discuss-phase/templates/discussion-log.md +2 -0
- package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
- package/gsd-core/workflows/discuss-phase-power.md +2 -0
- package/gsd-core/workflows/discuss-phase.md +1 -1
- package/gsd-core/workflows/do.md +43 -13
- package/gsd-core/workflows/docs-update.md +1 -1
- package/gsd-core/workflows/edit-phase.md +2 -0
- package/gsd-core/workflows/eval-review.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +2 -0
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +17 -1
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +8 -2
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -0
- package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +25 -0
- package/gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md +2 -0
- package/gsd-core/workflows/execute-phase.md +32 -14
- package/gsd-core/workflows/execute-plan.md +8 -8
- package/gsd-core/workflows/explore.md +2 -0
- package/gsd-core/workflows/extract-learnings.md +2 -0
- package/gsd-core/workflows/fast.md +6 -0
- package/gsd-core/workflows/forensics.md +2 -0
- package/gsd-core/workflows/graduation.md +1 -1
- package/gsd-core/workflows/health.md +1 -1
- package/gsd-core/workflows/help/modes/brief.md +2 -0
- package/gsd-core/workflows/help/modes/default.md +2 -0
- package/gsd-core/workflows/help/modes/full.md +12 -0
- package/gsd-core/workflows/help/modes/topic.md +2 -0
- package/gsd-core/workflows/help.md +2 -0
- package/gsd-core/workflows/import.md +3 -3
- package/gsd-core/workflows/inbox.md +1 -1
- package/gsd-core/workflows/ingest-docs.md +1 -1
- package/gsd-core/workflows/insert-phase.md +2 -0
- package/gsd-core/workflows/list-phase-assumptions.md +2 -0
- package/gsd-core/workflows/list-seeds.md +2 -0
- package/gsd-core/workflows/list-workspaces.md +2 -0
- package/gsd-core/workflows/manager.md +3 -3
- package/gsd-core/workflows/map-codebase.md +2 -0
- package/gsd-core/workflows/milestone-summary.md +2 -0
- package/gsd-core/workflows/mvp-phase.md +1 -1
- package/gsd-core/workflows/new-milestone.md +1 -1
- package/gsd-core/workflows/new-project.md +5 -3
- package/gsd-core/workflows/new-workspace.md +1 -1
- package/gsd-core/workflows/next.md +2 -0
- package/gsd-core/workflows/node-repair.md +2 -0
- package/gsd-core/workflows/note.md +2 -0
- package/gsd-core/workflows/onboard.md +1 -1
- package/gsd-core/workflows/pause-work.md +19 -4
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +100 -18
- package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +2 -0
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +9 -0
- package/gsd-core/workflows/plan-phase.md +130 -12
- package/gsd-core/workflows/plan-review-convergence.md +102 -10
- package/gsd-core/workflows/plant-seed.md +1 -1
- package/gsd-core/workflows/pr-branch.md +11 -3
- package/gsd-core/workflows/profile-user.md +1 -1
- package/gsd-core/workflows/progress/steps/forensic-audit.md +1 -1
- package/gsd-core/workflows/progress.md +25 -3
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +37 -2
- package/gsd-core/workflows/quick/steps/research-phase.md +3 -3
- package/gsd-core/workflows/quick-batch/steps/batch-init.md +55 -0
- package/gsd-core/workflows/quick-batch/steps/completion.md +65 -0
- package/gsd-core/workflows/quick-batch/steps/merge-wave.md +100 -0
- package/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md +147 -0
- package/gsd-core/workflows/quick-batch/steps/planner-wave.md +158 -0
- package/gsd-core/workflows/quick-batch/steps/research-phase.md +95 -0
- package/gsd-core/workflows/quick-batch/steps/resume-mode.md +49 -0
- package/gsd-core/workflows/quick-batch/steps/verification-wave.md +73 -0
- package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +169 -0
- package/gsd-core/workflows/quick-batch.md +203 -0
- package/gsd-core/workflows/quick.md +13 -3
- package/gsd-core/workflows/reapply-patches.md +2 -0
- package/gsd-core/workflows/remove-phase.md +2 -0
- package/gsd-core/workflows/remove-workspace.md +1 -1
- package/gsd-core/workflows/resume-project.md +6 -2
- package/gsd-core/workflows/review.md +215 -10
- package/gsd-core/workflows/scan.md +2 -0
- package/gsd-core/workflows/section-manifest.json +12 -0
- package/gsd-core/workflows/secure-phase.md +1 -1
- package/gsd-core/workflows/session-report.md +2 -0
- package/gsd-core/workflows/settings-advanced.md +2 -0
- package/gsd-core/workflows/settings-integrations.md +9 -8
- package/gsd-core/workflows/settings.md +1 -1
- package/gsd-core/workflows/ship.md +10 -10
- package/gsd-core/workflows/sketch-wrap-up.md +2 -0
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/smart-entry.md +1 -1
- package/gsd-core/workflows/spec-phase.md +24 -19
- package/gsd-core/workflows/spike-wrap-up.md +2 -0
- package/gsd-core/workflows/spike.md +1 -1
- package/gsd-core/workflows/stats.md +2 -0
- package/gsd-core/workflows/sync-skills.md +12 -4
- package/gsd-core/workflows/thread.md +2 -0
- package/gsd-core/workflows/transition.md +2 -0
- package/gsd-core/workflows/ui-phase.md +26 -5
- package/gsd-core/workflows/ui-review.md +1 -1
- package/gsd-core/workflows/ultraplan-phase.md +2 -0
- package/gsd-core/workflows/undo.md +1 -1
- package/gsd-core/workflows/update.md +41 -38
- package/gsd-core/workflows/validate-phase.md +1 -1
- package/gsd-core/workflows/verify-work.md +49 -3
- package/hooks/dist/gsd-check-update-worker.js +19 -2
- package/hooks/dist/gsd-context-monitor.js +283 -12
- package/hooks/dist/gsd-node-runner.sh +1 -0
- package/hooks/dist/gsd-prompt-guard.js +30 -5
- package/hooks/dist/gsd-read-guard.js +2 -0
- package/hooks/dist/gsd-read-injection-scanner.js +5 -5
- package/hooks/dist/gsd-secret-read-guard.js +1079 -0
- package/hooks/dist/gsd-statusline.js +7 -3
- package/hooks/dist/gsd-validate-commit.sh +444 -7
- package/hooks/dist/gsd-workflow-guard.js +2 -1
- package/hooks/dist/lib/git-cmd.js +210 -1
- package/hooks/dist/lib/injection-patterns.js +36 -6
- package/hooks/dist/managed-hooks-registry.cjs +1 -0
- package/hooks/gsd-check-update-worker.js +19 -2
- package/hooks/gsd-context-monitor.js +283 -12
- package/hooks/gsd-node-runner.sh +1 -0
- package/hooks/gsd-prompt-guard.js +30 -5
- package/hooks/gsd-read-guard.js +2 -0
- package/hooks/gsd-read-injection-scanner.js +5 -5
- package/hooks/gsd-secret-read-guard.js +1079 -0
- package/hooks/gsd-statusline.js +7 -3
- package/hooks/gsd-validate-commit.sh +444 -7
- package/hooks/gsd-workflow-guard.js +2 -1
- package/hooks/hooks.json +6 -0
- package/hooks/lib/git-cmd.js +210 -1
- package/hooks/lib/injection-patterns.js +36 -6
- package/hooks/managed-hooks-registry.cjs +1 -0
- package/package.json +5 -5
- package/scripts/build-hooks.js +11 -4
- package/scripts/ci-test-scope.cjs +7 -0
- package/scripts/docs-guard-registry.cjs +10 -0
- package/scripts/gen-loop-host-contract.cjs +67 -15
- package/scripts/lib/shellcheck-fetch.cjs +247 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -6
- package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +1 -1
- package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +5 -0
- package/scripts/lint-phase-enumeration-drift.cjs +24 -6
- package/scripts/lint-phase-id-drift.cjs +133 -8
- package/scripts/lint-portable-grep.cjs +176 -0
- package/scripts/lint-response-language-coverage.cjs +524 -0
- package/scripts/lint-test-file-count.allowlist.json +3 -1
- package/scripts/lint-workflow-shellcheck-baseline.json +1027 -0
- package/scripts/lint-workflow-shellcheck.cjs +614 -0
- package/scripts/npm-audit-baseline.cjs +376 -0
- package/scripts/prompt-injection-scan.sh +8 -0
- package/scripts/require-issue-link-policy.cjs +16 -1
- package/skills/gsd-execute-phase/SKILL.md +1 -1
- package/skills/gsd-ns-workflow/SKILL.md +1 -0
- package/skills/gsd-phase/SKILL.md +1 -1
- package/skills/gsd-quick-batch/SKILL.md +105 -0
- package/skills/gsd-surface/SKILL.md +18 -8
- package/vscode/package.json +1 -1
|
@@ -75,7 +75,9 @@ const { readVerificationStatus } = verificationMod;
|
|
|
75
75
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- plan-dependency-graph.cjs is an export= CommonJS module
|
|
76
76
|
const planDependencyGraphMod = require("./plan-dependency-graph.cjs");
|
|
77
77
|
const { computeHaltPropagation, buildSummaryFileIndex, isSummaryFileHalted, isSummaryFileBlocked } = planDependencyGraphMod;
|
|
78
|
-
|
|
78
|
+
// #612: `resolvePhaseIdConvention` selects the write-time milestone-scope
|
|
79
|
+
// guard's terminator vocabulary (see assertDescriptionPreservesMilestoneScope).
|
|
80
|
+
const { planningDir, withPlanningLock, listAvailableWorkstreams, peekActiveWorkstream, diagnoseUnresolvedActiveWorkstream, describeUnresolvedWorkstreamReason, resolvePhaseIdConvention, } = planningWorkspace;
|
|
79
81
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- milestone-lock.cjs is an export= CommonJS module
|
|
80
82
|
const milestoneLockMod = require("./milestone-lock.cjs");
|
|
81
83
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
@@ -385,6 +387,57 @@ function cmdPhaseMvpMode(cwd, args, raw) {
|
|
|
385
387
|
cli_flag_present: cliFlagPresent,
|
|
386
388
|
}, raw);
|
|
387
389
|
}
|
|
390
|
+
/**
|
|
391
|
+
* `phase.tdd-applicable <plan-file> [--cli-flag]` (#4273, Phase 1 of epic
|
|
392
|
+
* #4272) — resolves whether the TDD RED/GREEN/REFACTOR gate applies to a
|
|
393
|
+
* given plan, in strict precedence order: an explicit `--cli-flag` wins over
|
|
394
|
+
* the plan's own `type: tdd` frontmatter, which wins over any task in the
|
|
395
|
+
* plan carrying `tdd="true"` (the #4265 mixed-mode shape), which wins over
|
|
396
|
+
* the project-wide `workflow.tdd_mode` config default. Mirrors
|
|
397
|
+
* `cmdPhaseMvpMode`'s precedence-cascade shape immediately above.
|
|
398
|
+
*/
|
|
399
|
+
function cmdPhaseTddApplicable(cwd, args, raw) {
|
|
400
|
+
const planPath = args[0];
|
|
401
|
+
if (!planPath) {
|
|
402
|
+
error('Usage: phase.tdd-applicable <plan-file> [--cli-flag]', ERROR_REASON.USAGE);
|
|
403
|
+
}
|
|
404
|
+
const resolvedPath = node_path_1.default.isAbsolute(planPath) ? planPath : node_path_1.default.join(cwd, planPath);
|
|
405
|
+
if (!node_fs_1.default.existsSync(resolvedPath)) {
|
|
406
|
+
error(`Plan file not found: ${planPath}`, ERROR_REASON.PHASE_NOT_FOUND);
|
|
407
|
+
}
|
|
408
|
+
const cliFlagPresent = args.includes('--cli-flag');
|
|
409
|
+
const content = node_fs_1.default.readFileSync(resolvedPath, 'utf-8');
|
|
410
|
+
const doc = parsePlanDocument(content, resolvedPath);
|
|
411
|
+
const planType = doc.type;
|
|
412
|
+
const taskTddAttribute = doc.tasks.some((t) => t.tdd === 'true');
|
|
413
|
+
const config = loadConfig(cwd);
|
|
414
|
+
const configTddMode = Boolean(config.tdd_mode);
|
|
415
|
+
let applicable = false;
|
|
416
|
+
let source = 'none';
|
|
417
|
+
if (cliFlagPresent) {
|
|
418
|
+
applicable = true;
|
|
419
|
+
source = 'cli_flag';
|
|
420
|
+
}
|
|
421
|
+
else if (planType === 'tdd') {
|
|
422
|
+
applicable = true;
|
|
423
|
+
source = 'plan_frontmatter';
|
|
424
|
+
}
|
|
425
|
+
else if (taskTddAttribute) {
|
|
426
|
+
applicable = true;
|
|
427
|
+
source = 'task_attribute';
|
|
428
|
+
}
|
|
429
|
+
else if (configTddMode) {
|
|
430
|
+
applicable = true;
|
|
431
|
+
source = 'config';
|
|
432
|
+
}
|
|
433
|
+
output({
|
|
434
|
+
applicable,
|
|
435
|
+
source,
|
|
436
|
+
plan_type: planType,
|
|
437
|
+
config_tdd_mode: configTddMode,
|
|
438
|
+
cli_flag_present: cliFlagPresent,
|
|
439
|
+
}, raw);
|
|
440
|
+
}
|
|
388
441
|
function cmdFindPhase(cwd, phase, raw) {
|
|
389
442
|
if (!phase) {
|
|
390
443
|
error('phase identifier required');
|
|
@@ -968,16 +1021,36 @@ function phaseEntryInsertOffset(rawContent, cwd) {
|
|
|
968
1021
|
* the edit-phase workflow's depends_on gate. The predicate itself
|
|
969
1022
|
* (`findMilestoneScopeHeadingLines`) is fence-aware and Phase-heading-exempt,
|
|
970
1023
|
* so ordinary descriptions and the phase's own numbered heading never trip it.
|
|
1024
|
+
*
|
|
1025
|
+
* #612: the predicate is convention-SELECTED, because the terminator
|
|
1026
|
+
* vocabulary it mirrors is. On an opted-in bracket repo the ADR-canonical
|
|
1027
|
+
* `## [GSD.09] Hidden` carries none of the markers listed above and yet
|
|
1028
|
+
* terminates the window, so the blind call accepted the exact description the
|
|
1029
|
+
* guard exists to reject — measured at this CLI seam, two `phase add` calls,
|
|
1030
|
+
* the second phase silently outside the milestone phase set. Resolved through
|
|
1031
|
+
* the same tolerant shape the read path uses (`planningDir` throws on a
|
|
1032
|
+
* poisoned `GSD_PROJECT`/`GSD_WORKSTREAM` segment, and this guard runs BEFORE
|
|
1033
|
+
* `loadConfig` and the ROADMAP existence check — an unresolvable convention
|
|
1034
|
+
* must degrade to the pre-existing legacy vocabulary, never turn a rejection
|
|
1035
|
+
* into a crash).
|
|
971
1036
|
*/
|
|
972
|
-
function assertDescriptionPreservesMilestoneScope(description, command) {
|
|
973
|
-
|
|
1037
|
+
function assertDescriptionPreservesMilestoneScope(cwd, description, command) {
|
|
1038
|
+
let convention = null;
|
|
1039
|
+
try {
|
|
1040
|
+
convention = resolvePhaseIdConvention(cwd);
|
|
1041
|
+
}
|
|
1042
|
+
catch { /* unresolvable convention → treat as not-configured (base behaviour) */ }
|
|
1043
|
+
const offending = findMilestoneScopeHeadingLines(description, convention);
|
|
974
1044
|
if (offending.length === 0)
|
|
975
1045
|
return;
|
|
1046
|
+
const markerList = convention === 'bracket'
|
|
1047
|
+
? `(a vN.N version token, a ✅/📋/🚧/🔄 marker, the word "Milestone", or — under the bracket convention — a "[CODE.NN] Name" milestone heading)`
|
|
1048
|
+
: `(a vN.N version token, a ✅/📋/🚧/🔄 marker, or the word "Milestone")`;
|
|
976
1049
|
error(`${command}: description contains a milestone-scoping heading line — writing it to ROADMAP.md would terminate ` +
|
|
977
1050
|
`the current milestone window and silently drop later phases out of the milestone scope. ` +
|
|
978
1051
|
`Offending line(s): ${offending.map((line) => JSON.stringify(line)).join(', ')}. ` +
|
|
979
1052
|
`Rewrite the line so it is not a level 1-3 "#" heading carrying a milestone marker ` +
|
|
980
|
-
|
|
1053
|
+
markerList + `.`);
|
|
981
1054
|
}
|
|
982
1055
|
/**
|
|
983
1056
|
* #3849 — widen "used phase numbers" beyond this checkout. Every sibling git
|
|
@@ -1052,7 +1125,7 @@ function cmdPhaseAdd(cwd, description, raw, customId) {
|
|
|
1052
1125
|
if (!description) {
|
|
1053
1126
|
error('description required for phase add');
|
|
1054
1127
|
}
|
|
1055
|
-
assertDescriptionPreservesMilestoneScope(description, 'phase add');
|
|
1128
|
+
assertDescriptionPreservesMilestoneScope(cwd, description, 'phase add');
|
|
1056
1129
|
const config = loadConfig(cwd);
|
|
1057
1130
|
const roadmapPath = node_path_1.default.join(planningDir(cwd), 'ROADMAP.md');
|
|
1058
1131
|
if (!node_fs_1.default.existsSync(roadmapPath)) {
|
|
@@ -1169,7 +1242,7 @@ function cmdPhaseAddBatch(cwd, descriptions, raw) {
|
|
|
1169
1242
|
// all-or-nothing, so one offending description must reject the whole batch
|
|
1170
1243
|
// with no ROADMAP write and no phase directories created.
|
|
1171
1244
|
for (const description of descriptions) {
|
|
1172
|
-
assertDescriptionPreservesMilestoneScope(description, 'phase add-batch');
|
|
1245
|
+
assertDescriptionPreservesMilestoneScope(cwd, description, 'phase add-batch');
|
|
1173
1246
|
}
|
|
1174
1247
|
const config = loadConfig(cwd);
|
|
1175
1248
|
const roadmapPath = node_path_1.default.join(planningDir(cwd), 'ROADMAP.md');
|
|
@@ -1274,7 +1347,7 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
|
|
|
1274
1347
|
if (!afterPhase || !description) {
|
|
1275
1348
|
error('after-phase and description required for phase insert');
|
|
1276
1349
|
}
|
|
1277
|
-
assertDescriptionPreservesMilestoneScope(description, 'phase insert');
|
|
1350
|
+
assertDescriptionPreservesMilestoneScope(cwd, description, 'phase insert');
|
|
1278
1351
|
const roadmapPath = node_path_1.default.join(planningDir(cwd), 'ROADMAP.md');
|
|
1279
1352
|
if (!node_fs_1.default.existsSync(roadmapPath)) {
|
|
1280
1353
|
error('ROADMAP.md not found');
|
|
@@ -2054,6 +2127,640 @@ function phaseDisplayNameFromSlug(slug) {
|
|
|
2054
2127
|
const name = slug.replace(/-/g, ' ').trim();
|
|
2055
2128
|
return name || null;
|
|
2056
2129
|
}
|
|
2130
|
+
// A range operator, enumerated. CENSUS (round 3): the domain is "separator
|
|
2131
|
+
// spellings an author can put between two REQ-IDs", which is open, so the
|
|
2132
|
+
// enumeration draws a boundary rather than covering it. Reached: ASCII `..`+,
|
|
2133
|
+
// the seven Unicode dashes that are the SAME operator at different codepoints
|
|
2134
|
+
// (U+2010 hyphen, U+2011 non-breaking hyphen, U+2012 figure dash, U+2013 en,
|
|
2135
|
+
// U+2014 em, U+2015 horizontal bar, U+2212 minus) plus ASCII `-`, U+2026
|
|
2136
|
+
// ellipsis, and the words `to`/`thru`/`through`. NOT reached, and the
|
|
2137
|
+
// consequence is a silent under-selection — #3697's own defect — for that
|
|
2138
|
+
// spelling: `→`, `~`, `..=`, `..<`, `until`, and `up to` (two tokens, so it
|
|
2139
|
+
// cannot be one operator token at all). Those stay out deliberately: each is a
|
|
2140
|
+
// symbol or word with an independent non-range use between two IDs, which is
|
|
2141
|
+
// the over-warning class #2334 cost three rounds. The Unicode dashes DO carry
|
|
2142
|
+
// the ASCII hyphen's date/sub-number collision — an earlier round-3 commit
|
|
2143
|
+
// claimed they did not, and was wrong — so they take the strict arm with it;
|
|
2144
|
+
// see the rule below.
|
|
2145
|
+
const REQ_RANGE_DASHES = '\\u2010\\u2011\\u2012\\u2013\\u2014\\u2015\\u2212';
|
|
2146
|
+
// EVERY DASH IS STRICT — one rule, whatever the codepoint. `PREFIX-\d+ <dash>
|
|
2147
|
+
// \d+` is also a date (`FY-2026-08`) and a sub-numbered ID (`API-2-01`), and
|
|
2148
|
+
// that ambiguity is a property of the SHAPE, not of which dash key was pressed.
|
|
2149
|
+
// The design already chose strictness for ASCII `-` on exactly this trade: a
|
|
2150
|
+
// bare-hyphen tight range must carry a full ID on BOTH sides. Until round 3 the
|
|
2151
|
+
// other dashes sat in the loose arm, so `RANGE-01 (target FY-2026<en-dash>08)`
|
|
2152
|
+
// warned while its all-ASCII twin — pinned silent by #3697-4 — did not. That
|
|
2153
|
+
// inconsistency predates this PR for U+2013/U+2014; round 3 briefly widened it
|
|
2154
|
+
// to five more codepoints before this commit closed it for all seven.
|
|
2155
|
+
// The cost is symmetric and already accepted: `RANGE-01, RANGE-02<dash>05`
|
|
2156
|
+
// goes silent, exactly as `RANGE-01, RANGE-02-05` already does today. A bare
|
|
2157
|
+
// `RANGE-02<dash>05` still warns — it selects nothing, so R3 catches it.
|
|
2158
|
+
// LOOSE stays loose: `..`, `…` and the word operators have no date or
|
|
2159
|
+
// sub-number reading between two numbers, so they keep the numeric endpoint.
|
|
2160
|
+
const REQ_RANGE_OP = `(?:\\.{2,}|\\u2026|[${REQ_RANGE_DASHES}]|-|to|thru|through)`;
|
|
2161
|
+
const REQ_RANGE_OP_LOOSE = `(?:\\.{2,}|\\u2026|to|thru|through)`;
|
|
2162
|
+
const REQ_RANGE_OP_SYMBOL = `(?:\\.{2,}|\\u2026|[${REQ_RANGE_DASHES}]|-)`;
|
|
2163
|
+
const REQ_RANGE_TOKEN_RE = new RegExp(`^([A-Z][A-Z0-9]*)-(?:\\d+)\\s*(?:${REQ_RANGE_OP_LOOSE}\\s*(?:\\1-)?|[-${REQ_RANGE_DASHES}]\\s*\\1-)\\d+$`, 'i');
|
|
2164
|
+
const REQ_PURE_RANGE_OP_RE = new RegExp(`^${REQ_RANGE_OP}$`, 'i');
|
|
2165
|
+
const REQ_GLUED_RANGE_LEAD_RE = new RegExp(`^${REQ_RANGE_OP_SYMBOL}([A-Z][A-Z0-9]*-\\d+)$`, 'i');
|
|
2166
|
+
const REQ_GLUED_RANGE_TRAIL_RE = new RegExp(`^([A-Z][A-Z0-9]*-\\d+)${REQ_RANGE_OP}$`, 'i');
|
|
2167
|
+
const REQ_ID_SUBSTRING_RE = /[A-Z][A-Z0-9]*-\d+/i;
|
|
2168
|
+
const REQ_ID_SHAPE_RE = /^[A-Z][A-Z0-9]*-\d+$/i;
|
|
2169
|
+
const REQ_ID_PARTS_RE = /^([A-Z][A-Z0-9]*)-(\d+)$/i;
|
|
2170
|
+
// `LETTERS-\d+-\d+` — a date (`FY-2026-08`) or a sub-numbered ID (`API-2-01`).
|
|
2171
|
+
// REQ_RANGE_TOKEN_RE's strict-dash arm exists precisely to keep this shape
|
|
2172
|
+
// silent, because nothing at token level can tell the three readings apart.
|
|
2173
|
+
// Round 4 review Minor 2: the skipped-text rider re-reported it through the
|
|
2174
|
+
// side door — `REQ_ID_SUBSTRING_RE` is unanchored, so `FY-2026-08` matches as
|
|
2175
|
+
// `FY-2026` and landed in `unselectedIdShaped`. Whenever any OTHER rule fired
|
|
2176
|
+
// on a line carrying a date annotation, the warning then told the author to
|
|
2177
|
+
// "check whether any of it is a requirement" about a date. Not a false
|
|
2178
|
+
// warning — the line was warning anyway — but false CONTENT, and it is the
|
|
2179
|
+
// #2334 voice.
|
|
2180
|
+
// `PREFIX-<digits>-<digits>` — the shape the strict-dash range rule refuses to
|
|
2181
|
+
// act on because it is equally a date (`FY-2026-08`) and a sub-numbered id
|
|
2182
|
+
// (`API-2-01`). NO regex separates those: `API-2026-08` is a legal requirement
|
|
2183
|
+
// id and `FY-26-08` is a date, and both filters that tried scored a miss in
|
|
2184
|
+
// each direction under the pre-push review's continuation.
|
|
2185
|
+
//
|
|
2186
|
+
// So the rider stops adjudicating and starts DISCLOSING. Round 4 Minor 2's
|
|
2187
|
+
// real complaint was that the rider told the author to check whether a DATE
|
|
2188
|
+
// was a requirement; the fix is to name the ambiguity rather than to guess at
|
|
2189
|
+
// it — which is the same thing the two warning voices already do about a
|
|
2190
|
+
// range separator.
|
|
2191
|
+
const REQ_AMBIGUOUS_NUMERIC_RE = /^[A-Z][A-Z0-9]*(?:-\d+){2,}$/i;
|
|
2192
|
+
// The token-length cap. It bounds REQ_ID_SUBSTRING_RE, the one UNANCHORED
|
|
2193
|
+
// regex here, which backtracks quadratically on a pathological token. Round 3
|
|
2194
|
+
// review Nit 6 objected that the anchored regexes were left uncapped on the
|
|
2195
|
+
// strength of a comment asserting they scan linearly; they are applied through
|
|
2196
|
+
// the same cap now, so the claim is enforced rather than asserted. No real
|
|
2197
|
+
// REQ-ID-carrying token approaches this bound.
|
|
2198
|
+
const REQ_TOKEN_SCAN_LIMIT = 2048;
|
|
2199
|
+
/**
|
|
2200
|
+
* The Requirements-line warning KINDS, as a stable machine vocabulary (round 4
|
|
2201
|
+
* review Major 3).
|
|
2202
|
+
*
|
|
2203
|
+
* Before this, the kind existed only in the prose of the message, so every
|
|
2204
|
+
* consumer and every test had to regex an English sentence — and rewording a
|
|
2205
|
+
* message silently un-asserted the tests that pinned it. The repo already had
|
|
2206
|
+
* the settled seam for exactly these semantics: `diffLiveConfig` emits
|
|
2207
|
+
* `kind:'unverified'` for a truncated scan (`CONTEXT.md`), and
|
|
2208
|
+
* `WAVE_CLEANUP_WARNING` carries codes in `src/worktree-safety.cts`.
|
|
2209
|
+
*
|
|
2210
|
+
* Carried ALONGSIDE the prose, never instead of it. `warnings[]` is a
|
|
2211
|
+
* documented `string[]` in `phase complete`'s JSON output, rendered by
|
|
2212
|
+
* execute-phase.md's "If has_warnings is true" step, so changing its element
|
|
2213
|
+
* shape would be a breaking output-contract change for a shipped command. The
|
|
2214
|
+
* code is emitted as its own additive `requirements_line_warning` field.
|
|
2215
|
+
*/
|
|
2216
|
+
const REQ_LINE_WARNING_CODE = {
|
|
2217
|
+
/** ID-shaped content was demonstrably not selected — the line failed to parse. */
|
|
2218
|
+
misparse: 'req-line-misparse',
|
|
2219
|
+
/** A range READING is at stake; every endpoint the rule fired on was selected. */
|
|
2220
|
+
rangeReading: 'req-line-range-reading',
|
|
2221
|
+
/** A token past the scan cap means the line was not classified — never that it is clean. */
|
|
2222
|
+
unverified: 'req-line-unverified',
|
|
2223
|
+
};
|
|
2224
|
+
// R4 — a full ID with a trailing statement delimiter glued to it. ANCHORED on
|
|
2225
|
+
// both ends, so it is linear and needs no cap of its own beyond the token
|
|
2226
|
+
// length guard its caller applies.
|
|
2227
|
+
// Zero-width, bidi-control, joiner and variation-selector codepoints. INVISIBLE
|
|
2228
|
+
// to the author, and the pre-push review's continuation drove the consequence
|
|
2229
|
+
// from both sides: a line of only these warned with nothing on screen to
|
|
2230
|
+
// explain it, AND stripping them wholesale from the detector made
|
|
2231
|
+
// `REQ-01<ZWSP>, REQ-02` go SILENT while the selector really did drop REQ-01 —
|
|
2232
|
+
// #3697's own defect, introduced by the fix for its mirror image. So they are
|
|
2233
|
+
// never stripped from the line: they are DECORATION on a token (R4 below) and
|
|
2234
|
+
// absence-of-content for the empty test (visibleContent), which are two
|
|
2235
|
+
// different questions about the same character.
|
|
2236
|
+
const REQ_INVISIBLE_RE = /[\u00AD\u200B-\u200F\u2060-\u2064\u2066-\u2069\uFE0F\uFEFF]/g;
|
|
2237
|
+
// The wrappers R4 shaves. Emphasis, quotes and backticks, because the SELECTOR
|
|
2238
|
+
// shaves none of them — `**REQ-01**` is genuinely not selected and is a real,
|
|
2239
|
+
// silent drop.
|
|
2240
|
+
//
|
|
2241
|
+
// PARENTHESES ARE DELIBERATELY ABSENT, and this is load-bearing. A parenthesis
|
|
2242
|
+
// is this rule's citation MARKER, not decoration to shave: `(REQ-02)` and
|
|
2243
|
+
// `(ADR-7)` are the same shape and the rule declines both. Including them here
|
|
2244
|
+
// made `REQ-01, (REQ-02), REQ-03 — REQ-05` report a glued delimiter that was
|
|
2245
|
+
// never there, and broke #3697-9d's channel routing with it — caught by the
|
|
2246
|
+
// suite immediately after the widening.
|
|
2247
|
+
const REQ_WRAPPER_RE = /^["'`*_~“”‘’]+|["'`*_~“”‘’]+$/g;
|
|
2248
|
+
// An id with a list delimiter glued to EITHER end, once styling is removed.
|
|
2249
|
+
// The capture is the bare id; a match means the delimiter was ADJACENT to it.
|
|
2250
|
+
const REQ_DELIMITED_ID_RE = /^[;:]*([A-Z][A-Z0-9]*-\d+)[;:]*$/i;
|
|
2251
|
+
/**
|
|
2252
|
+
* CENSUS (round 4): the domain is "separators an author writes between two
|
|
2253
|
+
* REQ-IDs INSTEAD of a comma" — distinct from the range-operator domain
|
|
2254
|
+
* censused above, and it had no census at all before this round.
|
|
2255
|
+
*
|
|
2256
|
+
* ROUND 4'S CENSUS WAS WRONG, AND THE WAY IT WAS WRONG IS THE LESSON. It swept
|
|
2257
|
+
* 26 spellings and concluded "exactly two — `; ` and `: `". It reached that
|
|
2258
|
+
* answer because it swept the ONE-SIDED form (`REQ-01; REQ-02`) for the
|
|
2259
|
+
* semicolon and colon, and only the BARE and SYMMETRIC forms (`|`, ` | `) for
|
|
2260
|
+
* every other separator. Different members of the domain were tested in
|
|
2261
|
+
* different shapes, so the conclusion could not have come out any other way.
|
|
2262
|
+
*
|
|
2263
|
+
* Re-swept round 5, fully crossed: 21 separators x {bare, trailing-space,
|
|
2264
|
+
* leading-space, both-spaces} = 84 combinations, driven through the built
|
|
2265
|
+
* artifact. 26 select both IDs, 24 under-select and already warn, and
|
|
2266
|
+
* 34 UNDER-SELECT SILENTLY. All 34 are the same shape — a separator glued to
|
|
2267
|
+
* exactly ONE of the two IDs, e.g. `REQ-01/ REQ-02` or `REQ-01 /REQ-02` — for
|
|
2268
|
+
* every punctuation except `,` (the real delimiter) and `;` / `:` (R4).
|
|
2269
|
+
* Measured silent: | / + & \ > . ! ? • · ؛ ; , - ~ and the word operators
|
|
2270
|
+
* `and` / `plus` in trailing-space form.
|
|
2271
|
+
*
|
|
2272
|
+
* So the honest statement is that R4 covers TWO CHARACTERS of a domain that is
|
|
2273
|
+
* wide open, not that the domain has two members. The round-4 review
|
|
2274
|
+
* hand-listed the semicolon; the colon is its sibling and fails identically;
|
|
2275
|
+
* everything else in that list is disclosed here and NOT caught. Widening the
|
|
2276
|
+
* delimiter class is a small change and deliberately not made at the end of a
|
|
2277
|
+
* round: three successive cuts of this rule fired on a citation.
|
|
2278
|
+
*
|
|
2279
|
+
* THE GATE IS ADJACENCY, and it is the part to read. Styling is stripped, then
|
|
2280
|
+
* the delimiter must be touching the id: `REQ-01;`, `;REQ-02`, `**REQ-01;**`
|
|
2281
|
+
* and the backticked form all qualify. `**REQ-01**;` does NOT — outside the
|
|
2282
|
+
* styling a `;` is sentence punctuation, which is why `REQ-01, see **REQ-7**;
|
|
2283
|
+
* next topic` is a citation and not a drop. An INVISIBLE anywhere in the token
|
|
2284
|
+
* qualifies without an adjacency test, because nobody types one on purpose, so
|
|
2285
|
+
* it is corruption rather than intent.
|
|
2286
|
+
*
|
|
2287
|
+
* Markdown styling on its own is NOT a trigger and NOT reported. It reaches
|
|
2288
|
+
* the skipped-text rider, which names the id without asserting a drop — but a
|
|
2289
|
+
* rider only exists inside a MESSAGE, and a message only exists when some rule
|
|
2290
|
+
* set `warn`. On a line where nothing else fires, `REQ-01, **REQ-02**` is
|
|
2291
|
+
* wholly silent. Saying it is "left to the rider" reads as coverage and is
|
|
2292
|
+
* not; #3697-19m pins the silence so this comment cannot drift back.
|
|
2293
|
+
*
|
|
2294
|
+
* NOT reached, stated rather than fixed, and the second member is WIDER than
|
|
2295
|
+
* this comment first claimed:
|
|
2296
|
+
* - anything inside a parenthetical. A parenthesis is this rule's citation
|
|
2297
|
+
* MARKER, never decoration to shave — `(REQ-02)` and `(ADR-7)` are the
|
|
2298
|
+
* same shape and the rule declines both.
|
|
2299
|
+
* - a decorated id whose prefix is on NO selected id: `REQ-01, FOO-02: x`
|
|
2300
|
+
* stays silent even when FOO-02 is real. Prefix agreement is what
|
|
2301
|
+
* separates a drop from a bare citation — `REQ-01, see ADR-7: section 3`
|
|
2302
|
+
* carries `ADR-7:` in exactly `REQ-01;`'s shape — and it is the module's
|
|
2303
|
+
* own idiom, not a new heuristic (reqEndpointsImplyInterior already
|
|
2304
|
+
* requires an agreeing prefix). The gate is NOT complete: a citation that
|
|
2305
|
+
* DOES share a selected prefix (`ADR-01, see ADR-7: sec 3`) still fires,
|
|
2306
|
+
* and nothing at token level separates that from a real drop. Saying so is
|
|
2307
|
+
* the honest position; a prose heuristic on "see" is exactly the free-text
|
|
2308
|
+
* detector this module exists to avoid.
|
|
2309
|
+
* The trade, plainly: an under-report on a rare shape over an over-report on a
|
|
2310
|
+
* common one — the same call the strict-dash rule makes.
|
|
2311
|
+
*/
|
|
2312
|
+
function reqDelimiterDroppedIds(rawLine, selected, cap) {
|
|
2313
|
+
// MATCHED parenthetical spans are removed OUTRIGHT, not tracked as a depth.
|
|
2314
|
+
//
|
|
2315
|
+
// Two bugs died here. A running depth counter let an unbalanced `(` stay open
|
|
2316
|
+
// to end-of-line and swallow every real drop after it. Promoting a whole
|
|
2317
|
+
// token to immune because it CONTAINED a matched character then leaked the
|
|
2318
|
+
// other way: `REQ-01, REQ-02;(note) REQ-03` is one whitespace token, so the
|
|
2319
|
+
// parenthetical conferred immunity on the `REQ-02;` sitting outside it.
|
|
2320
|
+
// Deleting the span states what is actually meant — for this rule a citation
|
|
2321
|
+
// is not on the line — while an UNMATCHED paren is a typo and confers
|
|
2322
|
+
// nothing.
|
|
2323
|
+
//
|
|
2324
|
+
// Square brackets go too, exactly as the SELECTOR strips them: `[REQ-01;
|
|
2325
|
+
// REQ-02]` is the documented form and was silently dropping REQ-01.
|
|
2326
|
+
//
|
|
2327
|
+
// INVISIBLES STAY. They are the evidence this rule reads; the tokenizer
|
|
2328
|
+
// strips them for the classification rules, and the two sites answer two
|
|
2329
|
+
// different questions about the same character.
|
|
2330
|
+
const chars = [...String(rawLine).replace(/<!--[\s\S]*?-->/g, ' ')];
|
|
2331
|
+
const openStack = [];
|
|
2332
|
+
for (let i = 0; i < chars.length; i += 1) {
|
|
2333
|
+
if (chars[i] === '(')
|
|
2334
|
+
openStack.push(i);
|
|
2335
|
+
else if (chars[i] === ')' && openStack.length > 0) {
|
|
2336
|
+
const open = openStack.pop();
|
|
2337
|
+
for (let j = open; j <= i; j += 1)
|
|
2338
|
+
chars[j] = ' ';
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
const line = chars.join('').replace(/[[\]]/g, '');
|
|
2342
|
+
// The prefixes actually SELECTED on this line. A dropped id must agree with
|
|
2343
|
+
// one of them — that is what separates a delimiter typo from a citation,
|
|
2344
|
+
// since `REQ-01, see ADR-7: sec 3` carries `ADR-7:` in exactly `REQ-01;`'s
|
|
2345
|
+
// shape. Same-prefix agreement is the module's own idiom, not a new
|
|
2346
|
+
// heuristic (see reqEndpointsImplyInterior).
|
|
2347
|
+
const selectedPrefixes = new Set();
|
|
2348
|
+
for (const id of selected) {
|
|
2349
|
+
const m = REQ_ID_PARTS_RE.exec(id);
|
|
2350
|
+
if (m)
|
|
2351
|
+
selectedPrefixes.add(m[1].toUpperCase());
|
|
2352
|
+
}
|
|
2353
|
+
const hits = [];
|
|
2354
|
+
for (const raw of line.split(/[,\s]+/)) {
|
|
2355
|
+
if (!raw || raw.length > cap)
|
|
2356
|
+
continue;
|
|
2357
|
+
// Strip STYLING only. What survives is the id plus whatever was glued
|
|
2358
|
+
// directly to it.
|
|
2359
|
+
const core = raw.replace(REQ_INVISIBLE_RE, '').replace(REQ_WRAPPER_RE, '');
|
|
2360
|
+
const m = REQ_DELIMITED_ID_RE.exec(core);
|
|
2361
|
+
if (!m)
|
|
2362
|
+
continue;
|
|
2363
|
+
const bare = m[1];
|
|
2364
|
+
// ADJACENCY IS THE WHOLE RULE. A `;`/`:` touching the id is a list
|
|
2365
|
+
// separator someone meant; the same character OUTSIDE the styling is
|
|
2366
|
+
// sentence punctuation — `see **REQ-7**; next topic` cites a requirement
|
|
2367
|
+
// while `**REQ-01;** REQ-02` fails to list one, and only the delimiter's
|
|
2368
|
+
// POSITION separates them. An INVISIBLE needs no adjacency test: nobody
|
|
2369
|
+
// types one on purpose, so anywhere in the token it is corruption rather
|
|
2370
|
+
// than intent.
|
|
2371
|
+
const hadAdjacentDelimiter = core !== bare;
|
|
2372
|
+
REQ_INVISIBLE_RE.lastIndex = 0;
|
|
2373
|
+
const hadInvisible = REQ_INVISIBLE_RE.test(raw);
|
|
2374
|
+
REQ_INVISIBLE_RE.lastIndex = 0;
|
|
2375
|
+
if (!hadAdjacentDelimiter && !hadInvisible)
|
|
2376
|
+
continue;
|
|
2377
|
+
if (selected.has(bare.toUpperCase()))
|
|
2378
|
+
continue;
|
|
2379
|
+
const parts = REQ_ID_PARTS_RE.exec(bare);
|
|
2380
|
+
if (parts && selectedPrefixes.has(parts[1].toUpperCase()))
|
|
2381
|
+
hits.push(bare);
|
|
2382
|
+
}
|
|
2383
|
+
return [...new Set(hits)];
|
|
2384
|
+
}
|
|
2385
|
+
/** Endpoints imply a dropped interior only on an AGREEING prefix and a gap > 1. */
|
|
2386
|
+
function reqEndpointsImplyInterior(a, b) {
|
|
2387
|
+
const ma = REQ_ID_PARTS_RE.exec(a);
|
|
2388
|
+
const mb = REQ_ID_PARTS_RE.exec(b);
|
|
2389
|
+
if (!ma || !mb)
|
|
2390
|
+
return false;
|
|
2391
|
+
if (ma[1].toUpperCase() !== mb[1].toUpperCase())
|
|
2392
|
+
return false;
|
|
2393
|
+
// BigInt keeps the gap exact for numbers past 2^53.
|
|
2394
|
+
const gap = BigInt(mb[2]) - BigInt(ma[2]);
|
|
2395
|
+
return gap > 1n || gap < -1n;
|
|
2396
|
+
}
|
|
2397
|
+
function analyzeRequirementsLine(rawLine) {
|
|
2398
|
+
const line = typeof rawLine === 'string' ? rawLine : '';
|
|
2399
|
+
// SELECTOR — byte-identical to the pre-extraction expression.
|
|
2400
|
+
const citedReqIds = line
|
|
2401
|
+
.replace(/[\[\]]/g, '')
|
|
2402
|
+
.split(/[,\s]+/)
|
|
2403
|
+
.map((r) => r.trim())
|
|
2404
|
+
.filter(Boolean)
|
|
2405
|
+
.filter((r) => REQ_ID_SHAPE_RE.test(r));
|
|
2406
|
+
// DETECTOR tokenization. A token with NO alphanumerics is shaved of brackets
|
|
2407
|
+
// ONLY, so `(..)` surfaces its operator while a bare `..` is not shaved to
|
|
2408
|
+
// nothing by the punctuation classes. A trailing run of 2+ dots is a glued
|
|
2409
|
+
// range operator (`REQ-01.. REQ-05`), not sentence punctuation — keep it.
|
|
2410
|
+
const tokens = line
|
|
2411
|
+
.replace(/<!--[\s\S]*?-->/g, ' ')
|
|
2412
|
+
// Invisibles are removed HERE, for the classification rules — an operator
|
|
2413
|
+
// spelled `<ZWSP>..<ZWSP>` is still the range operator, and a line of only
|
|
2414
|
+
// invisibles yields no tokens at all. R4 works on the RAW line and does
|
|
2415
|
+
// NOT strip them, because there they are the evidence of a dropped id.
|
|
2416
|
+
// Removing them in both places is what made `REQ-01<ZWSP>, REQ-02` silent;
|
|
2417
|
+
// removing them in neither is what made `REQ-01 <ZWSP>..<ZWSP> REQ-05`
|
|
2418
|
+
// silent. The two questions have two different answers.
|
|
2419
|
+
.replace(REQ_INVISIBLE_RE, '')
|
|
2420
|
+
.split(/[,\s]+/)
|
|
2421
|
+
.map((t) => {
|
|
2422
|
+
const trimmed = t.trim();
|
|
2423
|
+
if (!/[A-Za-z0-9]/.test(trimmed)) {
|
|
2424
|
+
return trimmed.replace(/^[[({]+/, '').replace(/[\])}]+$/, '');
|
|
2425
|
+
}
|
|
2426
|
+
if (/\.{2,}$/.test(trimmed)) {
|
|
2427
|
+
return trimmed.replace(/^[[({"'`*_~“”‘’]+/, '');
|
|
2428
|
+
}
|
|
2429
|
+
return trimmed.replace(/^[[({"'`*_~“”‘’]+/, '').replace(/[\])}.;:"'`*_~“”‘’]+$/, '');
|
|
2430
|
+
})
|
|
2431
|
+
.filter(Boolean);
|
|
2432
|
+
// Every predicate below is applied through the scan limit (Nit 6): a token
|
|
2433
|
+
// past the bound is not classified at all rather than classified expensively.
|
|
2434
|
+
const short = (t) => t.length <= REQ_TOKEN_SCAN_LIMIT;
|
|
2435
|
+
const rangeTokens = tokens.filter((t) => short(t) && REQ_RANGE_TOKEN_RE.test(t));
|
|
2436
|
+
const spacedRangePairs = [];
|
|
2437
|
+
tokens.forEach((t, i) => {
|
|
2438
|
+
const left = tokens[i - 1] ?? '';
|
|
2439
|
+
const right = tokens[i + 1] ?? '';
|
|
2440
|
+
if (
|
|
2441
|
+
// EVERY participant is capped, not just the operator. Capping the operator
|
|
2442
|
+
// alone left `<2049-char ID> .. <2049-char ID>` running REQ_ID_SHAPE_RE and
|
|
2443
|
+
// BigInt over both neighbours unbounded — the cap read as uniform and was
|
|
2444
|
+
// not (found by the round's pre-push review).
|
|
2445
|
+
short(t) &&
|
|
2446
|
+
short(left) &&
|
|
2447
|
+
short(right) &&
|
|
2448
|
+
REQ_PURE_RANGE_OP_RE.test(t) &&
|
|
2449
|
+
i > 0 &&
|
|
2450
|
+
i < tokens.length - 1 &&
|
|
2451
|
+
REQ_ID_SHAPE_RE.test(left) &&
|
|
2452
|
+
REQ_ID_SHAPE_RE.test(right) &&
|
|
2453
|
+
reqEndpointsImplyInterior(left, right)) {
|
|
2454
|
+
spacedRangePairs.push([left, right]);
|
|
2455
|
+
}
|
|
2456
|
+
});
|
|
2457
|
+
const hasSpacedRange = spacedRangePairs.length > 0;
|
|
2458
|
+
// A half-spaced range splits at the tokenizer, so R1's own `\s*` never sees
|
|
2459
|
+
// it. SYMBOL operators only on the LEAD arm: a word operator glued to an ID
|
|
2460
|
+
// is an ID — `TORANGE-05` is a valid prefix-agnostic REQ-ID. The TRAIL arm
|
|
2461
|
+
// keeps the word operators, because a valid ID must end in digits, so
|
|
2462
|
+
// `REQ-01through` can only be a glued typo.
|
|
2463
|
+
const hasGluedRangeFragment = tokens.some((t, i) => {
|
|
2464
|
+
// Neighbours capped for the same reason as R2 above.
|
|
2465
|
+
if (!short(t))
|
|
2466
|
+
return false;
|
|
2467
|
+
const before = tokens[i - 1] ?? '';
|
|
2468
|
+
const after = tokens[i + 1] ?? '';
|
|
2469
|
+
const lead = REQ_GLUED_RANGE_LEAD_RE.exec(t);
|
|
2470
|
+
if (lead &&
|
|
2471
|
+
i > 0 &&
|
|
2472
|
+
short(before) &&
|
|
2473
|
+
REQ_ID_SHAPE_RE.test(before) &&
|
|
2474
|
+
reqEndpointsImplyInterior(before, lead[1])) {
|
|
2475
|
+
return true;
|
|
2476
|
+
}
|
|
2477
|
+
const trail = REQ_GLUED_RANGE_TRAIL_RE.exec(t);
|
|
2478
|
+
return Boolean(trail &&
|
|
2479
|
+
i < tokens.length - 1 &&
|
|
2480
|
+
short(after) &&
|
|
2481
|
+
REQ_ID_SHAPE_RE.test(after) &&
|
|
2482
|
+
reqEndpointsImplyInterior(trail[1], after));
|
|
2483
|
+
});
|
|
2484
|
+
const leadToken = (tokens[0] ?? '').toUpperCase();
|
|
2485
|
+
// CENSUS (round 3, review finding Minor 4): the placeholder domain is what
|
|
2486
|
+
// GSD itself seeds plus what an author writes for "deliberately empty".
|
|
2487
|
+
// Reached: `TBD` — the ONLY machine-written seed, at the three phase.add /
|
|
2488
|
+
// -batch / -insert sites — and `None`, the author convention. NOT reached:
|
|
2489
|
+
// `N/A`, `Deferred`, `Pending`, `TBA`, `-`. Consequence, and it is now
|
|
2490
|
+
// ENFORCED rather than asserted: such a line selects zero IDs and warns
|
|
2491
|
+
// through R3b below, which is what #3697's acceptance criterion asks for
|
|
2492
|
+
// ("when it selects zero IDs from a line that is non-empty and is not the
|
|
2493
|
+
// `TBD` placeholder"). Round 3 shipped this same paragraph while R3's
|
|
2494
|
+
// ID-shape gate made it false for all five words — bare `Deferred` was
|
|
2495
|
+
// silent, `Deferred (see ADR-7)` warned — and the claim sat in three
|
|
2496
|
+
// artifacts with no test in either direction. Inferring placeholder-ness
|
|
2497
|
+
// from arbitrary prose is still the free-text heuristic this detector
|
|
2498
|
+
// avoids: R3b keys on the SELECTION being empty, never on what the prose
|
|
2499
|
+
// means.
|
|
2500
|
+
const placeholderLed = leadToken === 'TBD' || leadToken === 'NONE';
|
|
2501
|
+
const inertIdShaped = citedReqIds.length === 0 && !placeholderLed
|
|
2502
|
+
? tokens.filter((t) => short(t) && t.includes('-') && REQ_ID_SUBSTRING_RE.test(t))
|
|
2503
|
+
: [];
|
|
2504
|
+
// R3b — the acceptance criterion's own narrow form. `tokens.length > 0` is
|
|
2505
|
+
// what keeps an empty line and a comment-only line silent: the tokenizer
|
|
2506
|
+
// strips `<!-- ... -->` before splitting, so `<!-- fill in -->` yields no
|
|
2507
|
+
// tokens and cannot reach this rule. Every other zero-selection,
|
|
2508
|
+
// non-placeholder line warns.
|
|
2509
|
+
const zeroSelectionInert = citedReqIds.length === 0 && !placeholderLed && tokens.length > 0;
|
|
2510
|
+
// R2 is the ONLY ambiguous rule — a tight range, a glued fragment and R3
|
|
2511
|
+
// residue each implicate ID-shaped text the selector demonstrably did not
|
|
2512
|
+
// take, so any of them means the line really did fail to parse. R2 is
|
|
2513
|
+
// ambiguous only when its OWN endpoints were selected: the detector shaves
|
|
2514
|
+
// brackets and the selector does not, so R2 can fire on a `(RANGE-02)` that
|
|
2515
|
+
// was never selected — a real drop, and the assertive channel is right there.
|
|
2516
|
+
// A token past the cap is NOT classified — and must therefore not be
|
|
2517
|
+
// silently discarded. Round 3's first cut of the uniform cap did exactly
|
|
2518
|
+
// that: a 2049-char range token warned before the round and went silent
|
|
2519
|
+
// after it, which is #3697's own defect introduced by the fix for a nit
|
|
2520
|
+
// (found by the round's pre-push review). The cap bounds the WORK, not the
|
|
2521
|
+
// warning — so an over-cap token that could carry an ID is reported as
|
|
2522
|
+
// unclassified. The test is `includes('-')`, a linear scan, never the
|
|
2523
|
+
// unanchored regex the cap exists to keep off these tokens.
|
|
2524
|
+
// ANY over-cap token, not just one carrying `-`. The first cut filtered on
|
|
2525
|
+
// `includes('-')` and therefore missed an over-cap OPERATOR:
|
|
2526
|
+
// `REQ-01 <2049 dots> REQ-05` warned before this round (R2 was uncapped) and
|
|
2527
|
+
// went silent after it. A token we could not examine makes the line
|
|
2528
|
+
// unverified whatever characters it happens to contain. Computed below,
|
|
2529
|
+
// where the selected set is available.
|
|
2530
|
+
// ID-shaped tokens the selector did not take, ANYWHERE on the line. This is
|
|
2531
|
+
// reported as a fact, never used to pick the channel: `(ADR-7)` and
|
|
2532
|
+
// `(REQ-02)` are indistinguishable by shape, so routing on it would put the
|
|
2533
|
+
// false "could not be parsed" claim back on a line carrying a citation.
|
|
2534
|
+
// Naming them lets the author see what the tokenizer skipped without the
|
|
2535
|
+
// warning asserting a verdict it cannot support in either direction.
|
|
2536
|
+
const selected = new Set(citedReqIds.map((id) => id.toUpperCase()));
|
|
2537
|
+
// A token the SELECTOR took has had its own SELECTION verified — the selector
|
|
2538
|
+
// is uncapped and anchored, so it examined the whole token. That is not the
|
|
2539
|
+
// same as "no rule was suppressed by it", and conflating the two was the
|
|
2540
|
+
// second continuation review's CLAIM J/K: two over-cap valid IDs either side
|
|
2541
|
+
// of `..` are both selected, both exempted, and R2 is capped — so a line that
|
|
2542
|
+
// warned before this round went silent, which is the very regression the
|
|
2543
|
+
// field exists to close, arriving through the fix for its own over-report.
|
|
2544
|
+
//
|
|
2545
|
+
// The exemption therefore applies only when nothing could have been
|
|
2546
|
+
// suppressed: an over-cap token that was selected AND has no neighbour that
|
|
2547
|
+
// could pair with it into a range. Everything else is unexaminable and is
|
|
2548
|
+
// reported as such.
|
|
2549
|
+
const couldPairIntoRange = (i) => {
|
|
2550
|
+
for (const n of [tokens[i - 1], tokens[i + 1]]) {
|
|
2551
|
+
if (n === undefined)
|
|
2552
|
+
continue;
|
|
2553
|
+
if (!short(n))
|
|
2554
|
+
return true;
|
|
2555
|
+
if (REQ_PURE_RANGE_OP_RE.test(n))
|
|
2556
|
+
return true;
|
|
2557
|
+
if (REQ_GLUED_RANGE_LEAD_RE.test(n) || REQ_GLUED_RANGE_TRAIL_RE.test(n))
|
|
2558
|
+
return true;
|
|
2559
|
+
}
|
|
2560
|
+
return false;
|
|
2561
|
+
};
|
|
2562
|
+
const oversizedTokens = tokens.filter((t, i) => !short(t) && (!selected.has(t.toUpperCase()) || couldPairIntoRange(i)));
|
|
2563
|
+
const unselectedIdShaped = tokens.filter((t) => short(t) && REQ_ID_SUBSTRING_RE.test(t) && !selected.has(t.toUpperCase()));
|
|
2564
|
+
// R4 runs on the RAW line, not on `tokens`: the shave that makes `REQ-01;`
|
|
2565
|
+
// look like a clean `REQ-01` is exactly the evidence this rule needs, so it
|
|
2566
|
+
// has to see the character the tokenizer removed.
|
|
2567
|
+
const delimiterDroppedIds = reqDelimiterDroppedIds(rawLine, selected, REQ_TOKEN_SCAN_LIMIT);
|
|
2568
|
+
const nothingDemonstrablyDropped = rangeTokens.length === 0 &&
|
|
2569
|
+
!hasGluedRangeFragment &&
|
|
2570
|
+
inertIdShaped.length === 0 &&
|
|
2571
|
+
// R4 is a DEMONSTRATED drop, so neither non-assertive voice — one claiming
|
|
2572
|
+
// nothing was dropped, the other that nothing could be checked — may speak
|
|
2573
|
+
// for a line carrying one.
|
|
2574
|
+
delimiterDroppedIds.length === 0 &&
|
|
2575
|
+
// R2 firing on an endpoint the selector did NOT take is itself a
|
|
2576
|
+
// demonstrated drop, and the assertive channel is right there. Vacuously
|
|
2577
|
+
// true when no spaced range fired, which is what makes this a strict
|
|
2578
|
+
// superset of the `!hasSpacedRange` guard the over-cap channel used to
|
|
2579
|
+
// carry — that channel's behaviour on a line with no spaced range is
|
|
2580
|
+
// unchanged, byte for byte.
|
|
2581
|
+
spacedRangePairs.every(([a, b]) => selected.has(a.toUpperCase()) && selected.has(b.toUpperCase()));
|
|
2582
|
+
const rangeReadingOnly = hasSpacedRange &&
|
|
2583
|
+
nothingDemonstrablyDropped &&
|
|
2584
|
+
// The cap bounds the WORK, never the warning. An over-cap token is not
|
|
2585
|
+
// classified by ANY rule (R1-R4 all skip it), so the voice whose entire
|
|
2586
|
+
// claim is that nothing was dropped has no basis to speak for this line.
|
|
2587
|
+
// It falls to the over-cap channel below instead — `unverified`, because
|
|
2588
|
+
// the line was not CHECKED; not `misparse`, because nothing on it
|
|
2589
|
+
// demonstrably failed to parse either. Round 7 review, Minor 1.
|
|
2590
|
+
oversizedTokens.length === 0;
|
|
2591
|
+
// Named rather than inlined into the return literal (round 3 review Minor 3):
|
|
2592
|
+
// this disjunction is the module's single most important predicate, and in
|
|
2593
|
+
// the literal a later edit that reordered a local below the `return` would be
|
|
2594
|
+
// a TDZ ReferenceError at runtime rather than an error at the reader's eye
|
|
2595
|
+
// level. R3b joins it here — see its field docs above for why it is not
|
|
2596
|
+
// gated on ID shape.
|
|
2597
|
+
const warn = rangeTokens.length > 0 ||
|
|
2598
|
+
hasSpacedRange ||
|
|
2599
|
+
hasGluedRangeFragment ||
|
|
2600
|
+
inertIdShaped.length > 0 ||
|
|
2601
|
+
zeroSelectionInert ||
|
|
2602
|
+
delimiterDroppedIds.length > 0 ||
|
|
2603
|
+
oversizedTokens.length > 0;
|
|
2604
|
+
return {
|
|
2605
|
+
citedReqIds,
|
|
2606
|
+
tokens,
|
|
2607
|
+
rangeTokens,
|
|
2608
|
+
hasSpacedRange,
|
|
2609
|
+
hasGluedRangeFragment,
|
|
2610
|
+
inertIdShaped,
|
|
2611
|
+
zeroSelectionInert,
|
|
2612
|
+
placeholderLed,
|
|
2613
|
+
spacedRangePairs,
|
|
2614
|
+
nothingDemonstrablyDropped,
|
|
2615
|
+
rangeReadingOnly,
|
|
2616
|
+
delimiterDroppedIds,
|
|
2617
|
+
oversizedTokens,
|
|
2618
|
+
unselectedIdShaped,
|
|
2619
|
+
warn,
|
|
2620
|
+
};
|
|
2621
|
+
}
|
|
2622
|
+
/**
|
|
2623
|
+
* Render the warning, or null when the line is clean.
|
|
2624
|
+
*
|
|
2625
|
+
* TWO CHANNELS, and the split is round 3's fix for review finding Major 3. The
|
|
2626
|
+
* detector cannot distinguish `RANGE-02 — RANGE-05` meaning a range from the
|
|
2627
|
+
* same text meaning an annotation separator; they are textually identical and
|
|
2628
|
+
* no token-level rule separates them. What the old single-channel message did
|
|
2629
|
+
* was resolve that ambiguity by ASSERTION — it told the author the line "could
|
|
2630
|
+
* not be parsed" and to rewrite it, on a line where every ID present had in
|
|
2631
|
+
* fact been selected and nothing had been dropped. That is a false statement
|
|
2632
|
+
* under the annotation reading and the #2334 over-warning class.
|
|
2633
|
+
*
|
|
2634
|
+
* Going silent instead is not available: the range reading is equally live, and
|
|
2635
|
+
* staying quiet on it re-opens the exact silent under-selection #3697 is about.
|
|
2636
|
+
* So the ambiguity is DISCLOSED rather than decided —
|
|
2637
|
+
*
|
|
2638
|
+
* * any rule other than R2 fired, or R2 fired on an endpoint that was not
|
|
2639
|
+
* selected → something ID-shaped was demonstrably NOT taken. The line did
|
|
2640
|
+
* fail to parse; say so plainly, as before.
|
|
2641
|
+
* * R2 alone fired and both its endpoints were selected → nothing was
|
|
2642
|
+
* dropped. State both readings and let the author pick; never claim a parse
|
|
2643
|
+
* failure that did not occur.
|
|
2644
|
+
*/
|
|
2645
|
+
function formatRequirementsLineWarning(phaseNum, rawLine, analysis) {
|
|
2646
|
+
if (!analysis.warn)
|
|
2647
|
+
return null;
|
|
2648
|
+
const shown = String(rawLine).trim();
|
|
2649
|
+
const rangeRuleFired = analysis.rangeTokens.length > 0 || analysis.hasSpacedRange || analysis.hasGluedRangeFragment;
|
|
2650
|
+
// Tokens the selector skipped, stated as a fact in EITHER channel. `(ADR-7)`
|
|
2651
|
+
// and `(REQ-02)` are the same shape, so no rule can say which one matters —
|
|
2652
|
+
// but the author can, and only if the warning tells them. Round 3's first
|
|
2653
|
+
// cut instead let this drive the channel, which put the false "could not be
|
|
2654
|
+
// parsed" claim back on a line carrying a citation.
|
|
2655
|
+
// Names only what the rule-specific clauses did NOT already name, so the
|
|
2656
|
+
// assertive voice can carry it too without repeating itself.
|
|
2657
|
+
const alreadyNamed = new Set([...analysis.rangeTokens, ...analysis.inertIdShaped, ...analysis.delimiterDroppedIds].map((t) => t.toUpperCase()));
|
|
2658
|
+
const skippedNames = analysis.unselectedIdShaped.filter((t) => !alreadyNamed.has(t.toUpperCase()));
|
|
2659
|
+
// Named, then qualified. The `PREFIX-N-N` shape is the one the range rules
|
|
2660
|
+
// deliberately decline to act on, so the rider says WHY it might not be a
|
|
2661
|
+
// requirement instead of silently deciding it is not.
|
|
2662
|
+
const ambiguousNamed = skippedNames.filter((t) => REQ_AMBIGUOUS_NUMERIC_RE.test(t));
|
|
2663
|
+
const skipped = skippedNames.length > 0
|
|
2664
|
+
? ` ID-shaped text on the line that was NOT selected: ${skippedNames.join(', ')}` +
|
|
2665
|
+
` (parentheses are not stripped, unlike square brackets) — check whether any of it is a` +
|
|
2666
|
+
` requirement.` +
|
|
2667
|
+
(ambiguousNamed.length > 0
|
|
2668
|
+
? ` ${ambiguousNamed.join(', ')} may equally be a date or a sub-numbered id, which is` +
|
|
2669
|
+
` why the range rules do not act on that shape.`
|
|
2670
|
+
: '')
|
|
2671
|
+
: '';
|
|
2672
|
+
// R4's clause. Named separately from the generic skipped-text rider because
|
|
2673
|
+
// this one is not a "check whether any of it is a requirement" hedge — the
|
|
2674
|
+
// token IS an ID, the selector demonstrably did not take it, and the cause
|
|
2675
|
+
// is nameable.
|
|
2676
|
+
const delimiterDropped = analysis.delimiterDroppedIds.length > 0
|
|
2677
|
+
? ` ${analysis.delimiterDroppedIds.join(', ')} ${analysis.delimiterDroppedIds.length === 1 ? 'was' : 'were'}` +
|
|
2678
|
+
` NOT selected: a \`;\` or \`:\` is glued to the ID, or it carries an invisible character, and` +
|
|
2679
|
+
` the line is split on commas and whitespace only. Write each requirement as a bare ID` +
|
|
2680
|
+
` separated by a comma.`
|
|
2681
|
+
: '';
|
|
2682
|
+
const oversized = analysis.oversizedTokens.length > 0
|
|
2683
|
+
? ` One or more tokens exceed the ${REQ_TOKEN_SCAN_LIMIT}-character scan limit and were NOT` +
|
|
2684
|
+
` classified, so this line may carry more than is reported here.`
|
|
2685
|
+
: '';
|
|
2686
|
+
if (analysis.rangeReadingOnly) {
|
|
2687
|
+
// AMBIGUOUS channel — the RANGE reading is what is at stake, not a parse
|
|
2688
|
+
// failure: every endpoint the range rule fired on was selected.
|
|
2689
|
+
//
|
|
2690
|
+
// What this voice must NOT do is claim the whole LINE is correct. It has
|
|
2691
|
+
// no basis for that: an unrelated `(REQ-02)` elsewhere on the line is
|
|
2692
|
+
// dropped by the selector and invisible to every rule, so "nothing needs
|
|
2693
|
+
// to change" is an affirmative false statement on exactly the input the
|
|
2694
|
+
// rule-scoped discriminator was built to reach. It speaks about the
|
|
2695
|
+
// SEPARATOR, and defers the rest to the skipped-text clause above.
|
|
2696
|
+
return {
|
|
2697
|
+
code: REQ_LINE_WARNING_CODE.rangeReading,
|
|
2698
|
+
message: `ROADMAP Phase ${phaseNum} **Requirements** line (\`${shown}\`) contains what reads as a range ` +
|
|
2699
|
+
`between two cited REQ-IDs. Range forms are not expanded, so no interior IDs were selected; ` +
|
|
2700
|
+
`the line selected: ${analysis.citedReqIds.join(', ')}. If a range was intended, rewrite it ` +
|
|
2701
|
+
`naming every requirement explicitly (e.g. \`REQ-01, REQ-02, REQ-03\`); if that separator is ` +
|
|
2702
|
+
`an annotation rather than a range, it selected nothing to expand and needs no change.` +
|
|
2703
|
+
delimiterDropped +
|
|
2704
|
+
skipped +
|
|
2705
|
+
oversized,
|
|
2706
|
+
};
|
|
2707
|
+
}
|
|
2708
|
+
if (analysis.oversizedTokens.length > 0 && analysis.nothingDemonstrablyDropped) {
|
|
2709
|
+
// A DEMONSTRATED drop outranks this voice, whose whole claim is that
|
|
2710
|
+
// NOTHING could be checked — both cannot be true at once. `REQ-01,
|
|
2711
|
+
// REQ-02: <over-cap token>` names REQ-02 in `delimiterDroppedIds` and
|
|
2712
|
+
// then reported `req-line-unverified`, whose message never mentions it:
|
|
2713
|
+
// the concrete, actionable finding masked by the token beside it. That
|
|
2714
|
+
// exclusion now lives in `nothingDemonstrablyDropped`, shared verbatim
|
|
2715
|
+
// with `rangeReadingOnly` above rather than duplicated here — the
|
|
2716
|
+
// duplication is what let the two drift (round 7 review, Minor 1). The
|
|
2717
|
+
// assertive channel already appends the over-cap rider, so routing a
|
|
2718
|
+
// demonstrated drop there loses nothing about the cap.
|
|
2719
|
+
// OVER-CAP channel — no rule could run, so no rule may be diagnosed. Say
|
|
2720
|
+
// exactly that: the line was not classified, rather than not a problem.
|
|
2721
|
+
return {
|
|
2722
|
+
code: REQ_LINE_WARNING_CODE.unverified,
|
|
2723
|
+
message: `ROADMAP Phase ${phaseNum} **Requirements** line (\`${shown.slice(0, 200)}…\`) could not be ` +
|
|
2724
|
+
`checked: one or more tokens exceed the ${REQ_TOKEN_SCAN_LIMIT}-character scan limit, so the ` +
|
|
2725
|
+
`REQ-ID selection on this line is unverified. Rewrite it as a comma-separated list ` +
|
|
2726
|
+
`(e.g. \`REQ-01, REQ-02, REQ-03\`).`,
|
|
2727
|
+
};
|
|
2728
|
+
}
|
|
2729
|
+
// ASSERTIVE channel — ID-shaped content was demonstrably not selected.
|
|
2730
|
+
// Deliberately says "selected", NOT "marked complete": a range whose
|
|
2731
|
+
// endpoints are themselves unregistered selects them and marks nothing, and a
|
|
2732
|
+
// warning that overclaims the write is a warning the reader learns to
|
|
2733
|
+
// distrust.
|
|
2734
|
+
const selectedDesc = analysis.citedReqIds.length > 0
|
|
2735
|
+
? `the only REQ-ID(s) selected from it were: ${analysis.citedReqIds.join(', ')}`
|
|
2736
|
+
: 'it selected NO REQ-IDs at all, so nothing was marked';
|
|
2737
|
+
const unparsed = [...new Set([...analysis.rangeTokens, ...analysis.inertIdShaped])];
|
|
2738
|
+
// Only diagnose "range" when a range rule actually fired — an R3 warning on
|
|
2739
|
+
// non-range ID text must not claim one was written. And on the R3 path the
|
|
2740
|
+
// residue is ID-SHAPED TEXT, which is not the same claim as "a requirement we
|
|
2741
|
+
// failed to parse" (round 3 review finding Minor 4: `Deferred (see ADR-7)`
|
|
2742
|
+
// reported `ADR-7` as missed requirement content when it is a citation). Name
|
|
2743
|
+
// what it is, and name the placeholder escape the author actually has.
|
|
2744
|
+
const advice = rangeRuleFired
|
|
2745
|
+
? ' Range forms are not expanded; rewrite the line naming every requirement explicitly ' +
|
|
2746
|
+
'(e.g. `REQ-01, REQ-02, REQ-03`).'
|
|
2747
|
+
: ' If these are requirements, name them explicitly (e.g. `REQ-01, REQ-02, REQ-03`); if the line ' +
|
|
2748
|
+
'is deliberately empty, write `TBD` or `None` — any other wording selects nothing and warns.';
|
|
2749
|
+
return {
|
|
2750
|
+
code: REQ_LINE_WARNING_CODE.misparse,
|
|
2751
|
+
message: `ROADMAP Phase ${phaseNum} **Requirements** line could not be parsed as a comma-separated REQ-ID list ` +
|
|
2752
|
+
`(\`${shown}\`) - ${selectedDesc}.` +
|
|
2753
|
+
(unparsed.length > 0
|
|
2754
|
+
? rangeRuleFired
|
|
2755
|
+
? ` Unparsed text: ${unparsed.join(', ')}.`
|
|
2756
|
+
: ` ID-shaped text that was not selected: ${unparsed.join(', ')}.`
|
|
2757
|
+
: '') +
|
|
2758
|
+
advice +
|
|
2759
|
+
delimiterDropped +
|
|
2760
|
+
skipped +
|
|
2761
|
+
oversized,
|
|
2762
|
+
};
|
|
2763
|
+
}
|
|
2057
2764
|
function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
2058
2765
|
if (!phaseNum) {
|
|
2059
2766
|
error('phase number required for phase complete');
|
|
@@ -2110,6 +2817,11 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
2110
2817
|
let roadmapUpdated = false;
|
|
2111
2818
|
let stateUpdated = false;
|
|
2112
2819
|
const warnings = [];
|
|
2820
|
+
// The machine kind of the Requirements-line warning, carried out to the JSON
|
|
2821
|
+
// result as its own field (round 4 review Major 3). Declared HERE, in the
|
|
2822
|
+
// same scope as `warnings[]`, because the assignment happens inside
|
|
2823
|
+
// withPlanningLock and the emission happens after it.
|
|
2824
|
+
let reqLineWarningCode;
|
|
2113
2825
|
// ADR-3408 §8.5 / D2 (#3374): "liberal but visible" — when the write-seam
|
|
2114
2826
|
// composition's preservation stage restores a curated frontmatter value
|
|
2115
2827
|
// over a disagreeing derived one, that divergence is surfaced here rather
|
|
@@ -2513,27 +3225,22 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
2513
3225
|
// `else`, discarding this fact silently instead of surfacing it.
|
|
2514
3226
|
const traceabilityWriteMisses = [];
|
|
2515
3227
|
if (reqMatch) {
|
|
2516
|
-
// #2334 HIGH 3
|
|
2517
|
-
//
|
|
2518
|
-
//
|
|
2519
|
-
//
|
|
2520
|
-
//
|
|
2521
|
-
//
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
.replace(/[\[\]]/g, '')
|
|
2533
|
-
.split(/[,\s]+/)
|
|
2534
|
-
.map((r) => r.trim())
|
|
2535
|
-
.filter(Boolean)
|
|
2536
|
-
.filter((r) => REQ_ID_SHAPE_RE.test(r));
|
|
3228
|
+
// #2334 HIGH 3 + #3697: selection and under-selection detection both
|
|
3229
|
+
// live in `analyzeRequirementsLine` (module scope, above), extracted in
|
|
3230
|
+
// round 3 so the parser is directly testable — a closure in here is
|
|
3231
|
+
// reachable only by spawning the CLI, which no fast-check property test
|
|
3232
|
+
// can do. `citedReqIds` is byte-identical to the expression that stood
|
|
3233
|
+
// here; nothing about what phase-complete MARKS has changed.
|
|
3234
|
+
const reqLineAnalysis = analyzeRequirementsLine(reqMatch[1]);
|
|
3235
|
+
citedReqIds = reqLineAnalysis.citedReqIds;
|
|
3236
|
+
const reqLineWarning = formatRequirementsLineWarning(phaseNum, reqMatch[1], reqLineAnalysis);
|
|
3237
|
+
if (reqLineWarning) {
|
|
3238
|
+
warnings.push(reqLineWarning.message);
|
|
3239
|
+
// Carried out to the JSON result as its own field — see
|
|
3240
|
+
// REQ_LINE_WARNING_CODE for why it is not folded into
|
|
3241
|
+
// `warnings[]`.
|
|
3242
|
+
reqLineWarningCode = reqLineWarning.code;
|
|
3243
|
+
}
|
|
2537
3244
|
for (const reqId of citedReqIds) {
|
|
2538
3245
|
const reqEscaped = (0, pattern_cjs_1.escapeRegex)(reqId);
|
|
2539
3246
|
// Surface 1 — the checkbox: - [ ] **REQ-ID** → - [x] **REQ-ID**.
|
|
@@ -2867,13 +3574,32 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
2867
3574
|
// #1729: `(?:\s*\([^)\n]{0,200}\))?` after the number tolerates a pre-colon
|
|
2868
3575
|
// ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE) so
|
|
2869
3576
|
// `### Phase N (Cluster B): X` resolves. Captures are unchanged.
|
|
2870
|
-
|
|
3577
|
+
//
|
|
3578
|
+
// #4078: the checkbox branch's separator is no longer colon-only. The
|
|
3579
|
+
// canonical phase lookup has accepted the bullet-house dash grammar
|
|
3580
|
+
// (`- [ ] **Phase N — Name**`, em/en-dash/hyphen/colon) since #2199
|
|
3581
|
+
// (`BULLET_PHASE_LINE_PATTERN`, roadmap-parser.cjs), but this scan still
|
|
3582
|
+
// required `:`, so on a roadmap whose original rows use the dash grammar
|
|
3583
|
+
// the ONLY parseable row above N was typically a later phase.add-ingested
|
|
3584
|
+
// colon-form phase — positionally last — and it won the numeric-minimum
|
|
3585
|
+
// vote it should never have been alone in (observed: 18 of 18 selected,
|
|
3586
|
+
// phases 2–17 skipped). The heading branch stays colon-only, mirroring
|
|
3587
|
+
// `findRoadmapPhaseInContent`'s heading grammar exactly; only the
|
|
3588
|
+
// checkbox branch widens, and only to the separators #2199 already
|
|
3589
|
+
// accepts. The two branches keep separate capture groups, normalized
|
|
3590
|
+
// just below the loop.
|
|
3591
|
+
const phasePattern = new RegExp(`(?:#{2,4}\\s*(?:\\*\\*|__)?\\s*Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:\\s*([^\\n*]+)` +
|
|
3592
|
+
`|-\\s*\\[[ xX]\\]\\s*(?:\\*\\*|__)?\\s*Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})(?:\\s*\\([^)\\n]{0,200}\\))?\\s*[—–:\\-]\\s*([^\\n*]+))`, 'gi');
|
|
2871
3593
|
let pm;
|
|
2872
3594
|
while ((pm = phasePattern.exec(roadmapForPhases)) !== null) {
|
|
3595
|
+
// #4078: normalize the two alternation branches' captures (heading
|
|
3596
|
+
// branch → groups 1/2, widened checkbox branch → groups 3/4).
|
|
3597
|
+
const pmNum = pm[1] ?? pm[3];
|
|
3598
|
+
const pmName = pm[2] ?? pm[4];
|
|
2873
3599
|
// #2786: skip sentinel phase ids (999.x backlog, 0.x drafts) — stage 1
|
|
2874
3600
|
// already skips sentinel dirs on disk via isSentinelPhaseId (#3185);
|
|
2875
3601
|
// stage 2's heading scan must not advance into backlog headings either.
|
|
2876
|
-
if (isSentinelPhaseId(
|
|
3602
|
+
if (isSentinelPhaseId(pmNum))
|
|
2877
3603
|
continue;
|
|
2878
3604
|
// #3701 review: the numeric MINIMUM above N, not the first row above N in
|
|
2879
3605
|
// DOCUMENT order. This scan walks raw roadmap text, and one global regex
|
|
@@ -2889,10 +3615,10 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
2889
3615
|
// Phase NUMBERS define sequence here, exactly as `comparePhaseNum` does for
|
|
2890
3616
|
// the disk scan and for #2028's lowest-outstanding override; the roadmap
|
|
2891
3617
|
// defines which phases EXIST and which milestone they belong to.
|
|
2892
|
-
if (comparePhaseNum(
|
|
2893
|
-
&& (roadmapNextNum === null || comparePhaseNum(
|
|
2894
|
-
roadmapNextNum =
|
|
2895
|
-
roadmapNextName =
|
|
3618
|
+
if (comparePhaseNum(pmNum, phaseNum) > 0
|
|
3619
|
+
&& (roadmapNextNum === null || comparePhaseNum(pmNum, roadmapNextNum) < 0)) {
|
|
3620
|
+
roadmapNextNum = pmNum;
|
|
3621
|
+
roadmapNextName = pmName
|
|
2896
3622
|
.replace(/\(INSERTED\)/i, '')
|
|
2897
3623
|
.trim()
|
|
2898
3624
|
.toLowerCase()
|
|
@@ -2953,7 +3679,12 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
2953
3679
|
if (roadmapContent !== null) {
|
|
2954
3680
|
try {
|
|
2955
3681
|
const milestoneScope = extractCurrentMilestone(roadmapContent, cwd);
|
|
2956
|
-
|
|
3682
|
+
// #4078: the separator class here mirrors stage 2's widened checkbox
|
|
3683
|
+
// branch (and #2199's BULLET_PHASE_LINE_PATTERN): em/en-dash/hyphen/colon.
|
|
3684
|
+
// Without it, this lowest-outstanding override was blind to dash-grammar
|
|
3685
|
+
// rows and could not correct an out-of-order completion on the same
|
|
3686
|
+
// mixed-grammar roadmaps that broke stage 2.
|
|
3687
|
+
const cbPattern = new RegExp(`-\\s*\\[(x| )\\]\\s*(?:\\*\\*|__)?\\s*Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})(?:\\s*\\([^)\\n]{0,200}\\))?\\s*[—–:\\-]\\s*([^\\n*]+)`, 'gi');
|
|
2957
3688
|
let cbm;
|
|
2958
3689
|
let lowestOutstanding = null;
|
|
2959
3690
|
while ((cbm = cbPattern.exec(milestoneScope)) !== null) {
|
|
@@ -3146,6 +3877,10 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
3146
3877
|
auto_pruned: autoPruned,
|
|
3147
3878
|
warnings,
|
|
3148
3879
|
has_warnings: warnings.length > 0,
|
|
3880
|
+
// ADDITIVE, never a change to `warnings[]`'s element shape — that array is
|
|
3881
|
+
// a documented string[] consumed by execute-phase.md, so re-typing it
|
|
3882
|
+
// would break a shipped output contract. Absent when the line is clean.
|
|
3883
|
+
...(reqLineWarningCode ? { requirements_line_warning: { code: reqLineWarningCode } } : {}),
|
|
3149
3884
|
verification_stale_check_indeterminate: staleCheckIndeterminate,
|
|
3150
3885
|
milestone_conflict: milestoneConflict,
|
|
3151
3886
|
preservation_warnings: preservationWarnings,
|
|
@@ -3207,9 +3942,13 @@ module.exports = {
|
|
|
3207
3942
|
cmdPhaseAdd,
|
|
3208
3943
|
cmdPhaseAddBatch,
|
|
3209
3944
|
cmdPhaseMvpMode,
|
|
3945
|
+
cmdPhaseTddApplicable,
|
|
3210
3946
|
cmdPhaseInsert,
|
|
3211
3947
|
cmdPhaseRemove,
|
|
3212
3948
|
cmdPhaseComplete,
|
|
3949
|
+
analyzeRequirementsLine,
|
|
3950
|
+
formatRequirementsLineWarning,
|
|
3951
|
+
REQ_LINE_WARNING_CODE,
|
|
3213
3952
|
cmdPhaseUatPassed,
|
|
3214
3953
|
cmdPhaseListPlans,
|
|
3215
3954
|
computeDependencyLevels,
|