@praxisflux/gates 0.17.0 → 0.18.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/README.md +3 -0
- package/codebase-to-course/lib/spec-derive.mjs +46 -5
- package/grounding-wiki/lib/spec-derive.mjs +46 -5
- package/lib/spec-derive.mjs +46 -5
- package/package.json +1 -1
- package/spec-bridge/gates/bridge.mjs +98 -12
- package/spec-bridge/gates/cli.mjs +8 -2
- package/spec-bridge/lib/spec-derive.mjs +46 -5
package/README.md
CHANGED
|
@@ -43,6 +43,9 @@ Dependabot's `github-actions` ecosystem automates that.
|
|
|
43
43
|
|
|
44
44
|
- **`spec-bridge`** — every Backlog task linked to a Spec Kit spec dir carries a status its
|
|
45
45
|
spec artifacts prove (needs a `backlog/` dir; passes trivially with zero linked tasks).
|
|
46
|
+
The gate honors the checked repo's own `.spec-bridge.json`: `strictDone` (analyze-gated
|
|
47
|
+
Done) and `statusVocabulary` (opt-in phase-level status names, enforced at that finer
|
|
48
|
+
granularity). The contract for both lives in `spec-bridge/README.md`.
|
|
46
49
|
- **`wiki-freshness`** — every `docs/wiki` note is fresh against its `verified_against` pin
|
|
47
50
|
(needs full git history: `fetch-depth: 0`; a shallow clone fails with exactly that fix).
|
|
48
51
|
- **`course`** — a built codebase-to-course course passes its output gate (self-contained,
|
|
@@ -13,6 +13,11 @@
|
|
|
13
13
|
// "Done-eligible" deliberately isn't "Done": the sync skill may move the Backlog task to Done,
|
|
14
14
|
// and the bridge gate treats a Done status without this derivation as a blocking problem.
|
|
15
15
|
//
|
|
16
|
+
// Every derivation also names a finer STAGE (specifying → planning → implementing →
|
|
17
|
+
// validating → reviewing); the status above is a fixed collapse of it (coarseStatus). The
|
|
18
|
+
// stage feeds the opt-in phase-level board vocabulary in gates/bridge.mjs and is purely
|
|
19
|
+
// additive — nothing here behaves differently because of it.
|
|
20
|
+
//
|
|
16
21
|
// Strict mode ({ requireAnalysis: true }): checked boxes are necessary but weak proof, so
|
|
17
22
|
// Done-eligible additionally requires an analysis report saved as analysis.md in the spec dir
|
|
18
23
|
// (the durable artifact of /speckit.analyze — chat output doesn't count) with no unresolved
|
|
@@ -28,6 +33,30 @@ export const STATUS = {
|
|
|
28
33
|
DONE_ELIGIBLE: "Done-eligible",
|
|
29
34
|
};
|
|
30
35
|
|
|
36
|
+
// The finer-grained lifecycle ladder underneath the 3-status vocabulary. Every derivation
|
|
37
|
+
// also names the stage it is in; the 3 statuses are a fixed collapse of the 5 stages
|
|
38
|
+
// (specifying → To Do; planning/implementing/validating → In Progress; reviewing →
|
|
39
|
+
// Done-eligible), so the stage is strictly additive information — consumers that never look
|
|
40
|
+
// at it see exactly the behavior above.
|
|
41
|
+
export const STAGE = {
|
|
42
|
+
SPECIFYING: "specifying", // no spec.md yet
|
|
43
|
+
PLANNING: "planning", // spec.md present, no plan.md
|
|
44
|
+
IMPLEMENTING: "implementing", // plan.md present, work remaining outside the final phase
|
|
45
|
+
VALIDATING: "validating", // only the final tasks.md phase still has unchecked work,
|
|
46
|
+
// or all boxes checked but strict-mode analysis pending
|
|
47
|
+
REVIEWING: "reviewing", // everything proven — same state as Done-eligible
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** The stages in lifecycle order — the ranking the bridge gate compares against. */
|
|
51
|
+
export const STAGES = [STAGE.SPECIFYING, STAGE.PLANNING, STAGE.IMPLEMENTING, STAGE.VALIDATING, STAGE.REVIEWING];
|
|
52
|
+
|
|
53
|
+
/** The fixed collapse: stage → 3-status. */
|
|
54
|
+
export function coarseStatus(stage) {
|
|
55
|
+
if (stage === STAGE.SPECIFYING) return STATUS.TODO;
|
|
56
|
+
if (stage === STAGE.REVIEWING) return STATUS.DONE_ELIGIBLE;
|
|
57
|
+
return STATUS.IN_PROGRESS;
|
|
58
|
+
}
|
|
59
|
+
|
|
31
60
|
const PHASE_HEADING = /^##\s+(.+?)\s*$/;
|
|
32
61
|
const TASK_LINE = /^\s*[-*]\s+\[([ xX])\]\s+\S/;
|
|
33
62
|
|
|
@@ -100,15 +129,27 @@ export function deriveSpecState(specDir, { requireAnalysis = false } = {}) {
|
|
|
100
129
|
const criticals = requireAnalysis && analysisPresent ? findCriticalFindings(read("analysis.md")) : [];
|
|
101
130
|
const analysisOk = !requireAnalysis || (analysisPresent && criticals.length === 0);
|
|
102
131
|
|
|
103
|
-
|
|
132
|
+
// Stage first, status as its fixed collapse — one ladder, two vocabularies. The stage
|
|
133
|
+
// rules refine "In Progress" without moving any 3-status boundary: Done-eligible is still
|
|
134
|
+
// exactly all-boxes-checked (+ clean analysis in strict mode), To Do is still no spec.md.
|
|
135
|
+
const allChecked = tasksTotal > 0 && tasksDone === tasksTotal;
|
|
136
|
+
let stage = STAGE.SPECIFYING;
|
|
104
137
|
if (has("spec.md")) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
138
|
+
if (!has("plan.md")) stage = STAGE.PLANNING;
|
|
139
|
+
else if (allChecked && analysisOk) stage = STAGE.REVIEWING;
|
|
140
|
+
else {
|
|
141
|
+
// Validating: the only unchecked work left sits in tasks.md's final phase (needs ≥2
|
|
142
|
+
// phases to mean anything), or every box is checked and only the strict-mode analysis
|
|
143
|
+
// artifact is outstanding. Anything earlier is implementing.
|
|
144
|
+
const earlierPhasesDone =
|
|
145
|
+
phases.length >= 2 && phases.slice(0, -1).every((p) => p.done === p.total);
|
|
146
|
+
stage = allChecked || earlierPhasesDone ? STAGE.VALIDATING : STAGE.IMPLEMENTING;
|
|
147
|
+
}
|
|
108
148
|
}
|
|
109
149
|
|
|
110
150
|
return {
|
|
111
|
-
status, phases, tasksDone, tasksTotal,
|
|
151
|
+
status: coarseStatus(stage), stage, phases, tasksDone, tasksTotal,
|
|
152
|
+
progressNote: progressNote(phases),
|
|
112
153
|
analysis: { required: requireAnalysis, present: analysisPresent, criticals },
|
|
113
154
|
};
|
|
114
155
|
}
|
|
@@ -13,6 +13,11 @@
|
|
|
13
13
|
// "Done-eligible" deliberately isn't "Done": the sync skill may move the Backlog task to Done,
|
|
14
14
|
// and the bridge gate treats a Done status without this derivation as a blocking problem.
|
|
15
15
|
//
|
|
16
|
+
// Every derivation also names a finer STAGE (specifying → planning → implementing →
|
|
17
|
+
// validating → reviewing); the status above is a fixed collapse of it (coarseStatus). The
|
|
18
|
+
// stage feeds the opt-in phase-level board vocabulary in gates/bridge.mjs and is purely
|
|
19
|
+
// additive — nothing here behaves differently because of it.
|
|
20
|
+
//
|
|
16
21
|
// Strict mode ({ requireAnalysis: true }): checked boxes are necessary but weak proof, so
|
|
17
22
|
// Done-eligible additionally requires an analysis report saved as analysis.md in the spec dir
|
|
18
23
|
// (the durable artifact of /speckit.analyze — chat output doesn't count) with no unresolved
|
|
@@ -28,6 +33,30 @@ export const STATUS = {
|
|
|
28
33
|
DONE_ELIGIBLE: "Done-eligible",
|
|
29
34
|
};
|
|
30
35
|
|
|
36
|
+
// The finer-grained lifecycle ladder underneath the 3-status vocabulary. Every derivation
|
|
37
|
+
// also names the stage it is in; the 3 statuses are a fixed collapse of the 5 stages
|
|
38
|
+
// (specifying → To Do; planning/implementing/validating → In Progress; reviewing →
|
|
39
|
+
// Done-eligible), so the stage is strictly additive information — consumers that never look
|
|
40
|
+
// at it see exactly the behavior above.
|
|
41
|
+
export const STAGE = {
|
|
42
|
+
SPECIFYING: "specifying", // no spec.md yet
|
|
43
|
+
PLANNING: "planning", // spec.md present, no plan.md
|
|
44
|
+
IMPLEMENTING: "implementing", // plan.md present, work remaining outside the final phase
|
|
45
|
+
VALIDATING: "validating", // only the final tasks.md phase still has unchecked work,
|
|
46
|
+
// or all boxes checked but strict-mode analysis pending
|
|
47
|
+
REVIEWING: "reviewing", // everything proven — same state as Done-eligible
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** The stages in lifecycle order — the ranking the bridge gate compares against. */
|
|
51
|
+
export const STAGES = [STAGE.SPECIFYING, STAGE.PLANNING, STAGE.IMPLEMENTING, STAGE.VALIDATING, STAGE.REVIEWING];
|
|
52
|
+
|
|
53
|
+
/** The fixed collapse: stage → 3-status. */
|
|
54
|
+
export function coarseStatus(stage) {
|
|
55
|
+
if (stage === STAGE.SPECIFYING) return STATUS.TODO;
|
|
56
|
+
if (stage === STAGE.REVIEWING) return STATUS.DONE_ELIGIBLE;
|
|
57
|
+
return STATUS.IN_PROGRESS;
|
|
58
|
+
}
|
|
59
|
+
|
|
31
60
|
const PHASE_HEADING = /^##\s+(.+?)\s*$/;
|
|
32
61
|
const TASK_LINE = /^\s*[-*]\s+\[([ xX])\]\s+\S/;
|
|
33
62
|
|
|
@@ -100,15 +129,27 @@ export function deriveSpecState(specDir, { requireAnalysis = false } = {}) {
|
|
|
100
129
|
const criticals = requireAnalysis && analysisPresent ? findCriticalFindings(read("analysis.md")) : [];
|
|
101
130
|
const analysisOk = !requireAnalysis || (analysisPresent && criticals.length === 0);
|
|
102
131
|
|
|
103
|
-
|
|
132
|
+
// Stage first, status as its fixed collapse — one ladder, two vocabularies. The stage
|
|
133
|
+
// rules refine "In Progress" without moving any 3-status boundary: Done-eligible is still
|
|
134
|
+
// exactly all-boxes-checked (+ clean analysis in strict mode), To Do is still no spec.md.
|
|
135
|
+
const allChecked = tasksTotal > 0 && tasksDone === tasksTotal;
|
|
136
|
+
let stage = STAGE.SPECIFYING;
|
|
104
137
|
if (has("spec.md")) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
138
|
+
if (!has("plan.md")) stage = STAGE.PLANNING;
|
|
139
|
+
else if (allChecked && analysisOk) stage = STAGE.REVIEWING;
|
|
140
|
+
else {
|
|
141
|
+
// Validating: the only unchecked work left sits in tasks.md's final phase (needs ≥2
|
|
142
|
+
// phases to mean anything), or every box is checked and only the strict-mode analysis
|
|
143
|
+
// artifact is outstanding. Anything earlier is implementing.
|
|
144
|
+
const earlierPhasesDone =
|
|
145
|
+
phases.length >= 2 && phases.slice(0, -1).every((p) => p.done === p.total);
|
|
146
|
+
stage = allChecked || earlierPhasesDone ? STAGE.VALIDATING : STAGE.IMPLEMENTING;
|
|
147
|
+
}
|
|
108
148
|
}
|
|
109
149
|
|
|
110
150
|
return {
|
|
111
|
-
status, phases, tasksDone, tasksTotal,
|
|
151
|
+
status: coarseStatus(stage), stage, phases, tasksDone, tasksTotal,
|
|
152
|
+
progressNote: progressNote(phases),
|
|
112
153
|
analysis: { required: requireAnalysis, present: analysisPresent, criticals },
|
|
113
154
|
};
|
|
114
155
|
}
|
package/lib/spec-derive.mjs
CHANGED
|
@@ -13,6 +13,11 @@
|
|
|
13
13
|
// "Done-eligible" deliberately isn't "Done": the sync skill may move the Backlog task to Done,
|
|
14
14
|
// and the bridge gate treats a Done status without this derivation as a blocking problem.
|
|
15
15
|
//
|
|
16
|
+
// Every derivation also names a finer STAGE (specifying → planning → implementing →
|
|
17
|
+
// validating → reviewing); the status above is a fixed collapse of it (coarseStatus). The
|
|
18
|
+
// stage feeds the opt-in phase-level board vocabulary in gates/bridge.mjs and is purely
|
|
19
|
+
// additive — nothing here behaves differently because of it.
|
|
20
|
+
//
|
|
16
21
|
// Strict mode ({ requireAnalysis: true }): checked boxes are necessary but weak proof, so
|
|
17
22
|
// Done-eligible additionally requires an analysis report saved as analysis.md in the spec dir
|
|
18
23
|
// (the durable artifact of /speckit.analyze — chat output doesn't count) with no unresolved
|
|
@@ -28,6 +33,30 @@ export const STATUS = {
|
|
|
28
33
|
DONE_ELIGIBLE: "Done-eligible",
|
|
29
34
|
};
|
|
30
35
|
|
|
36
|
+
// The finer-grained lifecycle ladder underneath the 3-status vocabulary. Every derivation
|
|
37
|
+
// also names the stage it is in; the 3 statuses are a fixed collapse of the 5 stages
|
|
38
|
+
// (specifying → To Do; planning/implementing/validating → In Progress; reviewing →
|
|
39
|
+
// Done-eligible), so the stage is strictly additive information — consumers that never look
|
|
40
|
+
// at it see exactly the behavior above.
|
|
41
|
+
export const STAGE = {
|
|
42
|
+
SPECIFYING: "specifying", // no spec.md yet
|
|
43
|
+
PLANNING: "planning", // spec.md present, no plan.md
|
|
44
|
+
IMPLEMENTING: "implementing", // plan.md present, work remaining outside the final phase
|
|
45
|
+
VALIDATING: "validating", // only the final tasks.md phase still has unchecked work,
|
|
46
|
+
// or all boxes checked but strict-mode analysis pending
|
|
47
|
+
REVIEWING: "reviewing", // everything proven — same state as Done-eligible
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** The stages in lifecycle order — the ranking the bridge gate compares against. */
|
|
51
|
+
export const STAGES = [STAGE.SPECIFYING, STAGE.PLANNING, STAGE.IMPLEMENTING, STAGE.VALIDATING, STAGE.REVIEWING];
|
|
52
|
+
|
|
53
|
+
/** The fixed collapse: stage → 3-status. */
|
|
54
|
+
export function coarseStatus(stage) {
|
|
55
|
+
if (stage === STAGE.SPECIFYING) return STATUS.TODO;
|
|
56
|
+
if (stage === STAGE.REVIEWING) return STATUS.DONE_ELIGIBLE;
|
|
57
|
+
return STATUS.IN_PROGRESS;
|
|
58
|
+
}
|
|
59
|
+
|
|
31
60
|
const PHASE_HEADING = /^##\s+(.+?)\s*$/;
|
|
32
61
|
const TASK_LINE = /^\s*[-*]\s+\[([ xX])\]\s+\S/;
|
|
33
62
|
|
|
@@ -100,15 +129,27 @@ export function deriveSpecState(specDir, { requireAnalysis = false } = {}) {
|
|
|
100
129
|
const criticals = requireAnalysis && analysisPresent ? findCriticalFindings(read("analysis.md")) : [];
|
|
101
130
|
const analysisOk = !requireAnalysis || (analysisPresent && criticals.length === 0);
|
|
102
131
|
|
|
103
|
-
|
|
132
|
+
// Stage first, status as its fixed collapse — one ladder, two vocabularies. The stage
|
|
133
|
+
// rules refine "In Progress" without moving any 3-status boundary: Done-eligible is still
|
|
134
|
+
// exactly all-boxes-checked (+ clean analysis in strict mode), To Do is still no spec.md.
|
|
135
|
+
const allChecked = tasksTotal > 0 && tasksDone === tasksTotal;
|
|
136
|
+
let stage = STAGE.SPECIFYING;
|
|
104
137
|
if (has("spec.md")) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
138
|
+
if (!has("plan.md")) stage = STAGE.PLANNING;
|
|
139
|
+
else if (allChecked && analysisOk) stage = STAGE.REVIEWING;
|
|
140
|
+
else {
|
|
141
|
+
// Validating: the only unchecked work left sits in tasks.md's final phase (needs ≥2
|
|
142
|
+
// phases to mean anything), or every box is checked and only the strict-mode analysis
|
|
143
|
+
// artifact is outstanding. Anything earlier is implementing.
|
|
144
|
+
const earlierPhasesDone =
|
|
145
|
+
phases.length >= 2 && phases.slice(0, -1).every((p) => p.done === p.total);
|
|
146
|
+
stage = allChecked || earlierPhasesDone ? STAGE.VALIDATING : STAGE.IMPLEMENTING;
|
|
147
|
+
}
|
|
108
148
|
}
|
|
109
149
|
|
|
110
150
|
return {
|
|
111
|
-
status, phases, tasksDone, tasksTotal,
|
|
151
|
+
status: coarseStatus(stage), stage, phases, tasksDone, tasksTotal,
|
|
152
|
+
progressNote: progressNote(phases),
|
|
112
153
|
analysis: { required: requireAnalysis, present: analysisPresent, criticals },
|
|
113
154
|
};
|
|
114
155
|
}
|
package/package.json
CHANGED
|
@@ -12,22 +12,77 @@
|
|
|
12
12
|
// ok — they agree.
|
|
13
13
|
// unknown — the task uses a status outside To Do / In Progress / Done (custom workflow);
|
|
14
14
|
// the bridge doesn't guess, so it neither blocks nor warns.
|
|
15
|
+
//
|
|
16
|
+
// A project MAY opt into a finer, phase-level status vocabulary via `statusVocabulary` in
|
|
17
|
+
// `.spec-bridge.json` (see vocabularyProfile): the same four verdicts, ranked on the
|
|
18
|
+
// derivation-stage ladder against the board's own status names. Absent that config, every
|
|
19
|
+
// path below behaves exactly as described above.
|
|
15
20
|
|
|
16
21
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
17
22
|
import { join } from "node:path";
|
|
18
|
-
import { deriveSpecState, STATUS } from "../lib/spec-derive.mjs";
|
|
23
|
+
import { deriveSpecState, STATUS, STAGE, STAGES } from "../lib/spec-derive.mjs";
|
|
19
24
|
import { hasChild, findRootsDownwards } from "../lib/project-root.mjs";
|
|
20
25
|
|
|
21
26
|
/**
|
|
22
27
|
* Per-project bridge config: `.spec-bridge.json` at the project root (beside backlog/).
|
|
23
|
-
* `{ "strictDone": true }` turns on analyze-gated Done (see lib/spec-derive.mjs)
|
|
24
|
-
*
|
|
28
|
+
* `{ "strictDone": true }` turns on analyze-gated Done (see lib/spec-derive.mjs);
|
|
29
|
+
* `"statusVocabulary"` opts the board into phase-level status names (see
|
|
30
|
+
* vocabularyProfile below). Missing or malformed config means checkbox-only mode with the
|
|
31
|
+
* 3-status vocabulary — everything finer is opt-in.
|
|
25
32
|
*/
|
|
26
33
|
export function loadBridgeConfig(root) {
|
|
27
34
|
try { return JSON.parse(readFileSync(join(root, ".spec-bridge.json"), "utf8")) ?? {}; }
|
|
28
35
|
catch { return {}; }
|
|
29
36
|
}
|
|
30
37
|
|
|
38
|
+
/**
|
|
39
|
+
* What each derivation stage is called on a board that has NOT renamed it. This is exactly
|
|
40
|
+
* the 3-status collapse ("reviewing" is named Done because the sync skill's only move from
|
|
41
|
+
* there is `-s Done`), which is why a statusVocabulary that renames nothing behaves
|
|
42
|
+
* bit-for-bit like no statusVocabulary at all.
|
|
43
|
+
*/
|
|
44
|
+
export const DEFAULT_STAGE_NAMES = {
|
|
45
|
+
[STAGE.SPECIFYING]: "To Do",
|
|
46
|
+
[STAGE.PLANNING]: "In Progress",
|
|
47
|
+
[STAGE.IMPLEMENTING]: "In Progress",
|
|
48
|
+
[STAGE.VALIDATING]: "In Progress",
|
|
49
|
+
[STAGE.REVIEWING]: "Done",
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The opt-in phase-level vocabulary, normalized. `.spec-bridge.json` may carry
|
|
54
|
+
* { "statusVocabulary": { "<stage>": "<board status name>", ... } }
|
|
55
|
+
* mapping any of the derivation stages (specifying / planning / implementing / validating /
|
|
56
|
+
* reviewing) to the consumer board's own status names; unmapped stages keep their
|
|
57
|
+
* DEFAULT_STAGE_NAMES. Returns null — 3-status behavior, unchanged — unless at least one
|
|
58
|
+
* stage is validly renamed (string values only; unknown keys ignored).
|
|
59
|
+
*
|
|
60
|
+
* The profile carries:
|
|
61
|
+
* names — stage → board status name (defaults overlaid with the config)
|
|
62
|
+
* cover — lowercase board status name → { min, max } span of stage ranks it stands for
|
|
63
|
+
* (a name used for several stages honestly covers all of them; "Done" always
|
|
64
|
+
* covers at least the top stage, so Done-eligibility is unchanged by opting in)
|
|
65
|
+
*/
|
|
66
|
+
export function vocabularyProfile(config) {
|
|
67
|
+
const raw = config?.statusVocabulary;
|
|
68
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
69
|
+
const names = { ...DEFAULT_STAGE_NAMES };
|
|
70
|
+
let renamed = false;
|
|
71
|
+
for (const key of STAGES) {
|
|
72
|
+
if (typeof raw[key] === "string" && raw[key].trim()) { names[key] = raw[key].trim(); renamed = true; }
|
|
73
|
+
}
|
|
74
|
+
if (!renamed) return null;
|
|
75
|
+
const top = STAGES.length - 1;
|
|
76
|
+
const cover = new Map([["done", { min: top, max: top }]]);
|
|
77
|
+
STAGES.forEach((stageKey, rank) => {
|
|
78
|
+
const name = names[stageKey].toLowerCase();
|
|
79
|
+
const c = cover.get(name);
|
|
80
|
+
if (!c) cover.set(name, { min: rank, max: rank });
|
|
81
|
+
else { c.min = Math.min(c.min, rank); c.max = Math.max(c.max, rank); }
|
|
82
|
+
});
|
|
83
|
+
return { names, cover };
|
|
84
|
+
}
|
|
85
|
+
|
|
31
86
|
const MARKER = /^Spec:\s*(\S+?)\/?\s*$/m;
|
|
32
87
|
const RANK = { "to do": 0, "in progress": 1, done: 2 };
|
|
33
88
|
const DERIVED_RANK = { [STATUS.TODO]: 0, [STATUS.IN_PROGRESS]: 1, [STATUS.DONE_ELIGIBLE]: 2 };
|
|
@@ -79,6 +134,20 @@ export function verdict(taskStatus, derivedStatus) {
|
|
|
79
134
|
return t > d ? "exceeds" : t < d ? "lags" : "ok";
|
|
80
135
|
}
|
|
81
136
|
|
|
137
|
+
/**
|
|
138
|
+
* The same comparison at phase grain, against an opted-in vocabulary profile: a board status
|
|
139
|
+
* exceeds when even the EARLIEST stage it stands for is later than the derived stage, lags
|
|
140
|
+
* when even the LATEST is earlier, is ok anywhere inside its span, and is unknown when the
|
|
141
|
+
* status isn't in the vocabulary (custom workflow: don't guess) — verdict()'s semantics,
|
|
142
|
+
* finer ruler.
|
|
143
|
+
*/
|
|
144
|
+
export function stageVerdict(taskStatus, derivedStage, profile) {
|
|
145
|
+
const c = profile.cover.get(String(taskStatus).toLowerCase());
|
|
146
|
+
const d = STAGES.indexOf(derivedStage);
|
|
147
|
+
if (!c || d < 0) return "unknown";
|
|
148
|
+
return c.min > d ? "exceeds" : c.max < d ? "lags" : "ok";
|
|
149
|
+
}
|
|
150
|
+
|
|
82
151
|
/** One human sentence on why a spec dir doesn't prove more than its derived status. */
|
|
83
152
|
function shortfall(root, specDir, derived) {
|
|
84
153
|
const missing = ["spec.md", "plan.md"].filter((f) => !existsSync(join(root, specDir, f)));
|
|
@@ -105,19 +174,24 @@ export function checkBridge(root) {
|
|
|
105
174
|
const links = [];
|
|
106
175
|
const problems = [];
|
|
107
176
|
const warnings = [];
|
|
108
|
-
const
|
|
177
|
+
const config = loadBridgeConfig(root);
|
|
178
|
+
const requireAnalysis = config.strictDone === true;
|
|
179
|
+
const profile = vocabularyProfile(config);
|
|
109
180
|
for (const task of findLinkedTasks(root)) {
|
|
110
181
|
const derived = deriveSpecState(join(root, task.specDir), { requireAnalysis });
|
|
111
|
-
|
|
182
|
+
// Opted-in boards are judged on the stage ladder against their own status names;
|
|
183
|
+
// everyone else gets the 3-status comparison, untouched.
|
|
184
|
+
const v = profile ? stageVerdict(task.status, derived.stage, profile) : verdict(task.status, derived.status);
|
|
185
|
+
const proven = profile ? profile.names[derived.stage] : derived.status;
|
|
112
186
|
links.push({ ...task, derived, verdict: v });
|
|
113
187
|
if (v === "exceeds") {
|
|
114
188
|
problems.push(
|
|
115
|
-
`[spec-bridge] ${task.id} is "${task.status}" but ${task.specDir} only proves "${
|
|
189
|
+
`[spec-bridge] ${task.id} is "${task.status}" but ${task.specDir} only proves "${proven}": ` +
|
|
116
190
|
`${shortfall(root, task.specDir, derived)}. Finish the spec work or set the task back (backlog task edit ${task.id} -s "...").`
|
|
117
191
|
);
|
|
118
192
|
} else if (v === "lags") {
|
|
119
193
|
warnings.push(
|
|
120
|
-
`[spec-bridge] ${task.id} is "${task.status}" but ${task.specDir} already derives "${
|
|
194
|
+
`[spec-bridge] ${task.id} is "${task.status}" but ${task.specDir} already derives "${proven}" — run the spec-bridge sync skill to catch the board up.`
|
|
121
195
|
);
|
|
122
196
|
} else if (
|
|
123
197
|
// Strict-mode near-miss: the status is honest ("ok"), every checkbox is checked, and
|
|
@@ -156,13 +230,22 @@ const sq = (s) => `'${String(s).replace(/'/g, "'\\''")}'`;
|
|
|
156
230
|
* removals (highest index first, so earlier indexes stay valid) → phase-AC additions →
|
|
157
231
|
* check/uncheck at post-edit indexes → one progress note (only when something changed).
|
|
158
232
|
* ACs that don't start with "Spec phase: " are human-authored and are never touched.
|
|
233
|
+
*
|
|
234
|
+
* With an opted-in vocabulary profile (third argument), status targets are the profile's
|
|
235
|
+
* stage names instead of the 3-status collapse. Done keeps its meaning: a board that leaves
|
|
236
|
+
* "reviewing" at its default still plans `-s Done` with the derived final summary; a board
|
|
237
|
+
* that names it (say "In Review") is planned to that name, and moving to Done stays a
|
|
238
|
+
* human/consumer act the gate already accepts (Done never exceeds a fully-proven spec).
|
|
159
239
|
*/
|
|
160
|
-
export function planLinkedTask(task, derived) {
|
|
240
|
+
export function planLinkedTask(task, derived, profile = null) {
|
|
161
241
|
const cmds = [];
|
|
162
242
|
const edit = (args) => cmds.push(`backlog task edit ${task.id} ${args}`);
|
|
163
243
|
|
|
164
244
|
// Status — Done-eligible is the only path to Done and carries the derived final summary.
|
|
165
|
-
const
|
|
245
|
+
const mapped = profile ? profile.names[derived.stage] : null;
|
|
246
|
+
const target = derived.status === STATUS.DONE_ELIGIBLE
|
|
247
|
+
? (mapped && mapped.toLowerCase() !== "done" ? mapped : "Done")
|
|
248
|
+
: (mapped ?? derived.status);
|
|
166
249
|
const statusChanged = String(task.status).toLowerCase() !== target.toLowerCase();
|
|
167
250
|
if (statusChanged) {
|
|
168
251
|
if (target === "Done")
|
|
@@ -218,11 +301,14 @@ export function planLinkedTask(task, derived) {
|
|
|
218
301
|
export function planBridge(root) {
|
|
219
302
|
const commands = [];
|
|
220
303
|
const skipped = [];
|
|
221
|
-
const
|
|
304
|
+
const config = loadBridgeConfig(root);
|
|
305
|
+
const requireAnalysis = config.strictDone === true;
|
|
306
|
+
const profile = vocabularyProfile(config);
|
|
222
307
|
for (const task of findLinkedTasks(root)) {
|
|
223
308
|
const derived = deriveSpecState(join(root, task.specDir), { requireAnalysis });
|
|
224
|
-
|
|
225
|
-
|
|
309
|
+
const v = profile ? stageVerdict(task.status, derived.stage, profile) : verdict(task.status, derived.status);
|
|
310
|
+
if (v === "unknown") { skipped.push({ id: task.id, status: task.status }); continue; }
|
|
311
|
+
commands.push(...planLinkedTask(task, derived, profile));
|
|
226
312
|
}
|
|
227
313
|
return { commands, skipped };
|
|
228
314
|
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { resolve } from "node:path";
|
|
11
11
|
import { deriveSpecState } from "../lib/spec-derive.mjs";
|
|
12
12
|
import { findRootUpwards, hasChild } from "../lib/project-root.mjs";
|
|
13
|
-
import { checkBridge, loadBridgeConfig, planBridge } from "./bridge.mjs";
|
|
13
|
+
import { checkBridge, loadBridgeConfig, planBridge, vocabularyProfile } from "./bridge.mjs";
|
|
14
14
|
|
|
15
15
|
const [cmd, target] = process.argv.slice(2);
|
|
16
16
|
if (!cmd || !target) {
|
|
@@ -36,8 +36,14 @@ if (cmd === "state") {
|
|
|
36
36
|
console.log(`spec-bridge ok: ${links.length} linked task(s), none exceed their artifacts`);
|
|
37
37
|
} else if (cmd === "plan") {
|
|
38
38
|
const { commands, skipped } = planBridge(target);
|
|
39
|
+
// Name the vocabulary the board actually speaks: the opted-in phase-level names when
|
|
40
|
+
// .spec-bridge.json carries a statusVocabulary, the 3-status default otherwise.
|
|
41
|
+
const profile = vocabularyProfile(loadBridgeConfig(target));
|
|
42
|
+
const vocab = profile
|
|
43
|
+
? [...new Set([...Object.values(profile.names), "Done"])].join("/")
|
|
44
|
+
: "To Do/In Progress/Done";
|
|
39
45
|
for (const s of skipped)
|
|
40
|
-
console.error(`# ${s.id}: status "${s.status}" is outside
|
|
46
|
+
console.error(`# ${s.id}: status "${s.status}" is outside ${vocab} — not planned; resolve by hand`);
|
|
41
47
|
for (const c of commands) console.log(c);
|
|
42
48
|
} else {
|
|
43
49
|
console.error(`unknown command: ${cmd}`);
|
|
@@ -13,6 +13,11 @@
|
|
|
13
13
|
// "Done-eligible" deliberately isn't "Done": the sync skill may move the Backlog task to Done,
|
|
14
14
|
// and the bridge gate treats a Done status without this derivation as a blocking problem.
|
|
15
15
|
//
|
|
16
|
+
// Every derivation also names a finer STAGE (specifying → planning → implementing →
|
|
17
|
+
// validating → reviewing); the status above is a fixed collapse of it (coarseStatus). The
|
|
18
|
+
// stage feeds the opt-in phase-level board vocabulary in gates/bridge.mjs and is purely
|
|
19
|
+
// additive — nothing here behaves differently because of it.
|
|
20
|
+
//
|
|
16
21
|
// Strict mode ({ requireAnalysis: true }): checked boxes are necessary but weak proof, so
|
|
17
22
|
// Done-eligible additionally requires an analysis report saved as analysis.md in the spec dir
|
|
18
23
|
// (the durable artifact of /speckit.analyze — chat output doesn't count) with no unresolved
|
|
@@ -28,6 +33,30 @@ export const STATUS = {
|
|
|
28
33
|
DONE_ELIGIBLE: "Done-eligible",
|
|
29
34
|
};
|
|
30
35
|
|
|
36
|
+
// The finer-grained lifecycle ladder underneath the 3-status vocabulary. Every derivation
|
|
37
|
+
// also names the stage it is in; the 3 statuses are a fixed collapse of the 5 stages
|
|
38
|
+
// (specifying → To Do; planning/implementing/validating → In Progress; reviewing →
|
|
39
|
+
// Done-eligible), so the stage is strictly additive information — consumers that never look
|
|
40
|
+
// at it see exactly the behavior above.
|
|
41
|
+
export const STAGE = {
|
|
42
|
+
SPECIFYING: "specifying", // no spec.md yet
|
|
43
|
+
PLANNING: "planning", // spec.md present, no plan.md
|
|
44
|
+
IMPLEMENTING: "implementing", // plan.md present, work remaining outside the final phase
|
|
45
|
+
VALIDATING: "validating", // only the final tasks.md phase still has unchecked work,
|
|
46
|
+
// or all boxes checked but strict-mode analysis pending
|
|
47
|
+
REVIEWING: "reviewing", // everything proven — same state as Done-eligible
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** The stages in lifecycle order — the ranking the bridge gate compares against. */
|
|
51
|
+
export const STAGES = [STAGE.SPECIFYING, STAGE.PLANNING, STAGE.IMPLEMENTING, STAGE.VALIDATING, STAGE.REVIEWING];
|
|
52
|
+
|
|
53
|
+
/** The fixed collapse: stage → 3-status. */
|
|
54
|
+
export function coarseStatus(stage) {
|
|
55
|
+
if (stage === STAGE.SPECIFYING) return STATUS.TODO;
|
|
56
|
+
if (stage === STAGE.REVIEWING) return STATUS.DONE_ELIGIBLE;
|
|
57
|
+
return STATUS.IN_PROGRESS;
|
|
58
|
+
}
|
|
59
|
+
|
|
31
60
|
const PHASE_HEADING = /^##\s+(.+?)\s*$/;
|
|
32
61
|
const TASK_LINE = /^\s*[-*]\s+\[([ xX])\]\s+\S/;
|
|
33
62
|
|
|
@@ -100,15 +129,27 @@ export function deriveSpecState(specDir, { requireAnalysis = false } = {}) {
|
|
|
100
129
|
const criticals = requireAnalysis && analysisPresent ? findCriticalFindings(read("analysis.md")) : [];
|
|
101
130
|
const analysisOk = !requireAnalysis || (analysisPresent && criticals.length === 0);
|
|
102
131
|
|
|
103
|
-
|
|
132
|
+
// Stage first, status as its fixed collapse — one ladder, two vocabularies. The stage
|
|
133
|
+
// rules refine "In Progress" without moving any 3-status boundary: Done-eligible is still
|
|
134
|
+
// exactly all-boxes-checked (+ clean analysis in strict mode), To Do is still no spec.md.
|
|
135
|
+
const allChecked = tasksTotal > 0 && tasksDone === tasksTotal;
|
|
136
|
+
let stage = STAGE.SPECIFYING;
|
|
104
137
|
if (has("spec.md")) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
138
|
+
if (!has("plan.md")) stage = STAGE.PLANNING;
|
|
139
|
+
else if (allChecked && analysisOk) stage = STAGE.REVIEWING;
|
|
140
|
+
else {
|
|
141
|
+
// Validating: the only unchecked work left sits in tasks.md's final phase (needs ≥2
|
|
142
|
+
// phases to mean anything), or every box is checked and only the strict-mode analysis
|
|
143
|
+
// artifact is outstanding. Anything earlier is implementing.
|
|
144
|
+
const earlierPhasesDone =
|
|
145
|
+
phases.length >= 2 && phases.slice(0, -1).every((p) => p.done === p.total);
|
|
146
|
+
stage = allChecked || earlierPhasesDone ? STAGE.VALIDATING : STAGE.IMPLEMENTING;
|
|
147
|
+
}
|
|
108
148
|
}
|
|
109
149
|
|
|
110
150
|
return {
|
|
111
|
-
status, phases, tasksDone, tasksTotal,
|
|
151
|
+
status: coarseStatus(stage), stage, phases, tasksDone, tasksTotal,
|
|
152
|
+
progressNote: progressNote(phases),
|
|
112
153
|
analysis: { required: requireAnalysis, present: analysisPresent, criticals },
|
|
113
154
|
};
|
|
114
155
|
}
|