@opengsd/gsd-core 1.6.1 → 1.7.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +20 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.opencode/plugins/gsd-core.js +711 -0
- package/agents/gsd-advisor-researcher.md +2 -0
- package/agents/gsd-ai-researcher.md +1 -1
- package/agents/gsd-assumptions-analyzer.md +2 -0
- package/agents/gsd-code-fixer.md +2 -0
- package/agents/gsd-code-reviewer.md +2 -0
- package/agents/gsd-codebase-mapper.md +2 -0
- package/agents/gsd-debugger.md +2 -0
- package/agents/gsd-doc-writer.md +2 -0
- package/agents/gsd-eval-auditor.md +2 -0
- package/agents/gsd-executor.md +9 -6
- package/agents/gsd-integration-checker.md +2 -0
- package/agents/gsd-nyquist-auditor.md +2 -0
- package/agents/gsd-phase-researcher.md +2 -0
- package/agents/gsd-plan-checker.md +2 -0
- package/agents/gsd-planner.md +2 -0
- package/agents/gsd-project-researcher.md +2 -0
- package/agents/gsd-research-synthesizer.md +2 -0
- package/agents/gsd-roadmapper.md +2 -0
- package/agents/gsd-security-auditor.md +2 -0
- package/agents/gsd-ui-auditor.md +2 -0
- package/agents/gsd-ui-checker.md +2 -0
- package/agents/gsd-ui-researcher.md +2 -0
- package/agents/gsd-verifier.md +5 -2
- package/bin/gsd-mcp-server.js +31 -0
- package/bin/install.js +411 -1146
- package/commands/gsd/review.md +6 -0
- package/gemini-extension.json +1 -1
- package/gsd-core/bin/gsd-tools.cjs +134 -8
- package/gsd-core/bin/lib/adapter-declarative.cjs +35 -0
- package/gsd-core/bin/lib/adapter-imperative.cjs +52 -0
- package/gsd-core/bin/lib/assumption-delta.cjs +231 -0
- package/gsd-core/bin/lib/capability-lifecycle.cjs +7 -7
- package/gsd-core/bin/lib/capability-loader.cjs +45 -9
- package/gsd-core/bin/lib/capability-lock.cjs +2 -2
- package/gsd-core/bin/lib/capability-registry.cjs +891 -82
- package/gsd-core/bin/lib/capability-source.cjs +26 -11
- package/gsd-core/bin/lib/capability-validator.cjs +222 -2
- package/gsd-core/bin/lib/cli-skew-check.cjs +44 -0
- package/gsd-core/bin/lib/command-aliases.cjs +8 -0
- package/gsd-core/bin/lib/commands.cjs +2 -1
- package/gsd-core/bin/lib/config.cjs +27 -0
- package/gsd-core/bin/lib/embedding-adapter.cjs +27 -0
- package/gsd-core/bin/lib/external-descriptor-trust.cjs +70 -0
- package/gsd-core/bin/lib/frontmatter.cjs +53 -6
- package/gsd-core/bin/lib/handshake-serialized.cjs +70 -0
- package/gsd-core/bin/lib/hook-bus.cjs +81 -0
- package/gsd-core/bin/lib/host-integration-sdk.cjs +53 -0
- package/gsd-core/bin/lib/host-integration.cjs +469 -0
- package/gsd-core/bin/lib/init.cjs +35 -7
- package/gsd-core/bin/lib/install-engine.cjs +755 -0
- package/gsd-core/bin/lib/install-profiles.cjs +35 -4
- package/gsd-core/bin/lib/installer-migrations.cjs +1 -1
- package/gsd-core/bin/lib/mcp-server.cjs +194 -0
- package/gsd-core/bin/lib/milestone.cjs +68 -40
- package/gsd-core/bin/lib/model-adapter.cjs +50 -0
- package/gsd-core/bin/lib/phase-id.cjs +18 -0
- package/gsd-core/bin/lib/phase.cjs +57 -90
- package/gsd-core/bin/lib/phases-command-router.cjs +4 -3
- package/gsd-core/bin/lib/planning-workspace.cjs +1 -1
- package/gsd-core/bin/lib/probe-core.cjs +132 -2
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +129 -13
- package/gsd-core/bin/lib/roadmap-command-router.cjs +3 -2
- package/gsd-core/bin/lib/roadmap-parser.cjs +21 -11
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +3 -2
- package/gsd-core/bin/lib/roadmap.cjs +33 -22
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +65 -9
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +54 -4
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +5 -2
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +1 -1
- package/gsd-core/bin/lib/runtime-name-policy.cjs +160 -30
- package/gsd-core/bin/lib/shell-command-projection.cjs +16 -0
- package/gsd-core/bin/lib/stale-bake-guard.cjs +254 -0
- package/gsd-core/bin/lib/state-command-router.cjs +4 -0
- package/gsd-core/bin/lib/state-io.cjs +55 -0
- package/gsd-core/bin/lib/state-transition.cjs +1603 -0
- package/gsd-core/bin/lib/state.cjs +327 -683
- package/gsd-core/bin/lib/surface.cjs +4 -1
- package/gsd-core/bin/lib/validate.cjs +2 -1
- package/gsd-core/bin/lib/verify.cjs +6 -4
- package/gsd-core/bin/lib/workstream-inventory-builder.cjs +12 -2
- package/gsd-core/bin/lib/workstream-inventory.cjs +28 -0
- package/gsd-core/bin/lib/workstream.cjs +4 -4
- package/gsd-core/bin/shared/config-schema.manifest.json +9 -0
- package/gsd-core/references/agent-skills-bootstrap.md +60 -0
- package/gsd-core/references/honest-verifier.md +105 -0
- package/gsd-core/references/model-profiles.md +27 -0
- package/gsd-core/references/reviewer-instances.md +99 -0
- package/gsd-core/workflows/autonomous.md +30 -32
- package/gsd-core/workflows/complete-milestone.md +6 -10
- package/gsd-core/workflows/execute-phase.md +1 -1
- package/gsd-core/workflows/forensics.md +3 -3
- package/gsd-core/workflows/help/modes/full.md +1 -1
- package/gsd-core/workflows/manager.md +15 -15
- package/gsd-core/workflows/milestone-summary.md +3 -3
- package/gsd-core/workflows/new-milestone.md +6 -0
- package/gsd-core/workflows/plan-phase/steps/closed-phase-gate.md +42 -0
- package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +102 -0
- package/gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md +23 -0
- package/gsd-core/workflows/plan-phase.md +4 -159
- package/gsd-core/workflows/review.md +33 -2
- package/gsd-core/workflows/thread.md +4 -4
- package/gsd-core/workflows/verify-phase.md +11 -4
- package/gsd-core/workflows/verify-work.md +1 -2
- package/hooks/dist/gsd-graphify-update.sh +7 -1
- package/hooks/gsd-graphify-update.sh +7 -1
- package/package.json +6 -4
- package/scripts/ci-test-scope.cjs +38 -9
- package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -1
- package/scripts/lint-regression-test-names.allowlist.json +3 -0
- package/scripts/lint-test-file-count.allowlist.json +19 -5
- package/scripts/mutation-matrix.cjs +45 -3
- package/scripts/prompt-injection-scan.sh +8 -0
- package/scripts/run-tests.cjs +51 -1
- package/scripts/sync-manifest-versions.cjs +66 -14
- package/skills/gsd-review/SKILL.md +6 -0
- package/scripts/lint-windows-test-portability.cjs +0 -178
|
@@ -32,7 +32,7 @@ const coreUtilsMod = require("./core-utils.cjs");
|
|
|
32
32
|
const { toPosixPath, generateSlugInternal, readSubdirectories } = coreUtilsMod;
|
|
33
33
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-id.cjs is an export= CommonJS module
|
|
34
34
|
const phaseIdMod = require("./phase-id.cjs");
|
|
35
|
-
const { escapeRegex, normalizePhaseName, phaseMarkdownRegexSource, comparePhaseNum, phaseTokenMatches, OPTIONAL_PROJECT_CODE_PREFIX_SOURCE, } = phaseIdMod;
|
|
35
|
+
const { escapeRegex, normalizePhaseName, phaseMarkdownRegexSource, comparePhaseNum, phaseTokenMatches, OPTIONAL_PROJECT_CODE_PREFIX_SOURCE, OPTIONAL_PHASE_TAG_SOURCE, } = phaseIdMod;
|
|
36
36
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-locator.cjs is an export= CommonJS module
|
|
37
37
|
const phaseLocatorMod = require("./phase-locator.cjs");
|
|
38
38
|
const { findPhaseInternal, getArchivedPhaseDirs } = phaseLocatorMod;
|
|
@@ -47,8 +47,8 @@ const frontmatterMod = require("./frontmatter.cjs");
|
|
|
47
47
|
const stateMod = require("./state.cjs");
|
|
48
48
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
49
49
|
const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
|
|
50
|
-
const phase_lifecycle_cjs_1 = require("./phase-lifecycle.cjs");
|
|
51
50
|
const clock_cjs_1 = require("./clock.cjs");
|
|
51
|
+
const state_transition_cjs_1 = require("./state-transition.cjs");
|
|
52
52
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- uat-predicate.cjs is an export= CommonJS module
|
|
53
53
|
const uatPredicate = require("./uat-predicate.cjs");
|
|
54
54
|
const { evaluateUatPassed } = uatPredicate;
|
|
@@ -57,7 +57,7 @@ const verificationMod = require("./verification.cjs");
|
|
|
57
57
|
const { readVerificationStatus } = verificationMod;
|
|
58
58
|
const { planningDir, withPlanningLock } = planningWorkspace;
|
|
59
59
|
const { extractFrontmatter } = frontmatterMod;
|
|
60
|
-
const { readModifyWriteStateMd, stateExtractField, stateReplaceField,
|
|
60
|
+
const { readModifyWriteStateMd, stateExtractField, stateReplaceField, syncStateFrontmatter, withStateLock, updatePerformanceMetricsSection, } = stateMod;
|
|
61
61
|
// #2893 — strict canonical filter: `{padded_phase}-{NN}-PLAN.md` or `PLAN.md`.
|
|
62
62
|
const isCanonicalPlanFile = (f) => f.endsWith('-PLAN.md') || f === 'PLAN.md';
|
|
63
63
|
// Any .md file with PLAN anywhere in the basename — diagnostic net
|
|
@@ -181,7 +181,7 @@ function cmdPhaseNextDecimal(cwd, basePhase, raw) {
|
|
|
181
181
|
if (node_fs_1.default.existsSync(roadmapPath)) {
|
|
182
182
|
try {
|
|
183
183
|
const roadmapContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
|
|
184
|
-
const phasePattern = new RegExp(`#{2,4}\\s*Phase\\s+${phaseMarkdownRegexSource(normalized)}\\.(\\d+)\\s*:`, 'gi');
|
|
184
|
+
const phasePattern = new RegExp(`#{2,4}\\s*Phase\\s+${phaseMarkdownRegexSource(normalized)}\\.(\\d+)${OPTIONAL_PHASE_TAG_SOURCE}\\s*:`, 'gi');
|
|
185
185
|
let pm;
|
|
186
186
|
while ((pm = phasePattern.exec(roadmapContent)) !== null) {
|
|
187
187
|
decimalSet.add(parseInt(pm[1], 10));
|
|
@@ -221,7 +221,7 @@ function getRoadmapModeForPhase(cwd, phaseNum) {
|
|
|
221
221
|
const milestoneContent = extractCurrentMilestone(rawContent, cwd);
|
|
222
222
|
const fullContent = stripShippedMilestones(rawContent);
|
|
223
223
|
const escapedPhase = phaseMarkdownRegexSource(phaseNum);
|
|
224
|
-
const phaseHeader = new RegExp(`#{2,4}\\s*Phase\\s+${escapedPhase}\\s*:`, 'i');
|
|
224
|
+
const phaseHeader = new RegExp(`#{2,4}\\s*Phase\\s+${escapedPhase}${OPTIONAL_PHASE_TAG_SOURCE}\\s*:`, 'i');
|
|
225
225
|
for (const content of [milestoneContent, fullContent]) {
|
|
226
226
|
const headerMatch = content.match(phaseHeader);
|
|
227
227
|
if (!headerMatch || headerMatch.index === undefined)
|
|
@@ -578,7 +578,8 @@ function cmdPhaseAdd(cwd, description, raw, customId) {
|
|
|
578
578
|
// Three sources are scanned so that a phase in ANY representation
|
|
579
579
|
// (section header, roadmap bullet, or on-disk directory) is counted:
|
|
580
580
|
// 1) Section headers: ### Phase N: / ## Phase N: / #### Phase N:
|
|
581
|
-
|
|
581
|
+
// #1729: `(?:\s*\([^)\n]*\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
|
|
582
|
+
const headerPattern = /#{2,4}\s*Phase\s+(\d+)[A-Z]?(?:\.\d+)*(?:\s*\([^)\n]*\))?:/gi;
|
|
582
583
|
// 2) Roadmap bullet entries: - [ ] **Phase N: ...** (all checkbox variants)
|
|
583
584
|
// The lookahead accepts colon, decimal-dot, whitespace, bold-close asterisk,
|
|
584
585
|
// or end-of-line so titleless forms ("- [ ] **Phase 11**", "- [ ] Phase 11")
|
|
@@ -662,7 +663,8 @@ function cmdPhaseAddBatch(cwd, descriptions, raw) {
|
|
|
662
663
|
const content = extractCurrentMilestone(rawContent, cwd);
|
|
663
664
|
let maxPhase = 0;
|
|
664
665
|
if (config.phase_naming !== 'custom') {
|
|
665
|
-
|
|
666
|
+
// #1729: `(?:\s*\([^)\n]*\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
|
|
667
|
+
const phasePattern = /#{2,4}\s*Phase\s+(\d+)[A-Z]?(?:\.\d+)*(?:\s*\([^)\n]*\))?:/gi;
|
|
666
668
|
let m;
|
|
667
669
|
while ((m = phasePattern.exec(content)) !== null) {
|
|
668
670
|
const num = parseInt(m[1], 10);
|
|
@@ -740,14 +742,14 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
|
|
|
740
742
|
const content = extractCurrentMilestone(rawContent, cwd);
|
|
741
743
|
const normalizedAfter = normalizePhaseName(afterPhase);
|
|
742
744
|
const afterPhaseEscaped = phaseMarkdownRegexSource(normalizedAfter);
|
|
743
|
-
const targetPattern = new RegExp(`#{2,4}\\s*Phase\\s+${afterPhaseEscaped}:`, 'i');
|
|
745
|
+
const targetPattern = new RegExp(`#{2,4}\\s*Phase\\s+${afterPhaseEscaped}${OPTIONAL_PHASE_TAG_SOURCE}:`, 'i');
|
|
744
746
|
const headingMatch = targetPattern.test(content);
|
|
745
|
-
const bulletPattern = new RegExp(`-\\s*\\[[ x]\\]\\s*(?:\\*\\*)?Phase\\s+${afterPhaseEscaped}[:\\s]`, 'i');
|
|
747
|
+
const bulletPattern = new RegExp(`-\\s*\\[[ x]\\]\\s*(?:\\*\\*)?Phase\\s+${afterPhaseEscaped}${OPTIONAL_PHASE_TAG_SOURCE}[:\\s]`, 'i');
|
|
746
748
|
const anyHeadingPattern = /#{2,4}\s*Phase\s+\d/i;
|
|
747
749
|
const roadmapHasHeadingPhases = anyHeadingPattern.test(content);
|
|
748
750
|
const isBulletStyle = !headingMatch && bulletPattern.test(content) && !roadmapHasHeadingPhases;
|
|
749
751
|
if (!headingMatch && !isBulletStyle) {
|
|
750
|
-
const checklistPattern = new RegExp(`-\\s*\\[[ x]\\]\\s*(?:\\*\\*)?Phase\\s+${afterPhaseEscaped}[:\\s]`, 'i');
|
|
752
|
+
const checklistPattern = new RegExp(`-\\s*\\[[ x]\\]\\s*(?:\\*\\*)?Phase\\s+${afterPhaseEscaped}${OPTIONAL_PHASE_TAG_SOURCE}[:\\s]`, 'i');
|
|
751
753
|
if (checklistPattern.test(content)) {
|
|
752
754
|
error(`Phase ${afterPhase} exists in roadmap summary but is missing a detail section (### Phase ${afterPhase}: ...).`);
|
|
753
755
|
}
|
|
@@ -769,7 +771,7 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
|
|
|
769
771
|
catch {
|
|
770
772
|
/* intentionally empty */
|
|
771
773
|
}
|
|
772
|
-
const rmPhasePattern = new RegExp(`#{2,4}\\s*Phase\\s+${phaseMarkdownRegexSource(normalizedBase)}\\.(\\d+)\\s*:`, 'gi');
|
|
774
|
+
const rmPhasePattern = new RegExp(`#{2,4}\\s*Phase\\s+${phaseMarkdownRegexSource(normalizedBase)}\\.(\\d+)${OPTIONAL_PHASE_TAG_SOURCE}\\s*:`, 'gi');
|
|
773
775
|
let rmMatch;
|
|
774
776
|
while ((rmMatch = rmPhasePattern.exec(rawContent)) !== null) {
|
|
775
777
|
decimalSet.add(parseInt(rmMatch[1], 10));
|
|
@@ -785,13 +787,13 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
|
|
|
785
787
|
(0, shell_command_projection_cjs_1.platformWriteSync)(node_path_1.default.join(dirPath, '.gitkeep'), '');
|
|
786
788
|
let updatedContent;
|
|
787
789
|
if (isBulletStyle) {
|
|
788
|
-
const boldBulletPattern = new RegExp(`-\\s*\\[[ x]\\]\\s*\\*\\*Phase\\s+${afterPhaseEscaped}:`, 'i');
|
|
790
|
+
const boldBulletPattern = new RegExp(`-\\s*\\[[ x]\\]\\s*\\*\\*Phase\\s+${afterPhaseEscaped}${OPTIONAL_PHASE_TAG_SOURCE}:`, 'i');
|
|
789
791
|
const useBold = boldBulletPattern.test(content);
|
|
790
792
|
const phaseLabel = useBold
|
|
791
793
|
? `**Phase ${_decimalPhase}: ${description}**`
|
|
792
794
|
: `Phase ${_decimalPhase}: ${description}`;
|
|
793
795
|
const bulletEntry = `\n- [ ] ${phaseLabel}`;
|
|
794
|
-
const targetBulletPattern = new RegExp(`(-\\s*\\[[ x]\\]\\s*(?:\\*\\*)?Phase\\s+${afterPhaseEscaped}[:\\s][^\\n]*)`, 'i');
|
|
796
|
+
const targetBulletPattern = new RegExp(`(-\\s*\\[[ x]\\]\\s*(?:\\*\\*)?Phase\\s+${afterPhaseEscaped}${OPTIONAL_PHASE_TAG_SOURCE}[:\\s][^\\n]*)`, 'i');
|
|
795
797
|
const bulletMatchResult = rawContent.match(targetBulletPattern);
|
|
796
798
|
if (!bulletMatchResult) {
|
|
797
799
|
error(`Could not find Phase ${afterPhase} bullet line`);
|
|
@@ -811,7 +813,7 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
|
|
|
811
813
|
}
|
|
812
814
|
else {
|
|
813
815
|
const phaseEntry = `\n### Phase ${_decimalPhase}: ${description} (INSERTED)\n\n**Goal:** [Urgent work - to be planned]\n**Requirements**: TBD\n**Depends on:** Phase ${afterPhase}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run ${(0, runtime_slash_cjs_1.formatGsdSlash)('plan-phase', (0, runtime_slash_cjs_1.resolveRuntime)(cwd))} ${_decimalPhase} to break down)\n`;
|
|
814
|
-
const headerPattern = new RegExp(`(#{2,4}\\s*Phase\\s+${afterPhaseEscaped}:[^\\n]*\\n)`, 'i');
|
|
816
|
+
const headerPattern = new RegExp(`(#{2,4}\\s*Phase\\s+${afterPhaseEscaped}${OPTIONAL_PHASE_TAG_SOURCE}:[^\\n]*\\n)`, 'i');
|
|
815
817
|
const headerMatch = rawContent.match(headerPattern);
|
|
816
818
|
if (!headerMatch) {
|
|
817
819
|
error(`Could not find Phase ${afterPhase} header`);
|
|
@@ -860,12 +862,12 @@ function renameDecimalPhases(phasesDir, baseInt, removedDecimal) {
|
|
|
860
862
|
const oldPhaseId = `${baseInt}.${item.oldDecimal}`;
|
|
861
863
|
const newPhaseId = `${baseInt}.${newDecimal}`;
|
|
862
864
|
const newDirName = `${item.prefix}.${newDecimal}-${item.slug}`;
|
|
863
|
-
|
|
865
|
+
(0, shell_command_projection_cjs_1.retryRenameSync)(node_path_1.default.join(phasesDir, item.dir), node_path_1.default.join(phasesDir, newDirName));
|
|
864
866
|
renamedDirs.push({ from: item.dir, to: newDirName });
|
|
865
867
|
for (const f of node_fs_1.default.readdirSync(node_path_1.default.join(phasesDir, newDirName))) {
|
|
866
868
|
if (f.includes(oldPhaseId)) {
|
|
867
869
|
const newFileName = f.replace(oldPhaseId, newPhaseId);
|
|
868
|
-
|
|
870
|
+
(0, shell_command_projection_cjs_1.retryRenameSync)(node_path_1.default.join(phasesDir, newDirName, f), node_path_1.default.join(phasesDir, newDirName, newFileName));
|
|
869
871
|
renamedFiles.push({ from: f, to: newFileName });
|
|
870
872
|
}
|
|
871
873
|
}
|
|
@@ -903,12 +905,12 @@ function renameIntegerPhases(phasesDir, removedInt) {
|
|
|
903
905
|
const oldPrefix = `${oldPadded}${letterSuffix}${decimalSuffix}`;
|
|
904
906
|
const newPrefix = `${newPadded}${letterSuffix}${decimalSuffix}`;
|
|
905
907
|
const newDirName = `${newPrefix}-${item.slug}`;
|
|
906
|
-
|
|
908
|
+
(0, shell_command_projection_cjs_1.retryRenameSync)(node_path_1.default.join(phasesDir, item.dir), node_path_1.default.join(phasesDir, newDirName));
|
|
907
909
|
renamedDirs.push({ from: item.dir, to: newDirName });
|
|
908
910
|
for (const f of node_fs_1.default.readdirSync(node_path_1.default.join(phasesDir, newDirName))) {
|
|
909
911
|
if (f.startsWith(oldPrefix)) {
|
|
910
912
|
const newFileName = newPrefix + f.slice(oldPrefix.length);
|
|
911
|
-
|
|
913
|
+
(0, shell_command_projection_cjs_1.retryRenameSync)(node_path_1.default.join(phasesDir, newDirName, f), node_path_1.default.join(phasesDir, newDirName, newFileName));
|
|
912
914
|
renamedFiles.push({ from: f, to: newFileName });
|
|
913
915
|
}
|
|
914
916
|
}
|
|
@@ -940,11 +942,13 @@ function updateRoadmapAfterPhaseRemoval(roadmapPath, targetPhase, isDecimal, rem
|
|
|
940
942
|
withPlanningLock(cwd, () => {
|
|
941
943
|
let content = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
|
|
942
944
|
const escaped = escapeRegex(targetPhase);
|
|
943
|
-
content = content.replace(new RegExp(`\\n?(?<h>#{2,4})\\s*Phase\\s+${escaped}\\s*:[\\s\\S]*?(?=\\n\\k<h>(?!#)\\s+Phase\\s+[^\\n:]+\\s*:|$)`, 'i'), '');
|
|
944
|
-
content = content.replace(new RegExp(`\\n?-\\s*\\[[ x]\\]\\s*.*Phase\\s+${escaped}[:\\s][^\\n]*`, 'gi'), '');
|
|
945
|
+
content = content.replace(new RegExp(`\\n?(?<h>#{2,4})\\s*Phase\\s+${escaped}${OPTIONAL_PHASE_TAG_SOURCE}\\s*:[\\s\\S]*?(?=\\n\\k<h>(?!#)\\s+Phase\\s+[^\\n:]+\\s*:|$)`, 'i'), '');
|
|
946
|
+
content = content.replace(new RegExp(`\\n?-\\s*\\[[ x]\\]\\s*.*Phase\\s+${escaped}${OPTIONAL_PHASE_TAG_SOURCE}[:\\s][^\\n]*`, 'gi'), '');
|
|
945
947
|
content = content.replace(new RegExp(`\\n?\\|\\s*${escaped}\\.?\\s[^|]*\\|[^\\n]*`, 'gi'), '');
|
|
946
948
|
if (!isDecimal) {
|
|
947
|
-
|
|
949
|
+
// #1729: fold an optional pre-colon ( ) tag into the suffix capture so it
|
|
950
|
+
// is re-emitted verbatim — a tagged later phase still gets renumbered.
|
|
951
|
+
content = content.replace(/(#{2,4}\s*Phase\s+)(\d+(?:\.\d+)?)((?:\s*\([^)\n]*\))?\s*:)/gi, (_match, prefix, num, suffix) => `${prefix}${decrementRoadmapPhaseToken(num, removedInt)}${suffix}`);
|
|
948
952
|
content = content.replace(/(-\s*\[[ x]\]\s*.*?Phase\s+)(\d+)(\s*:|\s+)/gi, (_match, prefix, num, suffix) => `${prefix}${decrementRoadmapPhaseNumber(num, removedInt)}${suffix}`);
|
|
949
953
|
content = content.replace(/(\|\s*)(\d+)(\.\s)/g, (_match, prefix, num, suffix) => `${prefix}${decrementRoadmapPhaseNumber(num, removedInt)}${suffix}`);
|
|
950
954
|
content = content.replace(/(?<![0-9-])(\d{2})-(\d{2})(?=(?:(?:-[A-Za-z][A-Za-z0-9-]*)?-(?:PLAN|SUMMARY)\.md)|(?![0-9-]))/g, (_match, phaseNum, planNum) => `${decrementRoadmapPaddedPhaseNumber(phaseNum, removedInt)}-${planNum}`);
|
|
@@ -1047,7 +1051,7 @@ function phaseDisplayNameFromRoadmap(roadmapContent, phaseNum) {
|
|
|
1047
1051
|
if (!roadmapContent || !phaseNum)
|
|
1048
1052
|
return null;
|
|
1049
1053
|
const phaseEscaped = phaseMarkdownRegexSource(phaseNum);
|
|
1050
|
-
const heading = roadmapContent.match(new RegExp(`^#{2,4}\\s*Phase\\s+${phaseEscaped}\\s*:\\s*([^\\n]+)`, 'im'));
|
|
1054
|
+
const heading = roadmapContent.match(new RegExp(`^#{2,4}\\s*Phase\\s+${phaseEscaped}${OPTIONAL_PHASE_TAG_SOURCE}\\s*:\\s*([^\\n]+)`, 'im'));
|
|
1051
1055
|
if (!heading)
|
|
1052
1056
|
return null;
|
|
1053
1057
|
const name = heading[1].replace(/\(INSERTED\)/i, '').trim();
|
|
@@ -1128,7 +1132,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
1128
1132
|
const originalRoadmapContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
|
|
1129
1133
|
roadmapContent = originalRoadmapContent;
|
|
1130
1134
|
const phaseEscaped = phaseMarkdownRegexSource(phaseNum);
|
|
1131
|
-
const checkboxPattern = new RegExp(`(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${phaseEscaped}[:\\s][^\\n]*)`, 'i');
|
|
1135
|
+
const checkboxPattern = new RegExp(`(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${phaseEscaped}${OPTIONAL_PHASE_TAG_SOURCE}[:\\s][^\\n]*)`, 'i');
|
|
1132
1136
|
roadmapContent = roadmapContent.replace(checkboxPattern, `$1x$2 (completed ${today})`);
|
|
1133
1137
|
const tableRowPattern = new RegExp(`^(\\|\\s*${phaseEscaped}\\.?\\s[^|]*(?:\\|[^\\n]*))$`, 'im');
|
|
1134
1138
|
roadmapContent = roadmapContent.replace(tableRowPattern, (fullRow) => {
|
|
@@ -1170,7 +1174,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
1170
1174
|
if (node_fs_1.default.existsSync(reqPath)) {
|
|
1171
1175
|
const phaseEsc = phaseMarkdownRegexSource(phaseNum);
|
|
1172
1176
|
const currentMilestoneRoadmap = extractCurrentMilestone(roadmapContent, cwd);
|
|
1173
|
-
const phaseSectionMatch = currentMilestoneRoadmap.match(new RegExp(`(#{2,4}\\s*Phase\\s+${phaseEsc}[:\\s][\\s\\S]*?)(?=#{2,4}\\s*Phase\\s+|$)`, 'i'));
|
|
1177
|
+
const phaseSectionMatch = currentMilestoneRoadmap.match(new RegExp(`(#{2,4}\\s*Phase\\s+${phaseEsc}${OPTIONAL_PHASE_TAG_SOURCE}[:\\s][\\s\\S]*?)(?=#{2,4}\\s*Phase\\s+|$)`, 'i'));
|
|
1174
1178
|
const sectionText = phaseSectionMatch ? phaseSectionMatch[1] : '';
|
|
1175
1179
|
const reqMatch = sectionText.match(/\*\*Requirements:?\*\*[^\S\n]*:?[^\S\n]*([^\n]+)/i);
|
|
1176
1180
|
const originalReqContent = node_fs_1.default.readFileSync(reqPath, 'utf-8');
|
|
@@ -1299,7 +1303,10 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
1299
1303
|
// phase. Allow optional `**`/`__` emphasis after the marker and stop
|
|
1300
1304
|
// the name capture at emphasis so bold names slug cleanly; the number
|
|
1301
1305
|
// capture is unchanged.
|
|
1302
|
-
|
|
1306
|
+
// #1729: `(?:\s*\([^)\n]*\))?` after the number tolerates a pre-colon
|
|
1307
|
+
// ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE) so
|
|
1308
|
+
// `### Phase N (Cluster B): X` resolves. Captures are unchanged.
|
|
1309
|
+
const phasePattern = /(?:#{2,4}|-\s*\[[ xX]\])\s*(?:\*\*|__)?\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)(?:\s*\([^)\n]*\))?\s*:\s*([^\n*]+)/gi;
|
|
1303
1310
|
let pm;
|
|
1304
1311
|
while ((pm = phasePattern.exec(roadmapForPhases)) !== null) {
|
|
1305
1312
|
if (comparePhaseNum(pm[1], phaseNum) > 0) {
|
|
@@ -1321,73 +1328,33 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
1321
1328
|
if (node_fs_1.default.existsSync(statePath)) {
|
|
1322
1329
|
const originalStateContent = (0, shell_command_projection_cjs_1.platformReadSync)(statePath) || '';
|
|
1323
1330
|
let stateContent = originalStateContent;
|
|
1324
|
-
|
|
1331
|
+
// ADR-1769 Phase 3: the STATE.md field-update policy (Current Phase
|
|
1332
|
+
// shape/name, Status, Current Plan, Last Activity + Description, and
|
|
1333
|
+
// the Completed/Total Phases + Progress percent block) now dispatches
|
|
1334
|
+
// to the STATE.md Transition Module. The ~90-line inline RMW callback
|
|
1335
|
+
// that lived here is the pure `completePhaseCore` in
|
|
1336
|
+
// src/state-transition.cts, backed by the field-classification table.
|
|
1337
|
+
// `updatePerformanceMetricsSection` + `syncStateFrontmatter` stay in
|
|
1338
|
+
// this adapter: they are section-table / disk-scan concerns, not
|
|
1339
|
+
// classified fields, and `syncStateFrontmatter` is the post-sync this
|
|
1340
|
+
// transaction needs (it does NOT go through readModifyWriteStateMd
|
|
1341
|
+
// because STATE.md is committed atomically with ROADMAP/REQUIREMENTS).
|
|
1325
1342
|
const nextPhaseDisplayName = phaseDisplayNameFromRoadmap(roadmapContent, nextPhaseNum) ??
|
|
1326
1343
|
phaseDisplayNameFromSlug(nextPhaseName);
|
|
1327
|
-
const
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
}
|
|
1342
|
-
else if (nextPhaseDisplayName) {
|
|
1343
|
-
newPhaseValue = `${phaseValue} — ${nextPhaseDisplayName}`;
|
|
1344
|
-
}
|
|
1345
|
-
}
|
|
1346
|
-
stateContent = stateReplaceFieldWithFallback(stateContent, 'Current Phase', 'Phase', newPhaseValue);
|
|
1347
|
-
if (nextPhaseDisplayName) {
|
|
1348
|
-
stateContent =
|
|
1349
|
-
stateReplaceField(stateContent, 'Current Phase Name', nextPhaseDisplayName) ||
|
|
1350
|
-
stateContent;
|
|
1351
|
-
}
|
|
1352
|
-
stateContent = stateReplaceFieldWithFallback(stateContent, 'Status', null, isLastPhase ? 'Milestone complete' : 'Ready to plan');
|
|
1353
|
-
stateContent = stateReplaceFieldWithFallback(stateContent, 'Current Plan', 'Plan', 'Not started');
|
|
1354
|
-
const lastActivityDescription = `Phase ${phaseNum} complete${nextPhaseNum ? `, transitioned to Phase ${nextPhaseNum}` : ''}`;
|
|
1355
|
-
if (/^Last activity:/m.test(stateContent)) {
|
|
1356
|
-
stateContent =
|
|
1357
|
-
stateReplaceField(stateContent, 'Last activity', `${today} — ${lastActivityDescription}`) ||
|
|
1358
|
-
stateContent;
|
|
1359
|
-
}
|
|
1360
|
-
else {
|
|
1361
|
-
stateContent =
|
|
1362
|
-
stateReplaceField(stateContent, 'Last Activity', today) ||
|
|
1363
|
-
stateContent;
|
|
1364
|
-
}
|
|
1365
|
-
stateContent =
|
|
1366
|
-
stateReplaceField(stateContent, 'Last Activity Description', lastActivityDescription) ||
|
|
1367
|
-
stateContent;
|
|
1368
|
-
const completedRaw = stateExtractField(stateContent, 'Completed Phases');
|
|
1369
|
-
if (completedRaw !== null) {
|
|
1370
|
-
let newCompleted = parseInt(completedRaw, 10);
|
|
1371
|
-
let derivedTotalPhases = null;
|
|
1372
|
-
if (roadmapContent !== null) {
|
|
1373
|
-
const derived = (0, phase_lifecycle_cjs_1.deriveProgressFromRoadmap)(roadmapContent);
|
|
1374
|
-
if (derived.completedPhases !== null)
|
|
1375
|
-
newCompleted = derived.completedPhases;
|
|
1376
|
-
if (derived.totalPhases !== null)
|
|
1377
|
-
derivedTotalPhases = derived.totalPhases;
|
|
1378
|
-
}
|
|
1379
|
-
stateContent =
|
|
1380
|
-
stateReplaceField(stateContent, 'Completed Phases', String(newCompleted)) ||
|
|
1381
|
-
stateContent;
|
|
1382
|
-
const totalRaw = stateExtractField(stateContent, 'Total Phases');
|
|
1383
|
-
const totalPhases = derivedTotalPhases || (totalRaw ? parseInt(totalRaw, 10) : null);
|
|
1384
|
-
if (totalPhases && totalPhases > 0) {
|
|
1385
|
-
const newPercent = (0, phase_lifecycle_cjs_1.clampPercent)(newCompleted, totalPhases);
|
|
1386
|
-
stateContent =
|
|
1387
|
-
stateReplaceField(stateContent, 'Progress', `${newPercent}%`) || stateContent;
|
|
1388
|
-
stateContent = stateContent.replace(/(percent:\s*)\d+/, `$1${newPercent}`);
|
|
1389
|
-
}
|
|
1390
|
-
}
|
|
1344
|
+
const completeResult = (0, state_transition_cjs_1.transitionCore)(stateContent, {
|
|
1345
|
+
kind: 'completePhase',
|
|
1346
|
+
phaseNum,
|
|
1347
|
+
nextPhaseNum,
|
|
1348
|
+
nextPhaseName: nextPhaseDisplayName,
|
|
1349
|
+
isLastPhase,
|
|
1350
|
+
planCount,
|
|
1351
|
+
summaryCount,
|
|
1352
|
+
}, {
|
|
1353
|
+
clock: clock_cjs_1.realClock,
|
|
1354
|
+
progressProvider: () => null, // completePhase derives progress from the roadmap, not disk
|
|
1355
|
+
roadmapProvider: () => roadmapContent,
|
|
1356
|
+
});
|
|
1357
|
+
stateContent = completeResult.content;
|
|
1391
1358
|
stateContent = updatePerformanceMetricsSection(stateContent, cwd, phaseNum, planCount, summaryCount);
|
|
1392
1359
|
stateContent = syncStateFrontmatter(stateContent, cwd);
|
|
1393
1360
|
writes.push({ filePath: statePath, before: originalStateContent, after: stateContent });
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Keeps gsd-tools.cjs thin while preserving current CJS semantics.
|
|
5
5
|
*
|
|
6
6
|
* Unsupported in this router (treated as unknown):
|
|
7
|
-
* - archive: `phases archive` is excluded
|
|
8
|
-
*
|
|
7
|
+
* - archive: `phases archive` is excluded (#2684) — it is an internal
|
|
8
|
+
* subcommand that milestone.complete forwards to, not a public surface.
|
|
9
9
|
*
|
|
10
10
|
* ADR-457 build-at-publish: the hand-written bin/lib/phases-command-router.cjs
|
|
11
11
|
* collapsed to a TypeScript source of truth. Behaviour is preserved byte-for-behaviour
|
|
@@ -19,7 +19,8 @@ const { routeCjsCommandFamily } = cjsCommandRouterAdapter;
|
|
|
19
19
|
function routePhasesCommand({ phase, milestone, args, cwd, raw, error }) {
|
|
20
20
|
routeCjsCommandFamily({
|
|
21
21
|
args,
|
|
22
|
-
//
|
|
22
|
+
// #2684: `phases archive` is deliberately excluded — it is an internal
|
|
23
|
+
// subcommand that milestone.complete forwards to, not a public surface.
|
|
23
24
|
subcommands: command_aliases_cjs_1.PHASES_SUBCOMMANDS.filter((s) => s !== 'archive'),
|
|
24
25
|
error,
|
|
25
26
|
unknownMessage: (_subcommand, available) => `Unknown phases subcommand. Available: ${available.join(', ')}`,
|
|
@@ -260,7 +260,7 @@ function withPlanningLock(cwd, fn, clock) {
|
|
|
260
260
|
const stolen = lockPath + '.stale-' + process.pid + '-' + clock.now() + '-' + (_planningStealSeq++);
|
|
261
261
|
let renamed = false;
|
|
262
262
|
try {
|
|
263
|
-
|
|
263
|
+
(0, shell_command_projection_cjs_1.retryRenameSync)(lockPath, stolen);
|
|
264
264
|
renamed = true;
|
|
265
265
|
}
|
|
266
266
|
catch { /* another racer won */ }
|
|
@@ -30,13 +30,17 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
30
30
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
31
31
|
};
|
|
32
32
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
33
|
-
exports.PROHIBITION_VALIDATORS = exports.VALID_STATUS = void 0;
|
|
33
|
+
exports.INSUFFICIENT_SPEC = exports.PROHIBITION_VALIDATORS = exports.VALID_STATUS = void 0;
|
|
34
34
|
exports.validateRequirement = validateRequirement;
|
|
35
35
|
exports.validateResolution = validateResolution;
|
|
36
36
|
exports.analyzeCoverage = analyzeCoverage;
|
|
37
37
|
exports.validateProhibitionResolution = validateProhibitionResolution;
|
|
38
38
|
exports.projectProhibitions = projectProhibitions;
|
|
39
39
|
exports.dispositionForProhibition = dispositionForProhibition;
|
|
40
|
+
exports.truthStatement = truthStatement;
|
|
41
|
+
exports.truthVerification = truthVerification;
|
|
42
|
+
exports.projectTruths = projectTruths;
|
|
43
|
+
exports.dispositionForUnverifiableTruth = dispositionForUnverifiableTruth;
|
|
40
44
|
exports.runProbeCli = runProbeCli;
|
|
41
45
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
42
46
|
/** The LOCKED set of valid lifecycle statuses (the re-cut: no covered/backstop). */
|
|
@@ -44,11 +48,40 @@ exports.VALID_STATUS = ['resolved', 'dismissed', 'unresolved'];
|
|
|
44
48
|
function errMessage(e) {
|
|
45
49
|
return e instanceof Error ? e.message : String(e);
|
|
46
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Structural guard for ONE item of the report `items[]` — enforces the `Item` contract (#1907).
|
|
53
|
+
* `analyzeCoverage` always emits fully-populated, category/status-validated Items, so this never
|
|
54
|
+
* rejects legitimate output; it catches an adapter that bypasses the merge and hands back per-item
|
|
55
|
+
* garbage (e.g. `items:[{}]`) inside a well-shaped envelope — which the container-only guard let
|
|
56
|
+
* sail through as green output despite the fail-closed docstring below.
|
|
57
|
+
*/
|
|
58
|
+
function isValidItem(item) {
|
|
59
|
+
if (item == null || typeof item !== 'object')
|
|
60
|
+
return false;
|
|
61
|
+
const i = item;
|
|
62
|
+
if (typeof i.requirement_id !== 'string' || !i.requirement_id.trim())
|
|
63
|
+
return false;
|
|
64
|
+
if (typeof i.category !== 'string' || !i.category.trim())
|
|
65
|
+
return false;
|
|
66
|
+
if (!exports.VALID_STATUS.includes(i.status))
|
|
67
|
+
return false;
|
|
68
|
+
if (typeof i.probe !== 'string')
|
|
69
|
+
return false;
|
|
70
|
+
// The three nullable fields must be a string or null — never some other type.
|
|
71
|
+
if (i.verification !== null && typeof i.verification !== 'string')
|
|
72
|
+
return false;
|
|
73
|
+
if (i.resolution !== null && typeof i.resolution !== 'string')
|
|
74
|
+
return false;
|
|
75
|
+
if (i.reason !== null && typeof i.reason !== 'string')
|
|
76
|
+
return false;
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
47
79
|
/**
|
|
48
80
|
* Structural guard for the report an adapter's `analyze` returns. The scaffold types `analyze`
|
|
49
81
|
* loosely (it runs over JSON-parsed input the adapter `as`-casts), so a future adapter (#644)
|
|
50
82
|
* that forgets to validate inside its closure could hand back a malformed object. Rather than
|
|
51
|
-
* stringify garbage as green output, `runProbeCli` checks the report shape
|
|
83
|
+
* stringify garbage as green output, `runProbeCli` checks the report shape — container AND every
|
|
84
|
+
* item — and fails closed.
|
|
52
85
|
*/
|
|
53
86
|
function isValidReport(report) {
|
|
54
87
|
if (report == null || typeof report !== 'object')
|
|
@@ -56,6 +89,8 @@ function isValidReport(report) {
|
|
|
56
89
|
const r = report;
|
|
57
90
|
if (!Array.isArray(r.items))
|
|
58
91
|
return false;
|
|
92
|
+
if (!r.items.every(isValidItem))
|
|
93
|
+
return false;
|
|
59
94
|
const c = r.coverage;
|
|
60
95
|
if (c == null || typeof c !== 'object')
|
|
61
96
|
return false;
|
|
@@ -355,6 +390,101 @@ function dispositionForProhibition(prohibition, context = {}) {
|
|
|
355
390
|
reason: 'judgment-tier prohibition routes to judgment review — never a silent green (ADR-550 D4)',
|
|
356
391
|
};
|
|
357
392
|
}
|
|
393
|
+
/** Extract a truth's statement text from either the string or the object form (the Hyrum normalizer). */
|
|
394
|
+
function truthStatement(truth) {
|
|
395
|
+
if (typeof truth === 'string')
|
|
396
|
+
return truth;
|
|
397
|
+
if (truth != null && typeof truth === 'object') {
|
|
398
|
+
const s = truth.statement;
|
|
399
|
+
if (typeof s === 'string')
|
|
400
|
+
return s;
|
|
401
|
+
}
|
|
402
|
+
return '';
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Extract a truth's verification tier, or `null` when it carries none (a plain string, or an object
|
|
406
|
+
* with no/garbled marker). Failing toward `null` is the Postel-safe direction: an unrecognized marker
|
|
407
|
+
* grades NORMALLY (never a spurious abstention — the over-abstention guard, AC#3).
|
|
408
|
+
*
|
|
409
|
+
* The marker is NOT only machine-emitted: `must_haves` markers can be authored BY HAND (#1820's
|
|
410
|
+
* spec-optional predicate rail), and the frontmatter continuation-KV parser preserves stray
|
|
411
|
+
* surrounding whitespace/quotes on a hand-authored value. So we normalize before comparison
|
|
412
|
+
* (Postel: be liberal in what you accept) — `'backstop '`, `' backstop'`, `'"backstop"'` all
|
|
413
|
+
* recognize as the tier. Without this, a hand-authored non-inferable `backstop` truth with a stray
|
|
414
|
+
* trailing space silently grades green instead of abstaining — the exact #1154 false-pass (#1905).
|
|
415
|
+
* An unrecoverably-corrupted marker (e.g. an embedded quote) stays unrecognized → null → graded
|
|
416
|
+
* normally (AC#3): we cannot know its intent, and abstaining on it would be a spurious abstention.
|
|
417
|
+
*/
|
|
418
|
+
function truthVerification(truth) {
|
|
419
|
+
if (truth == null || typeof truth !== 'object')
|
|
420
|
+
return null;
|
|
421
|
+
const raw = truth.verification;
|
|
422
|
+
const v = typeof raw === 'string' ? raw.trim().replace(/^["']|["']$/g, '').trim() : raw;
|
|
423
|
+
return v === 'explicit' || v === 'backstop' ? v : null;
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Conservative serializer (Postel: "send well-formed, minimal data") for projecting truths into a
|
|
427
|
+
* `must_haves.truths` block — the truth-axis analogue of `projectProhibitions`. A `backstop` truth is
|
|
428
|
+
* emitted as a flat-scalar object `{ statement, verification: 'backstop' }` (ADR-550 #1278: flat
|
|
429
|
+
* scalars round-trip the existing `parseMustHavesBlock`; a nested object would mangle it). Every other
|
|
430
|
+
* truth collapses to a bare statement string — only the non-inferable tier needs a structured marker,
|
|
431
|
+
* so an `explicit`/inferable truth never carries one (no spurious markers). Empty statements are dropped.
|
|
432
|
+
*/
|
|
433
|
+
function projectTruths(items) {
|
|
434
|
+
if (!Array.isArray(items))
|
|
435
|
+
return [];
|
|
436
|
+
const out = [];
|
|
437
|
+
for (const item of items) {
|
|
438
|
+
const statement = truthStatement(item);
|
|
439
|
+
if (!statement)
|
|
440
|
+
continue;
|
|
441
|
+
if (truthVerification(item) === 'backstop') {
|
|
442
|
+
out.push({ statement, verification: 'backstop' });
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
out.push(statement);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return out;
|
|
449
|
+
}
|
|
450
|
+
/** The stable, distinguishable verdict-reason token for an abstained non-inferable truth (review condition 1). */
|
|
451
|
+
exports.INSUFFICIENT_SPEC = 'insufficient_spec';
|
|
452
|
+
/**
|
|
453
|
+
* Deterministic verify-time disposition for a single truth (ADR-550 D4 truth-axis mirror, #1154).
|
|
454
|
+
* PURE — no LLM judgment (ADR-550 D5); the LLM verifier's only job is to decide whether `evidence`
|
|
455
|
+
* exists, this helper owns the routing once that is known.
|
|
456
|
+
*
|
|
457
|
+
* - A `backstop` (non-inferable) truth with NO explicit evidence → `{ unverified, flagged }`,
|
|
458
|
+
* reason `insufficient_spec`. NEVER green — the verify-time companion to D4's never-silent-pass.
|
|
459
|
+
* - A `backstop` truth WITH explicit evidence (a passing wired held-out/property test) → `green`.
|
|
460
|
+
* Abstention is for the *unconfirmable*, not for every non-inferable check.
|
|
461
|
+
* - Any non-`backstop` truth (explicit, or a plain inferable string) → `green`, never flagged.
|
|
462
|
+
* This is the over-abstention guard (AC#3): abstention fires ONLY on the exogenous backstop tag.
|
|
463
|
+
*/
|
|
464
|
+
function dispositionForUnverifiableTruth(truth, context = {}) {
|
|
465
|
+
const tier = truthVerification(truth);
|
|
466
|
+
// Over-abstention guard (AC#3): only a backstop (non-inferable) truth is ever a candidate to abstain.
|
|
467
|
+
if (tier !== 'backstop') {
|
|
468
|
+
return {
|
|
469
|
+
status: 'green',
|
|
470
|
+
flagged: false,
|
|
471
|
+
tier,
|
|
472
|
+
reason: 'inferable truth — verified normally (no abstention; ADR-550 D4 over-abstention guard)',
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
const evidence = Array.isArray(context.evidence) ? context.evidence : [];
|
|
476
|
+
if (evidence.length === 0) {
|
|
477
|
+
// ABSTAIN: a non-inferable truth the verifier cannot confirm with explicit evidence. Routes to
|
|
478
|
+
// human_needed with the distinguishable insufficient_spec reason — never a silent pass (ADR-550 D4).
|
|
479
|
+
return { status: 'unverified', flagged: true, tier, reason: exports.INSUFFICIENT_SPEC };
|
|
480
|
+
}
|
|
481
|
+
return {
|
|
482
|
+
status: 'green',
|
|
483
|
+
flagged: false,
|
|
484
|
+
tier,
|
|
485
|
+
reason: 'backstop truth confirmed by explicit evidence (a passing wired held-out/property test or directly-observed behavior)',
|
|
486
|
+
};
|
|
487
|
+
}
|
|
358
488
|
/**
|
|
359
489
|
* Read the requirements file (and optional resolutions file), run the adapter's `analyze`,
|
|
360
490
|
* and write the report as pretty JSON + newline. With no requirements path, writes the usage
|