dflow-sdd-ddd 0.13.0 → 0.14.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/CHANGELOG.md +65 -1
- package/CONTRIBUTING.md +6 -9
- package/README.en.md +36 -25
- package/README.md +10 -8
- package/TEMPLATE-COVERAGE.md +2 -2
- package/bin/dflow.js +22 -1
- package/docs/npm-publish-checklist.md +3 -1
- package/docs/release-versioning-policy.md +3 -2
- package/docs/using-with-claude-code.en.md +12 -6
- package/docs/using-with-claude-code.md +7 -3
- package/docs/using-with-codex.en.md +5 -2
- package/docs/using-with-codex.md +3 -1
- package/docs/using-with-github-copilot.en.md +12 -6
- package/docs/using-with-github-copilot.md +8 -4
- package/lib/doctor-checks.js +178 -0
- package/lib/init.js +802 -29
- package/package.json +2 -2
- package/templates/brownfield/references/init-project-flow.md +3 -1
- package/templates/brownfield/scaffolding/AI-AGENT-GUIDE.md +8 -1
- package/templates/greenfield/references/init-project-flow.md +5 -5
- package/templates/greenfield/scaffolding/AI-AGENT-GUIDE.md +8 -1
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// PROPOSAL-058: pure, shippable drift-detection helpers for `dflow doctor`.
|
|
2
|
+
//
|
|
3
|
+
// The dev-only cross-ref resolver (scripts/check-cross-refs.mjs, PROPOSAL-055)
|
|
4
|
+
// is not part of the npm package, so the runtime checks reimplement the narrow
|
|
5
|
+
// subset doctor needs: fence-aware heading extraction, "<file> § Heading"
|
|
6
|
+
// reference extraction with soft-wrap joining, tolerant heading matching, and
|
|
7
|
+
// template-shape comparison. Everything here is I/O-free — callers read the
|
|
8
|
+
// files and pass contents — which keeps the checks unit-testable.
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
// Machine-readable context lines. Shared with lib/init.js inference so the
|
|
13
|
+
// doctor "machine format" checks can never drift from what inference actually
|
|
14
|
+
// parses: inferGitPolicy / inferAiCommitMarker / inferProseLanguage read
|
|
15
|
+
// _conventions.md; inferTechStackSummary / inferMigrationContext read the
|
|
16
|
+
// `| Tech stack |` / `| Migration / legacy context |` rows of the guide's
|
|
17
|
+
// "## Project Context" table (PROPOSAL-076 — no packaged _overview.md template
|
|
18
|
+
// ever carried those rows).
|
|
19
|
+
const GIT_POLICY_LINE_RE = /Selected Git policy:\s*`([^`]+)`/;
|
|
20
|
+
const AI_COMMIT_MARKER_LINE_RE = /AI commit marker:\s*`([^`]+)`/;
|
|
21
|
+
const PROSE_LANGUAGE_LINE_RE = /Project prose language:\s*`([^`]+)`/;
|
|
22
|
+
// The row values accept Markdown-escaped pipes (`\|`) so a cell like
|
|
23
|
+
// `Node \| Express` is captured whole; parseContextLine unescapes them
|
|
24
|
+
// (PROPOSAL-076 gate G1 — a bare `[^|\n]` capture silently truncated at the
|
|
25
|
+
// escaped pipe while doctor still called the row machine-readable).
|
|
26
|
+
const TECH_STACK_ROW_RE = /\|\s*Tech stack\s*\|\s*((?:\\\||[^|\n])+?)\s*\|/i;
|
|
27
|
+
const MIGRATION_CONTEXT_ROW_RE = /\|\s*Migration \/ legacy context\s*\|\s*((?:\\\||[^|\n])+?)\s*\|/i;
|
|
28
|
+
|
|
29
|
+
const GIT_POLICY_VALUES = new Set(['gitflow', 'trunk']);
|
|
30
|
+
const AI_COMMIT_MARKER_VALUES = new Set(['none', 'co-authored-by', 'prefix']);
|
|
31
|
+
|
|
32
|
+
// Trimmed capture of a machine-readable context line, or null when the line is
|
|
33
|
+
// absent or its value is whitespace-only. Inference and the doctor checks must
|
|
34
|
+
// both parse through this helper: a value doctor accepts has to be the exact
|
|
35
|
+
// value configure-agents consumes (e.g. a stray space inside the backticks
|
|
36
|
+
// would otherwise pass doctor's trimmed validation yet miss strict comparisons
|
|
37
|
+
// like buildSubstitutionMap's `gitflow` check downstream).
|
|
38
|
+
function parseContextLine(content, re) {
|
|
39
|
+
const match = content.match(re);
|
|
40
|
+
if (!match) return null;
|
|
41
|
+
// Unescape Markdown-escaped pipes captured by the table-row patterns; the
|
|
42
|
+
// backtick context lines never contain `\|`, so this is a no-op for them.
|
|
43
|
+
const value = match[1].replace(/\\\|/g, '|').trim();
|
|
44
|
+
return value === '' ? null : value;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Blank out fenced code blocks (``` / ~~~) line-by-line so headings and § refs
|
|
48
|
+
// inside examples are never extracted. Line positions are preserved. CommonMark
|
|
49
|
+
// close rules (PROPOSAL-076 gates G4/G6): a block closes only on the same fence
|
|
50
|
+
// character repeated at least the opening length, indented at most three
|
|
51
|
+
// spaces, with nothing but whitespace after — so a three-backtick line inside a
|
|
52
|
+
// four-backtick example, or an info-string line like ```js inside an open
|
|
53
|
+
// fence, is content and does not end the block early.
|
|
54
|
+
function blankFencedBlocks(content) {
|
|
55
|
+
const lines = content.split(/\r?\n/);
|
|
56
|
+
const out = [];
|
|
57
|
+
let fenceChar = null;
|
|
58
|
+
let fenceLen = 0;
|
|
59
|
+
for (const line of lines) {
|
|
60
|
+
if (fenceChar) {
|
|
61
|
+
out.push('');
|
|
62
|
+
const close = line.match(/^[ \t]{0,3}(```+|~~~+)[ \t]*$/);
|
|
63
|
+
if (close && close[1][0] === fenceChar && close[1].length >= fenceLen) {
|
|
64
|
+
fenceChar = null;
|
|
65
|
+
fenceLen = 0;
|
|
66
|
+
}
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const open = line.match(/^[ \t]{0,3}(```+|~~~+)/);
|
|
70
|
+
if (open) {
|
|
71
|
+
fenceChar = open[1][0];
|
|
72
|
+
fenceLen = open[1].length;
|
|
73
|
+
out.push('');
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
out.push(line);
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// All Markdown heading texts (any level), fence-aware.
|
|
82
|
+
function extractHeadings(content) {
|
|
83
|
+
const headings = [];
|
|
84
|
+
for (const line of blankFencedBlocks(content)) {
|
|
85
|
+
const m = line.match(/^#{1,6}\s+(.+?)\s*#*\s*$/);
|
|
86
|
+
if (m) headings.push(m[1].trim());
|
|
87
|
+
}
|
|
88
|
+
return headings;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// H2 ("## ") heading texts only, fence-aware — the section shape of a template.
|
|
92
|
+
function extractH2Headings(content) {
|
|
93
|
+
const headings = [];
|
|
94
|
+
for (const line of blankFencedBlocks(content)) {
|
|
95
|
+
const m = line.match(/^##\s+(.+?)\s*#*\s*$/);
|
|
96
|
+
if (m) headings.push(m[1].trim());
|
|
97
|
+
}
|
|
98
|
+
return headings;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// `<file>.md § Heading` references whose file basename is `targetBasename`.
|
|
102
|
+
// A soft-wrapped heading name is captured by joining the next line; a match
|
|
103
|
+
// anchored beyond the current line is left to that line's own iteration.
|
|
104
|
+
function extractSectionRefs(content, targetBasename) {
|
|
105
|
+
const lines = blankFencedBlocks(content);
|
|
106
|
+
const refs = [];
|
|
107
|
+
lines.forEach((line, i) => {
|
|
108
|
+
const firstLen = line.replace(/\s+$/, '').length;
|
|
109
|
+
const joined = line.replace(/\s+$/, '') + ' ' + (lines[i + 1] || '').replace(/^\s+/, '');
|
|
110
|
+
for (const m of joined.matchAll(/`?([A-Za-z0-9._/-]+\.md)`?\s*§\s*"?([^."`)\n]+)/g)) {
|
|
111
|
+
if (m.index > firstLen) continue;
|
|
112
|
+
const base = m[1].split('/').pop();
|
|
113
|
+
if (base !== targetBasename) continue;
|
|
114
|
+
refs.push({ line: i + 1, headingText: m[2].trim() });
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
return refs;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function normalizeHeading(text) {
|
|
121
|
+
return text.replace(/[`*_]/g, '').trim();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Tolerant heading resolution: a reference resolves when it prefix-matches a
|
|
125
|
+
// real heading in either direction (covers soft wraps like "§ Workflow" for
|
|
126
|
+
// "Workflow Transparency" and shorthand references).
|
|
127
|
+
function headingResolves(referenceText, headings) {
|
|
128
|
+
const want = normalizeHeading(referenceText);
|
|
129
|
+
if (!want) return true;
|
|
130
|
+
return headings.some((heading) => {
|
|
131
|
+
const have = normalizeHeading(heading);
|
|
132
|
+
return have.startsWith(want) || want.startsWith(have);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Which of the template's H2 sections are missing from a filled document — the
|
|
137
|
+
// "created from an older template shape" signal.
|
|
138
|
+
function missingTemplateSections(templateContent, documentContent) {
|
|
139
|
+
const have = new Set(extractH2Headings(documentContent).map(normalizeHeading));
|
|
140
|
+
return extractH2Headings(templateContent).filter((heading) => !have.has(normalizeHeading(heading)));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// True when `content` matches `templateContent` up to placeholder substitution:
|
|
144
|
+
// every single-line `{...}` placeholder in the template may match any text.
|
|
145
|
+
// Distinguishes "pristine current starter" from "edited or older starter"
|
|
146
|
+
// without knowing the values init substituted.
|
|
147
|
+
function matchesTemplateWithPlaceholders(content, templateContent) {
|
|
148
|
+
const normalize = (s) => s.replace(/\r\n/g, '\n').replace(/\s+$/, '');
|
|
149
|
+
const parts = normalize(templateContent).split(/\{[^}\n]*\}/);
|
|
150
|
+
const pattern = `^${parts.map(escapeRegExp).join('[\\s\\S]*?')}$`;
|
|
151
|
+
try {
|
|
152
|
+
return new RegExp(pattern).test(normalize(content));
|
|
153
|
+
} catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function escapeRegExp(s) {
|
|
159
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
module.exports = {
|
|
163
|
+
GIT_POLICY_LINE_RE,
|
|
164
|
+
AI_COMMIT_MARKER_LINE_RE,
|
|
165
|
+
PROSE_LANGUAGE_LINE_RE,
|
|
166
|
+
TECH_STACK_ROW_RE,
|
|
167
|
+
MIGRATION_CONTEXT_ROW_RE,
|
|
168
|
+
GIT_POLICY_VALUES,
|
|
169
|
+
AI_COMMIT_MARKER_VALUES,
|
|
170
|
+
parseContextLine,
|
|
171
|
+
blankFencedBlocks,
|
|
172
|
+
extractHeadings,
|
|
173
|
+
extractH2Headings,
|
|
174
|
+
extractSectionRefs,
|
|
175
|
+
headingResolves,
|
|
176
|
+
missingTemplateSections,
|
|
177
|
+
matchesTemplateWithPlaceholders
|
|
178
|
+
};
|