@khoavu882/doflow 2.0.0-beta.3 → 2.0.0-beta.5
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/bin/doflow.js +2 -2
- package/core/.antigravity-plugin/plugin.json +0 -1
- package/core/.claude-plugin/plugin.json +1 -1
- package/core/.codex-plugin/plugin.json +1 -1
- package/core/.plugin/plugin.json +1 -1
- package/core/harnesses/claude/hooks/mcp-tool-guard.sh +7 -17
- package/core/harnesses/claude/hooks/pre-bash-guard.sh +7 -17
- package/core/harnesses/claude/hooks/pre-implement-gate.sh +7 -18
- package/core/harnesses/claude/hooks/session-start.sh +5 -3
- package/core/harnesses/claude/hooks/stop-check.sh +5 -3
- package/core/harnesses/claude/hooks/subagent-audit.sh +5 -5
- package/core/harnesses/claude/hooks/user-prompt-submit.sh +5 -3
- package/core/harnesses/codex/hooks/mcp-tool-guard.sh +7 -15
- package/core/harnesses/codex/hooks/pre-bash-guard.sh +7 -15
- package/core/harnesses/codex/hooks/pre-implement-gate.sh +7 -16
- package/core/harnesses/codex/hooks/session-start.sh +5 -3
- package/core/harnesses/codex/hooks/stop-check.sh +6 -5
- package/core/harnesses/codex/hooks/subagent-audit.sh +17 -6
- package/core/harnesses/codex/hooks/user-prompt-submit.sh +5 -4
- package/core/harnesses/gemini/hooks/mcp-tool-guard.sh +7 -18
- package/core/harnesses/gemini/hooks/pre-bash-guard.sh +7 -18
- package/core/harnesses/gemini/hooks/pre-implement-gate.sh +7 -19
- package/core/harnesses/gemini/hooks/session-start.sh +5 -3
- package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +20 -5
- package/core/harnesses/kiro/hooks/pre-bash-guard.sh +20 -5
- package/core/harnesses/kiro/hooks/pre-implement-gate.sh +20 -5
- package/core/harnesses/kiro/hooks/session-start.sh +11 -5
- package/core/harnesses/kiro/hooks/stop-check.sh +21 -30
- package/core/harnesses/pi/extensions/doflow-guard.js +159 -0
- package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +27 -11
- package/core/harnesses/shared/hooks/adapters/antigravity.js +14 -57
- package/core/harnesses/shared/hooks/bridge/bridge.sh +184 -0
- package/core/harnesses/shared/hooks/bridge/envelope.jq +60 -0
- package/core/harnesses/shared/hooks/bridge/respond.sh +62 -0
- package/core/harnesses/shared/hooks/policies/capture-failure.sh +4 -2
- package/core/harnesses/shared/hooks/policies/contract.sh +75 -0
- package/core/harnesses/shared/hooks/policies/deny-json.sh +39 -4
- package/core/harnesses/shared/hooks/policies/lib.sh +4 -2
- package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +46 -34
- package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +75 -25
- package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +67 -73
- package/core/harnesses/shared/hooks/policies/session-context.sh +37 -7
- package/core/harnesses/shared/hooks/policies/stop-check.sh +42 -15
- package/core/harnesses/shared/hooks/policies/subagent-audit.sh +35 -5
- package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +40 -7
- package/core/harnesses/shared/hooks/stream-hook-runner.js +69 -181
- package/core/registry/assets.json +19 -0
- package/core/registry/contracts.json +2 -2
- package/core/registry/harnesses.json +196 -10
- package/core/registry/lifecycle.json +2626 -7
- package/core/registry/storage.json +110 -83
- package/core/registry/verification.json +1 -1
- package/core/shared/scripts/doflow/bash/render-puml.sh +2 -2
- package/core/shared/scripts/doflow/bin/doflow-run +10 -6
- package/core/shared/skills/do-design/SKILL.md +6 -6
- package/core/shared/skills/do-execute-plan/SKILL.md +11 -10
- package/core/shared/skills/do-execute-plan/references/readiness_gate.md +2 -0
- package/core/shared/skills/do-execute-plan/references/scaffold.md +2 -2
- package/core/shared/skills/do-implement/SKILL.md +3 -2
- package/core/shared/skills/do-plan/SKILL.md +7 -8
- package/package.json +4 -2
- package/src/adapters/antigravity/{index.ts → index.js} +141 -105
- package/src/adapters/claude/{index.ts → index.js} +63 -49
- package/src/adapters/claude/{mcp.ts → mcp.js} +16 -16
- package/src/adapters/codex/{agents.ts → agents.js} +24 -24
- package/src/adapters/codex/{config.ts → config.js} +49 -49
- package/src/adapters/codex/{hooks.ts → hooks.js} +62 -27
- package/src/adapters/codex/{index.ts → index.js} +148 -105
- package/src/adapters/codex/{mcp.ts → mcp.js} +38 -38
- package/src/adapters/codex/{native-projection.ts → native-projection.js} +5 -5
- package/src/adapters/copilot/{index.ts → index.js} +42 -42
- package/src/adapters/{copy-tree.ts → copy-tree.js} +74 -74
- package/src/adapters/gemini/{hooks.ts → hooks.js} +78 -37
- package/src/adapters/gemini/{index.ts → index.js} +96 -57
- package/src/adapters/{hook-commands.ts → hook-commands.js} +8 -8
- package/src/adapters/{index.ts → index.js} +13 -13
- package/src/adapters/kiro/{index.ts → index.js} +49 -35
- package/src/adapters/{mcp-entries.ts → mcp-entries.js} +55 -55
- package/src/adapters/opencode/{index.ts → index.js} +52 -52
- package/src/adapters/pi/hooks.js +119 -0
- package/src/adapters/pi/{index.ts → index.js} +64 -39
- package/src/adapters/pi/{json-members.ts → json-members.js} +23 -23
- package/src/adapters/pi/{mcp.ts → mcp.js} +70 -70
- package/src/adapters/{resolve-source.ts → resolve-source.js} +1 -1
- package/src/cli/commands/{install.ts → install.js} +25 -25
- package/src/cli/commands/{list-backups.ts → list-backups.js} +4 -4
- package/src/cli/commands/{reconcile.ts → reconcile.js} +19 -19
- package/src/cli/commands/{remove.ts → remove.js} +10 -10
- package/src/cli/commands/{rollback.ts → rollback.js} +17 -17
- package/src/cli/commands/{self-update.ts → self-update.js} +3 -3
- package/src/cli/commands/{status.ts → status.js} +24 -24
- package/src/cli/commands/{tools.ts → tools.js} +16 -16
- package/src/cli/commands/{update.ts → update.js} +20 -20
- package/src/cli/{index.ts → index.js} +43 -31
- package/src/cli/{runtime-commands.ts → runtime-commands.js} +97 -58
- package/src/cli/{shared.ts → shared.js} +40 -40
- package/src/helper/{bash-helper.ts → bash-helper.js} +7 -7
- package/src/helper/{git.ts → git.js} +2 -2
- package/src/helper/{guardrails.ts → guardrails.js} +10 -10
- package/src/helper/{harness-paths.ts → harness-paths.js} +24 -24
- package/src/helper/{marker-merge.ts → marker-merge.js} +4 -4
- package/src/helper/{prompt.ts → prompt.js} +5 -5
- package/src/helper/{repo-root.ts → repo-root.js} +2 -2
- package/src/helper/{settings-merge.ts → settings-merge.js} +27 -27
- package/src/helper/{settings-scope.ts → settings-scope.js} +3 -3
- package/src/helper/{toml.ts → toml.js} +48 -48
- package/src/install/{backup-set.ts → backup-set.js} +16 -16
- package/src/install/{backup.ts → backup.js} +65 -65
- package/src/install/{context.ts → context.js} +15 -15
- package/src/install/{manifest.ts → manifest.js} +27 -27
- package/src/install/{mcp.ts → mcp.js} +30 -30
- package/src/install/{paths.ts → paths.js} +15 -15
- package/src/install/{targets.ts → targets.js} +10 -10
- package/src/install/{tool-lifecycle.ts → tool-lifecycle.js} +20 -20
- package/src/lifecycle/{bash-availability.ts → bash-availability.js} +1 -1
- package/src/lifecycle/coverage.js +57 -0
- package/src/lifecycle/hook-step.js +61 -0
- package/src/lifecycle/{index.ts → index.js} +160 -146
- package/src/lifecycle/{mcp-index.ts → mcp-index.js} +1 -1
- package/src/lifecycle/{policies.ts → policies.js} +11 -11
- package/src/lifecycle/{view.ts → view.js} +28 -28
- package/src/registry/{index.ts → index.js} +195 -66
- package/src/release/build-package.js +91 -0
- package/src/release/changelog-section.js +70 -0
- package/src/release/{sync-plugin-versions.ts → sync-plugin-versions.js} +4 -4
- package/src/runtime/advice/{cli.ts → cli.js} +31 -31
- package/src/runtime/advice/{client.ts → client.js} +44 -44
- package/src/runtime/advice/{config.ts → config.js} +16 -16
- package/src/runtime/advice/{doctor.ts → doctor.js} +32 -32
- package/src/runtime/advice/{endpoint-guard.ts → endpoint-guard.js} +20 -20
- package/src/runtime/advice/{kinds.ts → kinds.js} +16 -16
- package/src/runtime/advice/providers/{ollama-systemone.ts → ollama-systemone.js} +21 -21
- package/src/runtime/advice/{record.ts → record.js} +19 -19
- package/src/runtime/advice/{registry.ts → registry.js} +16 -16
- package/src/runtime/advice/{strength.ts → strength.js} +7 -7
- package/src/runtime/{brief-tools.ts → brief-tools.js} +50 -50
- package/src/runtime/{c4-project.ts → c4-project.js} +27 -27
- package/src/runtime/candidate.js +505 -0
- package/src/runtime/{capability-router.ts → capability-router.js} +39 -39
- package/src/runtime/{checkouts.ts → checkouts.js} +21 -21
- package/src/runtime/{claims.ts → claims.js} +44 -35
- package/src/runtime/{cli-result.ts → cli-result.js} +15 -8
- package/src/runtime/{cli.ts → cli.js} +59 -292
- package/src/runtime/{command-detect.ts → command-detect.js} +47 -47
- package/src/runtime/context/budget.js +203 -0
- package/src/runtime/context/compose.js +1367 -0
- package/src/runtime/context/delivery-store.js +102 -0
- package/src/runtime/context/mandatory.js +594 -0
- package/src/runtime/context/pointer.js +142 -0
- package/src/runtime/context-pack.js +1277 -0
- package/src/runtime/{decision-register.ts → decision-register.js} +67 -67
- package/src/runtime/evidence-fields.js +39 -0
- package/src/runtime/{evidence-ledger.ts → evidence-ledger.js} +32 -25
- package/src/runtime/failure/{capture.ts → capture.js} +29 -29
- package/src/runtime/failure/{classifier.ts → classifier.js} +4 -4
- package/src/runtime/failure/{cli.ts → cli.js} +30 -30
- package/src/runtime/failure/{home.ts → home.js} +6 -6
- package/src/runtime/failure/{store.ts → store.js} +32 -32
- package/src/runtime/{feature-resolve.ts → feature-resolve.js} +6 -6
- package/src/runtime/{freshness.ts → freshness.js} +59 -22
- package/src/runtime/{health.ts → health.js} +72 -72
- package/src/runtime/{history-compactor.ts → history-compactor.js} +25 -25
- package/src/runtime/{implementation-gate.ts → implementation-gate.js} +30 -30
- package/src/runtime/inventory/{coverage.ts → coverage.js} +10 -10
- package/src/runtime/inventory/{identity.ts → identity.js} +2 -2
- package/src/runtime/inventory/{index.ts → index.js} +36 -36
- package/src/runtime/inventory/{precedence.ts → precedence.js} +2 -2
- package/src/runtime/inventory/{read-scopes.ts → read-scopes.js} +9 -9
- package/src/runtime/inventory/{siblings.ts → siblings.js} +15 -15
- package/src/runtime/knowledge/{chunker.ts → chunker.js} +12 -12
- package/src/runtime/knowledge/{hybrid.ts → hybrid.js} +17 -17
- package/src/runtime/knowledge/{index-store.ts → index-store.js} +23 -23
- package/src/runtime/knowledge/{retrieval.ts → retrieval.js} +27 -27
- package/src/runtime/{leak-scan.ts → leak-scan.js} +12 -12
- package/src/runtime/lifecycle/{cli.ts → cli.js} +32 -32
- package/src/runtime/lifecycle/{event-store.ts → event-store.js} +48 -48
- package/src/runtime/lifecycle/{fold.ts → fold.js} +41 -41
- package/src/runtime/lifecycle/{followup.ts → followup.js} +54 -54
- package/src/runtime/lifecycle/{goal.ts → goal.js} +50 -50
- package/src/runtime/lifecycle/{intent-writer.ts → intent-writer.js} +5 -5
- package/src/runtime/lifecycle/{overview.ts → overview.js} +47 -47
- package/src/runtime/lifecycle/{release.ts → release.js} +29 -29
- package/src/runtime/lifecycle/{report-store.ts → report-store.js} +25 -25
- package/src/runtime/lifecycle/{retention.ts → retention.js} +37 -37
- package/src/runtime/lifecycle/{root.ts → root.js} +9 -9
- package/src/runtime/lifecycle/{status.ts → status.js} +64 -64
- package/src/runtime/lifecycle/{store-upkeep.ts → store-upkeep.js} +46 -46
- package/src/runtime/{locator-resolve.ts → locator-resolve.js} +7 -7
- package/src/runtime/{mask.ts → mask.js} +22 -22
- package/src/runtime/{mcp-consultations.ts → mcp-consultations.js} +54 -54
- package/src/runtime/{mcp-reach.ts → mcp-reach.js} +24 -24
- package/src/runtime/{model-router.ts → model-router.js} +20 -20
- package/src/runtime/{outcome.ts → outcome.js} +27 -27
- package/src/runtime/{reach.ts → reach.js} +21 -21
- package/src/runtime/{readiness-record.ts → readiness-record.js} +12 -12
- package/src/runtime/{readiness.ts → readiness.js} +113 -43
- package/src/runtime/{recovery.ts → recovery.js} +15 -15
- package/src/runtime/{research-request.ts → research-request.js} +34 -34
- package/src/runtime/{retrieval-plan.ts → retrieval-plan.js} +453 -104
- package/src/runtime/retrieval-record.js +223 -0
- package/src/runtime/scaffold/{artifacts.ts → artifacts.js} +22 -19
- package/src/runtime/scaffold/{fingerprint.ts → fingerprint.js} +3 -3
- package/src/runtime/scaffold/{generate.ts → generate.js} +109 -109
- package/src/runtime/scaffold/{languages.ts → languages.js} +34 -34
- package/src/runtime/source-version.js +289 -0
- package/src/runtime/storage/{cli.ts → cli.js} +9 -9
- package/src/runtime/storage/{engine.ts → engine.js} +37 -37
- package/src/runtime/storage/{registry.ts → registry.js} +19 -19
- package/src/runtime/storage/{roots.ts → roots.js} +8 -8
- package/src/runtime/storage/{rules.ts → rules.js} +26 -26
- package/src/runtime/storage/{upkeep.ts → upkeep.js} +35 -35
- package/src/runtime/storage/{work-status.ts → work-status.js} +39 -39
- package/src/runtime/{task-attempts.ts → task-attempts.js} +62 -62
- package/src/runtime/{task-classifier.ts → task-classifier.js} +34 -34
- package/src/runtime/{task-scope.ts → task-scope.js} +40 -15
- package/src/runtime/{task-state.ts → task-state.js} +86 -21
- package/src/runtime/trace/{indicators.ts → indicators.js} +22 -22
- package/src/runtime/trace/{ledger.ts → ledger.js} +42 -42
- package/src/runtime/trace/{render.ts → render.js} +7 -7
- package/src/runtime/trace/{views.ts → views.js} +40 -40
- package/src/runtime/verification/{contract-runner.ts → contract-runner.js} +23 -23
- package/src/runtime/verification/{engine.ts → engine.js} +143 -143
- package/src/runtime/verification/{registry.ts → registry.js} +6 -6
- package/src/runtime/verification/{scope-bound.ts → scope-bound.js} +50 -50
- package/src/runtime/verification/{test-selection.ts → test-selection.js} +28 -28
- package/src/runtime/{workflow-engine.ts → workflow-engine.js} +50 -50
- package/src/runtime/{workflow-orchestrator.ts → workflow-orchestrator.js} +91 -91
- package/src/runtime/{worktree.ts → worktree.js} +31 -31
- package/src/state/{index.ts → index.js} +26 -26
- package/src/state/{lockfile.ts → lockfile.js} +20 -20
- package/src/state/{run-lock.ts → run-lock.js} +44 -44
- package/core/.antigravity-plugin/hooks.json +0 -39
- package/core/harnesses/shared/hooks/adapters/arg-fields.js +0 -19
- package/core/harnesses/shared/hooks/adapters/gemini.js +0 -114
- package/core/harnesses/shared/hooks/hooks.json +0 -47
- package/src/runtime/context-pack.ts +0 -492
|
@@ -58,9 +58,9 @@
|
|
|
58
58
|
|
|
59
59
|
const nodeFs = require('node:fs');
|
|
60
60
|
const path = require('node:path');
|
|
61
|
-
const { finishRuntime } = require('../cli-result.
|
|
62
|
-
const { resolveActiveFeature } = require('../feature-resolve.
|
|
63
|
-
const { parseRequirement, parseDesign, parsePlan } = require('./artifacts.
|
|
61
|
+
const { finishRuntime } = require('../cli-result.js');
|
|
62
|
+
const { resolveActiveFeature } = require('../feature-resolve.js');
|
|
63
|
+
const { parseRequirement, parseDesign, parsePlan } = require('./artifacts.js');
|
|
64
64
|
const {
|
|
65
65
|
EXTENSION_LANGUAGE,
|
|
66
66
|
PROSE_EXTENSIONS,
|
|
@@ -71,14 +71,14 @@ const {
|
|
|
71
71
|
snake,
|
|
72
72
|
EMITTERS,
|
|
73
73
|
repoLanguage,
|
|
74
|
-
} = require('./languages.
|
|
74
|
+
} = require('./languages.js');
|
|
75
75
|
const {
|
|
76
76
|
FINGERPRINT_MARKER,
|
|
77
77
|
FINGERPRINT_RE,
|
|
78
78
|
sha256,
|
|
79
79
|
compose,
|
|
80
80
|
inspectExisting,
|
|
81
|
-
} = require('./fingerprint.
|
|
81
|
+
} = require('./fingerprint.js');
|
|
82
82
|
|
|
83
83
|
// `handleScaffoldCommand` reads through the plain `fs` binding it had when it lived in bin/doflow.js;
|
|
84
84
|
// aliased to the `node:fs` module this file already imports as `nodeFs`.
|
|
@@ -102,7 +102,7 @@ const MANIFEST_NAME = 'MANIFEST.md';
|
|
|
102
102
|
* One suffix on everything rather than a per-toolchain exclusion list, because the invariant worth
|
|
103
103
|
* having is stronger than "no runner we currently know about picks this up": nothing in the
|
|
104
104
|
* scaffold is loadable by any toolchain until a human deliberately drops the suffix. The mirrored
|
|
105
|
-
* layout stays legible either way — `src/runtime/scaffold/generate.
|
|
105
|
+
* layout stays legible either way — `src/runtime/scaffold/generate.js.stub` reads as exactly what it is.
|
|
106
106
|
*
|
|
107
107
|
* Part 2's `contracts/` frames deliberately do not carry it: `default/` exists to be compiled
|
|
108
108
|
* against, and its tree contains no directory a test runner sweeps.
|
|
@@ -111,16 +111,16 @@ const STUB_SUFFIX = '.stub';
|
|
|
111
111
|
|
|
112
112
|
/** The three chain artifacts, in the order a reader meets them. All three are required: a scaffold
|
|
113
113
|
* missing one of them is a scaffold with no traceability, no shape, or no file list. */
|
|
114
|
-
const ARTIFACTS
|
|
114
|
+
const ARTIFACTS = Object.freeze(['requirement.md', 'design.md', 'plan.md']);
|
|
115
115
|
|
|
116
116
|
/**
|
|
117
117
|
* Uniform exit codes, matching design §4.2's contract for every DoFlow verb.
|
|
118
118
|
* `1` is "a finding the caller must act on" — which includes an unparseable artifact, so a caller
|
|
119
119
|
* that only checks for a crash still sees the difference between a full and a partial scaffold.
|
|
120
120
|
*/
|
|
121
|
-
const EXIT
|
|
121
|
+
const EXIT = Object.freeze({ OK: 0, FINDING: 1, USAGE: 2 });
|
|
122
122
|
|
|
123
|
-
const STATUS
|
|
123
|
+
const STATUS = Object.freeze({
|
|
124
124
|
COMPLETE: 'COMPLETE',
|
|
125
125
|
INCOMPLETE: 'INCOMPLETE',
|
|
126
126
|
BLOCKED: 'BLOCKED',
|
|
@@ -136,16 +136,16 @@ const STATUS: Readonly<Record<string, string>> = Object.freeze({
|
|
|
136
136
|
// `scaffold-languages.js` and `scaffold-artifacts.js` respectively — required above. What is left
|
|
137
137
|
// here is generic and used only by the generator below.
|
|
138
138
|
|
|
139
|
-
function readIfPresent(file
|
|
139
|
+
function readIfPresent(file , fsImpl ) {
|
|
140
140
|
try {
|
|
141
141
|
return fsImpl.readFileSync(file, 'utf8');
|
|
142
|
-
} catch (error
|
|
142
|
+
} catch (error ) {
|
|
143
143
|
if (error && ['ENOENT', 'ENOTDIR', 'EISDIR'].includes(error.code)) return null;
|
|
144
144
|
throw error;
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
function isDirectory(file
|
|
148
|
+
function isDirectory(file , fsImpl ) {
|
|
149
149
|
try {
|
|
150
150
|
return fsImpl.statSync(file).isDirectory();
|
|
151
151
|
} catch {
|
|
@@ -154,11 +154,11 @@ function isDirectory(file: string, fsImpl: any): boolean {
|
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
/** Table cells are pipe-delimited; an unescaped pipe in a task description would shear the row. */
|
|
157
|
-
const cell = (text
|
|
157
|
+
const cell = (text ) => String(text ?? '').replace(/\|/g, '\\|').replace(/\r?\n/g, ' ').trim();
|
|
158
158
|
|
|
159
159
|
/** Sorts by a string key without depending on locale collation, which varies by platform and would
|
|
160
160
|
* make "re-running produces no diff" false across machines. */
|
|
161
|
-
const byKey = (key
|
|
161
|
+
const byKey = (key ) => (a , b ) => (key(a) < key(b) ? -1 : key(a) > key(b) ? 1 : 0);
|
|
162
162
|
|
|
163
163
|
// ---------------------------------------------------------------------------------------------
|
|
164
164
|
// Generator
|
|
@@ -178,10 +178,10 @@ const byKey = (key: (a: any) => any) => (a: any, b: any) => (key(a) < key(b) ? -
|
|
|
178
178
|
* @param {Object} fsImpl
|
|
179
179
|
* @returns {{ sources: Array<Object>, notEvaluated: Array<Object>, texts: Object }}
|
|
180
180
|
*/
|
|
181
|
-
function readArtifacts(featureDir
|
|
182
|
-
const sources
|
|
183
|
-
const notEvaluated
|
|
184
|
-
const texts
|
|
181
|
+
function readArtifacts(featureDir , fsImpl ) {
|
|
182
|
+
const sources = [];
|
|
183
|
+
const notEvaluated = [];
|
|
184
|
+
const texts = {};
|
|
185
185
|
for (const name of ARTIFACTS) {
|
|
186
186
|
const text = readIfPresent(path.join(featureDir, name), fsImpl);
|
|
187
187
|
texts[name] = text;
|
|
@@ -199,11 +199,11 @@ function readArtifacts(featureDir: string, fsImpl: any): { sources: any[]; notEv
|
|
|
199
199
|
* @param {Object} texts keyed by artifact name, from `readArtifacts`
|
|
200
200
|
* @returns {{ requirement: Object, design: Object, plan: Object, notEvaluated: Array<Object> }}
|
|
201
201
|
*/
|
|
202
|
-
function parseArtifacts(texts
|
|
202
|
+
function parseArtifacts(texts ) {
|
|
203
203
|
const requirement = texts['requirement.md'] ? parseRequirement(texts['requirement.md']) : { requirements: new Map(), criteria: [], gaps: [] };
|
|
204
204
|
const design = texts['design.md'] ? parseDesign(texts['design.md']) : { components: new Map(), gaps: [] };
|
|
205
205
|
const plan = texts['plan.md'] ? parsePlan(texts['plan.md']) : { tasks: [], gaps: [] };
|
|
206
|
-
const notEvaluated
|
|
206
|
+
const notEvaluated = [];
|
|
207
207
|
for (const gap of [...requirement.gaps, ...design.gaps, ...plan.gaps]) {
|
|
208
208
|
notEvaluated.push({ what: `${gap.artifact} → ${gap.what}`, why: gap.why });
|
|
209
209
|
}
|
|
@@ -217,27 +217,27 @@ function parseArtifacts(texts: Record<string, string | null>): { requirement: an
|
|
|
217
217
|
* @param {Object} design from `parseArtifacts`
|
|
218
218
|
* @returns {{ requirementsForStory: Map, componentsForRequirement: Map, traceTask: Function }}
|
|
219
219
|
*/
|
|
220
|
-
function buildTraceabilityIndex(requirement
|
|
221
|
-
const requirementsForStory = new Map
|
|
220
|
+
function buildTraceabilityIndex(requirement , design ) {
|
|
221
|
+
const requirementsForStory = new Map ();
|
|
222
222
|
for (const req of requirement.requirements.values()) {
|
|
223
223
|
for (const story of req.stories) {
|
|
224
224
|
if (!requirementsForStory.has(story)) requirementsForStory.set(story, []);
|
|
225
|
-
requirementsForStory.get(story)
|
|
225
|
+
requirementsForStory.get(story) .push(req);
|
|
226
226
|
}
|
|
227
227
|
}
|
|
228
|
-
const componentsForRequirement = new Map
|
|
228
|
+
const componentsForRequirement = new Map ();
|
|
229
229
|
for (const component of design.components.values()) {
|
|
230
230
|
for (const id of component.serves) {
|
|
231
231
|
if (!componentsForRequirement.has(id)) componentsForRequirement.set(id, []);
|
|
232
|
-
componentsForRequirement.get(id)
|
|
232
|
+
componentsForRequirement.get(id) .push(component);
|
|
233
233
|
}
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
-
function traceTask(task
|
|
237
|
-
const reqs = [...new Set(task.stories.flatMap((s
|
|
238
|
-
.sort(byKey((r
|
|
239
|
-
const comps = [...new Set(reqs.flatMap((r
|
|
240
|
-
.sort(byKey((c
|
|
236
|
+
function traceTask(task ) {
|
|
237
|
+
const reqs = [...new Set(task.stories.flatMap((s ) => requirementsForStory.get(s) || []))]
|
|
238
|
+
.sort(byKey((r ) => r.id));
|
|
239
|
+
const comps = [...new Set(reqs.flatMap((r ) => componentsForRequirement.get(r.id) || []))]
|
|
240
|
+
.sort(byKey((c ) => c.id));
|
|
241
241
|
return { requirements: reqs, components: comps };
|
|
242
242
|
}
|
|
243
243
|
|
|
@@ -249,9 +249,9 @@ function buildTraceabilityIndex(requirement: any, design: any): { requirementsFo
|
|
|
249
249
|
* @param {Array<Object>} tasks from `plan.tasks`
|
|
250
250
|
* @returns {{ byPath: Map<string, Array<Object>>, skipped: Array<Object> }}
|
|
251
251
|
*/
|
|
252
|
-
function groupTasksByFile(tasks
|
|
253
|
-
const byPath = new Map
|
|
254
|
-
const skipped
|
|
252
|
+
function groupTasksByFile(tasks ) {
|
|
253
|
+
const byPath = new Map ();
|
|
254
|
+
const skipped = [];
|
|
255
255
|
for (const task of tasks) {
|
|
256
256
|
if (task.files.length === 0) {
|
|
257
257
|
skipped.push({ item: `task ${task.id}`, why: 'no `files:` field — nothing to mirror', benign: false });
|
|
@@ -259,7 +259,7 @@ function groupTasksByFile(tasks: any[]): { byPath: Map<string, any[]>; skipped:
|
|
|
259
259
|
}
|
|
260
260
|
for (const file of task.files) {
|
|
261
261
|
if (!byPath.has(file)) byPath.set(file, []);
|
|
262
|
-
byPath.get(file)
|
|
262
|
+
byPath.get(file) .push(task);
|
|
263
263
|
}
|
|
264
264
|
}
|
|
265
265
|
return { byPath, skipped };
|
|
@@ -277,14 +277,14 @@ function groupTasksByFile(tasks: any[]): { byPath: Map<string, any[]>; skipped:
|
|
|
277
277
|
* @param {Function} options.traceTask from `buildTraceabilityIndex`
|
|
278
278
|
* @returns {{ planned: Map<string, Object>, skipped: Array<Object>, shapeUndeclared: Array<Object> }}
|
|
279
279
|
*/
|
|
280
|
-
function planSourceStubs({ byPath, scaffoldDir, repoRoot, fsImpl, slug, message, traceTask }
|
|
281
|
-
const planned = new Map
|
|
282
|
-
const skipped
|
|
283
|
-
const shapeUndeclared
|
|
280
|
+
function planSourceStubs({ byPath, scaffoldDir, repoRoot, fsImpl, slug, message, traceTask } ) {
|
|
281
|
+
const planned = new Map (); // relative-to-scaffoldDir path → { body, header, kind, from }
|
|
282
|
+
const skipped = [];
|
|
283
|
+
const shapeUndeclared = [];
|
|
284
284
|
|
|
285
|
-
for (const [file, owners] of [...byPath.entries()].sort(byKey((e
|
|
285
|
+
for (const [file, owners] of [...byPath.entries()].sort(byKey((e ) => e[0]))) {
|
|
286
286
|
const ext = path.extname(file).toLowerCase();
|
|
287
|
-
const label = `${file} (${owners.map((t
|
|
287
|
+
const label = `${file} (${owners.map((t ) => t.id).join(', ')})`;
|
|
288
288
|
|
|
289
289
|
if (file.endsWith('/') || isDirectory(path.join(repoRoot, file), fsImpl)) {
|
|
290
290
|
skipped.push({ item: label, why: 'names a directory, not a file — no single signature to emit', benign: true });
|
|
@@ -321,34 +321,34 @@ function planSourceStubs({ byPath, scaffoldDir, repoRoot, fsImpl, slug, message,
|
|
|
321
321
|
}
|
|
322
322
|
|
|
323
323
|
const emitter = EMITTERS[languageId];
|
|
324
|
-
const trace = owners.map((t
|
|
325
|
-
const reqs = [...new Set(trace.flatMap((t
|
|
326
|
-
const comps = [...new Set(trace.flatMap((t
|
|
327
|
-
const declared = comps.flatMap((c
|
|
324
|
+
const trace = owners.map((t ) => ({ task: t, ...traceTask(t) }));
|
|
325
|
+
const reqs = [...new Set(trace.flatMap((t ) => t.requirements))].sort(byKey((r ) => r.id));
|
|
326
|
+
const comps = [...new Set(trace.flatMap((t ) => t.components))].sort(byKey((c ) => c.id));
|
|
327
|
+
const declared = comps.flatMap((c ) => c.declared.map((d ) => ({ ...d, component: c.id })));
|
|
328
328
|
|
|
329
329
|
const header = [
|
|
330
330
|
`doflow scaffold — generated from ${slug}'s requirement.md, design.md and plan.md.`,
|
|
331
331
|
'Signatures and stubs only. Do not implement here; this tree is never promoted automatically.',
|
|
332
332
|
'',
|
|
333
333
|
`intended path: ${file}`,
|
|
334
|
-
...owners.map((t
|
|
334
|
+
...owners.map((t ) => `task: ${t.id} — ${t.description}`),
|
|
335
335
|
...(reqs.length
|
|
336
|
-
? reqs.map((r
|
|
336
|
+
? reqs.map((r ) => `requirement: ${r.id} — ${r.summary}`)
|
|
337
337
|
: ['requirement: none — no user story on the owning task resolves to a requirement']),
|
|
338
338
|
...(comps.length
|
|
339
|
-
? comps.map((c
|
|
339
|
+
? comps.map((c ) => `component: ${c.id} — ${c.name} (design.md §3)`)
|
|
340
340
|
: ['component: none — no design component serves this task\'s requirements']),
|
|
341
341
|
];
|
|
342
342
|
|
|
343
|
-
const bodyParts
|
|
343
|
+
const bodyParts = [''];
|
|
344
344
|
if (declared.length) {
|
|
345
|
-
header.push(`shape: declared in design.md §4 (${[...new Set(declared.map((d
|
|
345
|
+
header.push(`shape: declared in design.md §4 (${[...new Set(declared.map((d ) => d.component))].join(', ')}), quoted below`);
|
|
346
346
|
for (const decl of declared) {
|
|
347
347
|
const lines = decl.block.split('\n');
|
|
348
348
|
const shown = lines.slice(0, 40);
|
|
349
349
|
bodyParts.push([
|
|
350
350
|
emitter.line(`declared interface — design.md "${decl.heading}", verbatim:`),
|
|
351
|
-
...shown.map((l
|
|
351
|
+
...shown.map((l ) => emitter.line(` ${l}`.replace(/\s+$/, ''))),
|
|
352
352
|
...(lines.length > shown.length ? [emitter.line(` … ${lines.length - shown.length} more lines in design.md`)] : []),
|
|
353
353
|
'',
|
|
354
354
|
].join('\n'));
|
|
@@ -371,7 +371,7 @@ function planSourceStubs({ byPath, scaffoldDir, repoRoot, fsImpl, slug, message,
|
|
|
371
371
|
bodyParts.push(emitter.module({ symbol, message, pkg: pkg || 'scaffold' }));
|
|
372
372
|
planned.set(path.join('src', file) + STUB_SUFFIX, {
|
|
373
373
|
content: compose(emitter, header, `${bodyParts.join('\n')}\n`),
|
|
374
|
-
from: owners.map((t
|
|
374
|
+
from: owners.map((t ) => t.id).join(', '),
|
|
375
375
|
shape: declared.length ? 'declared interface + placeholder signature' : 'placeholder signature',
|
|
376
376
|
});
|
|
377
377
|
}
|
|
@@ -389,10 +389,10 @@ function planSourceStubs({ byPath, scaffoldDir, repoRoot, fsImpl, slug, message,
|
|
|
389
389
|
* @param {string} options.message the pinned "not implemented" signal
|
|
390
390
|
* @returns {{ planned: Map<string, Object>, skipped: Array<Object>, notEvaluated: Array<Object> }}
|
|
391
391
|
*/
|
|
392
|
-
function planTestStubs({ requirement, componentsForRequirement, language, slug, message }
|
|
393
|
-
const planned = new Map
|
|
394
|
-
const skipped
|
|
395
|
-
const notEvaluated
|
|
392
|
+
function planTestStubs({ requirement, componentsForRequirement, language, slug, message } ) {
|
|
393
|
+
const planned = new Map ();
|
|
394
|
+
const skipped = [];
|
|
395
|
+
const notEvaluated = [];
|
|
396
396
|
|
|
397
397
|
if (requirement.criteria.length > 0) {
|
|
398
398
|
if (!language.id) {
|
|
@@ -402,27 +402,27 @@ function planTestStubs({ requirement, componentsForRequirement, language, slug,
|
|
|
402
402
|
});
|
|
403
403
|
} else {
|
|
404
404
|
const emitter = EMITTERS[language.id];
|
|
405
|
-
const grouped = new Map
|
|
405
|
+
const grouped = new Map ();
|
|
406
406
|
for (const criterion of requirement.criteria) {
|
|
407
407
|
const ids = criterion.requirements.length ? criterion.requirements : ['unattributed'];
|
|
408
408
|
for (const id of ids) {
|
|
409
409
|
if (!grouped.has(id)) grouped.set(id, []);
|
|
410
|
-
grouped.get(id)
|
|
410
|
+
grouped.get(id) .push(criterion);
|
|
411
411
|
}
|
|
412
412
|
if (criterion.requirements.length === 0) {
|
|
413
413
|
skipped.push({ item: `acceptance criterion "${criterion.statement}"`, why: 'names no FR/NFR — stubbed under "unattributed", traceability incomplete', benign: false });
|
|
414
414
|
}
|
|
415
415
|
}
|
|
416
|
-
for (const [id, cases] of [...grouped.entries()].sort(byKey((e
|
|
416
|
+
for (const [id, cases] of [...grouped.entries()].sort(byKey((e ) => e[0]))) {
|
|
417
417
|
const req = requirement.requirements.get(id);
|
|
418
|
-
const comps = (componentsForRequirement.get(id) || []).sort(byKey((c
|
|
418
|
+
const comps = (componentsForRequirement.get(id) || []).sort(byKey((c ) => c.id));
|
|
419
419
|
const header = [
|
|
420
420
|
`doflow scaffold — test stubs generated from ${slug}'s requirement.md acceptance criteria.`,
|
|
421
421
|
'Each stub fails until implemented, on purpose: a stub that passes asserts nothing and',
|
|
422
422
|
'reads as coverage. Replace the body, do not delete the failure.',
|
|
423
423
|
'',
|
|
424
424
|
`requirement: ${id}${req ? ` — ${req.summary}` : ' — not present in the requirement index'}`,
|
|
425
|
-
...(comps.length ? comps.map((c
|
|
425
|
+
...(comps.length ? comps.map((c ) => `component: ${c.id} — ${c.name} (design.md §3)`) : []),
|
|
426
426
|
`criteria: ${cases.length} from requirement.md §6`,
|
|
427
427
|
`language: ${language.id} (detected from ${language.signal} by command-detect.ts)`,
|
|
428
428
|
];
|
|
@@ -446,21 +446,21 @@ function planTestStubs({ requirement, componentsForRequirement, language, slug,
|
|
|
446
446
|
* @param {Set<string>} ownedPaths every path a task in this plan already owns
|
|
447
447
|
* @returns {Array<Object>} externalDependencies
|
|
448
448
|
*/
|
|
449
|
-
function partitionExternalDependencies(tasks
|
|
450
|
-
const externalDependencies
|
|
451
|
-
const dependencyTasks = new Map
|
|
449
|
+
function partitionExternalDependencies(tasks , ownedPaths ) {
|
|
450
|
+
const externalDependencies = [];
|
|
451
|
+
const dependencyTasks = new Map ();
|
|
452
452
|
for (const task of tasks) {
|
|
453
453
|
for (const dep of task.dependsOn) {
|
|
454
454
|
if (!dependencyTasks.has(dep)) dependencyTasks.set(dep, []);
|
|
455
|
-
dependencyTasks.get(dep)
|
|
455
|
+
dependencyTasks.get(dep) .push(task);
|
|
456
456
|
}
|
|
457
457
|
}
|
|
458
|
-
for (const [dep, owners] of [...dependencyTasks.entries()].sort(byKey((e
|
|
458
|
+
for (const [dep, owners] of [...dependencyTasks.entries()].sort(byKey((e ) => e[0]))) {
|
|
459
459
|
const inScope = [...ownedPaths].some((p) => p === dep || p.startsWith(`${dep.replace(/\/$/, '')}/`));
|
|
460
|
-
const docs = [...new Set(owners.map((t
|
|
460
|
+
const docs = [...new Set(owners.map((t ) => t.externalContract).filter(Boolean))];
|
|
461
461
|
externalDependencies.push({
|
|
462
462
|
dependency: dep,
|
|
463
|
-
tasks: owners.map((t
|
|
463
|
+
tasks: owners.map((t ) => t.id).sort(),
|
|
464
464
|
disposition: inScope
|
|
465
465
|
? 'in scope — a task in this plan owns it, no contract frame needed'
|
|
466
466
|
: docs.length === 1
|
|
@@ -482,12 +482,12 @@ function partitionExternalDependencies(tasks: any[], ownedPaths: Set<string>): a
|
|
|
482
482
|
* @param {Object} options.fsImpl
|
|
483
483
|
* @returns {{ written: Array<string>, unchanged: Array<string>, preserved: Array<Object> }}
|
|
484
484
|
*/
|
|
485
|
-
function applyPlannedFiles({ planned, blocked, scaffoldDir, fsImpl }
|
|
486
|
-
const written
|
|
487
|
-
const unchanged
|
|
488
|
-
const preserved
|
|
485
|
+
function applyPlannedFiles({ planned, blocked, scaffoldDir, fsImpl } ) {
|
|
486
|
+
const written = [];
|
|
487
|
+
const unchanged = [];
|
|
488
|
+
const preserved = [];
|
|
489
489
|
|
|
490
|
-
function place(relative
|
|
490
|
+
function place(relative , content ) {
|
|
491
491
|
const target = path.join(scaffoldDir, relative);
|
|
492
492
|
const existing = readIfPresent(target, fsImpl);
|
|
493
493
|
if (existing !== null) {
|
|
@@ -507,7 +507,7 @@ function applyPlannedFiles({ planned, blocked, scaffoldDir, fsImpl }: any): { wr
|
|
|
507
507
|
}
|
|
508
508
|
|
|
509
509
|
if (blocked.length === 0) {
|
|
510
|
-
for (const [relative, entry] of [...planned.entries()].sort(byKey((e
|
|
510
|
+
for (const [relative, entry] of [...planned.entries()].sort(byKey((e ) => e[0]))) {
|
|
511
511
|
place(relative, entry.content);
|
|
512
512
|
}
|
|
513
513
|
}
|
|
@@ -524,14 +524,14 @@ function applyPlannedFiles({ planned, blocked, scaffoldDir, fsImpl }: any): { wr
|
|
|
524
524
|
* @param {Object} options.fsImpl
|
|
525
525
|
* @returns {Array<string>} orphans
|
|
526
526
|
*/
|
|
527
|
-
function findOrphans({ scaffoldDir, planned, fsImpl }
|
|
528
|
-
const orphans
|
|
529
|
-
(function scan(dir
|
|
530
|
-
let entries
|
|
527
|
+
function findOrphans({ scaffoldDir, planned, fsImpl } ) {
|
|
528
|
+
const orphans = [];
|
|
529
|
+
(function scan(dir , prefix ) {
|
|
530
|
+
let entries ;
|
|
531
531
|
try {
|
|
532
532
|
entries = fsImpl.readdirSync(dir, { withFileTypes: true });
|
|
533
533
|
} catch { return; }
|
|
534
|
-
for (const entry of entries.sort(byKey((e
|
|
534
|
+
for (const entry of entries.sort(byKey((e ) => e.name))) {
|
|
535
535
|
const relative = prefix ? path.join(prefix, entry.name) : entry.name;
|
|
536
536
|
if (entry.isDirectory()) { scan(path.join(dir, entry.name), relative); continue; }
|
|
537
537
|
if (relative === MANIFEST_NAME || planned.has(relative)) continue;
|
|
@@ -554,8 +554,8 @@ function findOrphans({ scaffoldDir, planned, fsImpl }: any): string[] {
|
|
|
554
554
|
* @param {number} options.plannedSize
|
|
555
555
|
* @returns {{ status: string }}
|
|
556
556
|
*/
|
|
557
|
-
function classifyRunStatus({ blocked, notEvaluated, skipped, plannedSize }
|
|
558
|
-
const realGaps = [...notEvaluated, ...skipped.filter((s
|
|
557
|
+
function classifyRunStatus({ blocked, notEvaluated, skipped, plannedSize } ) {
|
|
558
|
+
const realGaps = [...notEvaluated, ...skipped.filter((s ) => !s.benign)];
|
|
559
559
|
const status = blocked.length
|
|
560
560
|
? STATUS.BLOCKED
|
|
561
561
|
: realGaps.length || plannedSize === 0
|
|
@@ -579,14 +579,14 @@ function classifyRunStatus({ blocked, notEvaluated, skipped, plannedSize }: any)
|
|
|
579
579
|
* @param {Object} options
|
|
580
580
|
* @returns {{ stateSummary: string, summary: string }}
|
|
581
581
|
*/
|
|
582
|
-
function buildSummaries({ status, blocked, planned, shapeUndeclared, preserved, skipped, notEvaluated, externalDependencies, written, unchanged }
|
|
583
|
-
const blockedWhy = blocked.map((b
|
|
582
|
+
function buildSummaries({ status, blocked, planned, shapeUndeclared, preserved, skipped, notEvaluated, externalDependencies, written, unchanged } ) {
|
|
583
|
+
const blockedWhy = blocked.map((b ) => b.what).join(', ');
|
|
584
584
|
|
|
585
585
|
const stateSummary = status === STATUS.BLOCKED
|
|
586
586
|
? `BLOCKED — ${blockedWhy} could not be read or parsed; no scaffold written`
|
|
587
587
|
: `${status} — ${planned.size} file(s), ${shapeUndeclared.length} of them with no shape declared in design.md, `
|
|
588
588
|
+ `${preserved.length} preserved (hand-edited), ${skipped.length} skipped `
|
|
589
|
-
+ `(${skipped.filter((s
|
|
589
|
+
+ `(${skipped.filter((s ) => !s.benign).length} of them a real gap), ${notEvaluated.length} not evaluated, `
|
|
590
590
|
+ `${externalDependencies.length} dependency reference(s) routed to the contract-frame case`;
|
|
591
591
|
|
|
592
592
|
const summary = status === STATUS.BLOCKED
|
|
@@ -601,7 +601,7 @@ function buildSummaries({ status, blocked, planned, shapeUndeclared, preserved,
|
|
|
601
601
|
* @param {Object} options
|
|
602
602
|
* @returns {Object} result
|
|
603
603
|
*/
|
|
604
|
-
function buildResult({ status, slug, featureDir, scaffoldDir, repoRoot, language, sources, planned, written, unchanged, preserved, skipped, shapeUndeclared, notEvaluated, externalDependencies, orphans, stateSummary, summary }
|
|
604
|
+
function buildResult({ status, slug, featureDir, scaffoldDir, repoRoot, language, sources, planned, written, unchanged, preserved, skipped, shapeUndeclared, notEvaluated, externalDependencies, orphans, stateSummary, summary } ) {
|
|
605
605
|
return {
|
|
606
606
|
status,
|
|
607
607
|
exitCode: status === STATUS.COMPLETE ? EXIT.OK : EXIT.FINDING,
|
|
@@ -616,10 +616,10 @@ function buildResult({ status, slug, featureDir, scaffoldDir, repoRoot, language
|
|
|
616
616
|
generated: [...planned.keys()].sort(),
|
|
617
617
|
written: written.sort(),
|
|
618
618
|
unchanged: unchanged.sort(),
|
|
619
|
-
preserved: preserved.sort(byKey((p
|
|
620
|
-
skipped: skipped.sort(byKey((s
|
|
621
|
-
shapeUndeclared: shapeUndeclared.sort(byKey((s
|
|
622
|
-
notEvaluated: notEvaluated.sort(byKey((n
|
|
619
|
+
preserved: preserved.sort(byKey((p ) => p.path)),
|
|
620
|
+
skipped: skipped.sort(byKey((s ) => s.item)),
|
|
621
|
+
shapeUndeclared: shapeUndeclared.sort(byKey((s ) => s.path)),
|
|
622
|
+
notEvaluated: notEvaluated.sort(byKey((n ) => n.what)),
|
|
623
623
|
externalDependencies,
|
|
624
624
|
orphans: orphans.sort(),
|
|
625
625
|
stateSummary,
|
|
@@ -639,7 +639,7 @@ function buildResult({ status, slug, featureDir, scaffoldDir, repoRoot, language
|
|
|
639
639
|
* @param {Object} options.fsImpl
|
|
640
640
|
* @returns {Object} result
|
|
641
641
|
*/
|
|
642
|
-
function writeManifest({ result, planned, scaffoldDir, fsImpl }
|
|
642
|
+
function writeManifest({ result, planned, scaffoldDir, fsImpl } ) {
|
|
643
643
|
const manifest = renderManifest(result, planned);
|
|
644
644
|
const manifestTarget = path.join(scaffoldDir, MANIFEST_NAME);
|
|
645
645
|
const existingManifest = readIfPresent(manifestTarget, fsImpl);
|
|
@@ -672,12 +672,12 @@ function writeManifest({ result, planned, scaffoldDir, fsImpl }: any): any {
|
|
|
672
672
|
* @param {Object} [options.fsImpl] injection seam for tests; same contract as `node:fs`
|
|
673
673
|
* @returns {Object} result — see `summary`, `status` and `exitCode`
|
|
674
674
|
*/
|
|
675
|
-
function generateScaffold(options
|
|
675
|
+
function generateScaffold(options = {}) {
|
|
676
676
|
const fsImpl = options.fsImpl || nodeFs;
|
|
677
677
|
const repoRoot = path.resolve(options.repoRoot || process.cwd());
|
|
678
678
|
const featureDir = options.featureDir ? path.resolve(options.featureDir) : null;
|
|
679
679
|
|
|
680
|
-
const usage = (message
|
|
680
|
+
const usage = (message ) => ({
|
|
681
681
|
status: STATUS.USAGE, exitCode: EXIT.USAGE, ok: false, summary: message,
|
|
682
682
|
featureDir, scaffoldDir: null, manifestPath: null,
|
|
683
683
|
generated: [], written: [], unchanged: [], preserved: [], skipped: [], notEvaluated: [],
|
|
@@ -696,7 +696,7 @@ function generateScaffold(options: any = {}): any {
|
|
|
696
696
|
const notEvaluated = [...artifactsNotEvaluated, ...parseGapsNotEvaluated];
|
|
697
697
|
|
|
698
698
|
const blocked = notEvaluated.filter(
|
|
699
|
-
(n
|
|
699
|
+
(n ) => ARTIFACTS.includes(n.what) || n.what.startsWith('plan.md → task list'),
|
|
700
700
|
);
|
|
701
701
|
|
|
702
702
|
const { requirementsForStory, componentsForRequirement, traceTask } = buildTraceabilityIndex(requirement, design);
|
|
@@ -743,9 +743,9 @@ function generateScaffold(options: any = {}): any {
|
|
|
743
743
|
* the second run of an unchanged plan produce a diff — losing the idempotency the whole artifact is
|
|
744
744
|
* reviewed on.
|
|
745
745
|
*/
|
|
746
|
-
function renderManifest(result
|
|
747
|
-
const lines
|
|
748
|
-
const table = (headers
|
|
746
|
+
function renderManifest(result , planned ) {
|
|
747
|
+
const lines = [];
|
|
748
|
+
const table = (headers , rows , empty ) => {
|
|
749
749
|
if (rows.length === 0) { lines.push(empty, ''); return; }
|
|
750
750
|
lines.push(`| ${headers.join(' | ')} |`, `|${headers.map(() => '---').join('|')}|`);
|
|
751
751
|
for (const r of rows) lines.push(`| ${r.map(cell).join(' | ')} |`);
|
|
@@ -754,7 +754,7 @@ function renderManifest(result: any, planned: Map<string, any>): string {
|
|
|
754
754
|
|
|
755
755
|
lines.push(`# Scaffold manifest — ${result.slug}`, '');
|
|
756
756
|
lines.push(
|
|
757
|
-
'Generated by DoFlow\'s scaffold generator (`src/runtime/scaffold/generate.
|
|
757
|
+
'Generated by DoFlow\'s scaffold generator (`src/runtime/scaffold/generate.js`) from this feature\'s own',
|
|
758
758
|
'`requirement.md`, `design.md` and `plan.md`. Signatures, types and failing stubs only — no',
|
|
759
759
|
'implementation logic, by design. Nothing here is written into the source tree; promoting any of',
|
|
760
760
|
'it is a deliberate act.',
|
|
@@ -768,23 +768,23 @@ function renderManifest(result: any, planned: Map<string, any>): string {
|
|
|
768
768
|
|
|
769
769
|
lines.push('## Source artifacts', '');
|
|
770
770
|
table(['Artifact', 'Read', 'SHA-256'],
|
|
771
|
-
result.sources.map((s
|
|
771
|
+
result.sources.map((s ) => [s.name, s.present ? 'yes' : 'NOT FOUND', s.sha256 || '—']),
|
|
772
772
|
'_None read._');
|
|
773
773
|
|
|
774
774
|
lines.push('## Generated', '');
|
|
775
775
|
table(['Path', 'Derived from', 'Shape'],
|
|
776
|
-
result.generated.map((p
|
|
776
|
+
result.generated.map((p ) => [p, planned.get(p).from, planned.get(p).shape]),
|
|
777
777
|
'_Nothing generated._');
|
|
778
778
|
|
|
779
779
|
lines.push('## Emitted without a declared shape', '');
|
|
780
780
|
lines.push('These files exist and are traceable, but `design.md` §4 never declared an interface for',
|
|
781
781
|
'them, so their signature is a placeholder rather than a contract. Reviewing the placeholder as',
|
|
782
782
|
'if it were the designed API is the mistake this section exists to prevent.', '');
|
|
783
|
-
table(['Path', 'Why'], result.shapeUndeclared.map((s
|
|
783
|
+
table(['Path', 'Why'], result.shapeUndeclared.map((s ) => [s.path, s.why]),
|
|
784
784
|
'_Every emitted file has a declared shape behind it._');
|
|
785
785
|
|
|
786
786
|
lines.push('## Preserved — edited by hand, not overwritten', '');
|
|
787
|
-
table(['Path', 'Why'], result.preserved.map((p
|
|
787
|
+
table(['Path', 'Why'], result.preserved.map((p ) => [p.path, p.why]),
|
|
788
788
|
'_No hand-edited files detected._');
|
|
789
789
|
|
|
790
790
|
lines.push('## Skipped', '');
|
|
@@ -794,14 +794,14 @@ function renderManifest(result: any, planned: Map<string, any>): string {
|
|
|
794
794
|
'"Gap" separates the two kinds. A markdown file has no signature to emit and skipping it costs',
|
|
795
795
|
'nothing; a source file in a language this generator cannot emit is work that silently did not',
|
|
796
796
|
'happen, and it is what holds the status below `COMPLETE`.', '');
|
|
797
|
-
table(['Item', 'Gap', 'Why'], result.skipped.map((s
|
|
797
|
+
table(['Item', 'Gap', 'Why'], result.skipped.map((s ) => [s.item, s.benign ? 'no' : 'YES', s.why]),
|
|
798
798
|
'_Nothing skipped._');
|
|
799
799
|
|
|
800
800
|
lines.push('## Not evaluated', '');
|
|
801
801
|
lines.push('Things this run could not judge either way. These, together with every skip marked as a',
|
|
802
802
|
'gap above, are why the status is not `COMPLETE`. None of them is a claim that the underlying',
|
|
803
803
|
'item is fine.', '');
|
|
804
|
-
table(['What', 'Why'], result.notEvaluated.map((n
|
|
804
|
+
table(['What', 'Why'], result.notEvaluated.map((n ) => [n.what, n.why]),
|
|
805
805
|
'_Everything the artifacts declared was evaluated._');
|
|
806
806
|
|
|
807
807
|
lines.push('## External dependencies', '');
|
|
@@ -810,13 +810,13 @@ function renderManifest(result: any, planned: Map<string, any>): string {
|
|
|
810
810
|
'nested `.git` boundaries, non-local vendors and `external-contract:` targets; this generator reports',
|
|
811
811
|
'the partition rather than deriving the boundaries a second way.', '');
|
|
812
812
|
table(['Dependency', 'Named by', 'Disposition'],
|
|
813
|
-
result.externalDependencies.map((d
|
|
813
|
+
result.externalDependencies.map((d ) => [d.dependency, d.tasks.join(', '), d.disposition]),
|
|
814
814
|
'_No task declares a `depends-on:` value._');
|
|
815
815
|
|
|
816
816
|
if (result.orphans.length) {
|
|
817
817
|
lines.push('## Orphaned', '');
|
|
818
818
|
lines.push('Generated by an earlier run and no longer implied by the plan. Reported, never deleted.', '');
|
|
819
|
-
table(['Path'], result.orphans.map((p
|
|
819
|
+
table(['Path'], result.orphans.map((p ) => [p]), '');
|
|
820
820
|
}
|
|
821
821
|
|
|
822
822
|
const body = `\n${lines.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd()}\n`;
|
|
@@ -846,7 +846,7 @@ function renderManifest(result: any, planned: Map<string, any>): string {
|
|
|
846
846
|
* @param {string|null} [options.slug] explicit feature override
|
|
847
847
|
* @returns {number} process exit code
|
|
848
848
|
*/
|
|
849
|
-
function handleScaffoldCommand({ json = false, projectRoot, slug = null }
|
|
849
|
+
function handleScaffoldCommand({ json = false, projectRoot, slug = null } = {}) {
|
|
850
850
|
const root = projectRoot || process.cwd();
|
|
851
851
|
const feature = resolveActiveFeature({ projectRoot: root, slug });
|
|
852
852
|
if (feature.error) {
|
|
@@ -870,10 +870,10 @@ function handleScaffoldCommand({ json = false, projectRoot, slug = null }: any =
|
|
|
870
870
|
|
|
871
871
|
// A reader who cannot see the gaps reads the tree as the whole shape of the plan, so the
|
|
872
872
|
// negative space is printed at the same level as the counts rather than left to the manifest.
|
|
873
|
-
const sections
|
|
874
|
-
['Skipped', (result.skipped || []).map((s
|
|
875
|
-
['Not evaluated', (result.notEvaluated || []).map((n
|
|
876
|
-
['Preserved (hand-edited, not overwritten)', (result.preserved || []).map((p
|
|
873
|
+
const sections = [
|
|
874
|
+
['Skipped', (result.skipped || []).map((s ) => `${s.item}${s.benign ? '' : ' [GAP]'} — ${s.why}`)],
|
|
875
|
+
['Not evaluated', (result.notEvaluated || []).map((n ) => `${n.what} — ${n.why}`)],
|
|
876
|
+
['Preserved (hand-edited, not overwritten)', (result.preserved || []).map((p ) => `${p.path} — ${p.why}`)],
|
|
877
877
|
['Orphaned (no longer implied by the plan)', result.orphans || []],
|
|
878
878
|
];
|
|
879
879
|
for (const [title, lines] of sections) {
|