devflow-kit 3.12.1 → 3.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/dist/agents/architect.md +10 -9
- package/dist/agents/code.md +6 -6
- package/dist/agents/design.md +7 -7
- package/dist/agents/harden.md +6 -8
- package/dist/agents/review.md +5 -6
- package/dist/cli/commands/init-seed.js +5 -1
- package/dist/cli/commands/init.js +27 -19
- package/dist/cli/commands/tdd.js +230 -0
- package/dist/cli/commands/uninstall.js +2 -2
- package/dist/cli.js +2 -0
- package/dist/commands/implement.md +1 -1
- package/dist/commands/plan.md +23 -61
- package/dist/compliance-off/commands/implement.md +1 -1
- package/dist/compliance-off/commands/plan.md +23 -61
- package/dist/compliance-off/skills/git/references/tracker/github/ensure-pr-ready.md +1 -0
- package/dist/compliance-off/skills/git/references/tracker/jira/ensure-pr-ready.md +1 -0
- package/dist/compliance-off/skills/git/references/tracker/linear/ensure-pr-ready.md +1 -0
- package/dist/compliance-tdd-off/agents/triage.md +173 -0
- package/dist/core/assets.js +40 -33
- package/dist/core/curation-variants.js +191 -76
- package/dist/core/feature-switch.js +3 -2
- package/dist/core/installed-variants.js +45 -14
- package/dist/core/manifest.js +3 -0
- package/dist/core/mds-variants.js +13 -6
- package/dist/core/plugins.js +38 -9
- package/dist/curation-compliance-off/commands/implement.md +1 -1
- package/dist/curation-compliance-off/commands/plan.md +23 -61
- package/dist/curation-compliance-tdd-off/agents/triage.md +170 -0
- package/dist/curation-off/agents/architect.md +10 -9
- package/dist/curation-off/agents/code.md +6 -6
- package/dist/curation-off/agents/design.md +7 -7
- package/dist/curation-off/agents/harden.md +6 -8
- package/dist/curation-off/agents/review.md +5 -6
- package/dist/curation-off/commands/implement.md +1 -1
- package/dist/curation-off/commands/plan.md +23 -61
- package/dist/curation-tdd-off/agents/code.md +173 -0
- package/dist/curation-tdd-off/agents/triage.md +171 -0
- package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +1 -0
- package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +1 -0
- package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +1 -0
- package/dist/targets/claude-code/agent-type-owner.js +20 -15
- package/dist/targets/claude-code/curation-install.js +120 -99
- package/dist/targets/claude-code/installer.js +26 -29
- package/dist/targets/claude-code/shipped-set.js +2 -3
- package/dist/tdd-off/agents/code.md +176 -0
- package/dist/tdd-off/agents/triage.md +174 -0
- package/dist/variant-axes.json +54 -27
- package/package.json +1 -1
- package/src/assets/agents/_partials/_design.mds +7 -7
- package/src/assets/agents/architect.mds +3 -2
- package/src/assets/agents/code.mds +19 -7
- package/src/assets/agents/harden.mds +6 -8
- package/src/assets/agents/plan.md +63 -0
- package/src/assets/agents/review.mds +5 -6
- package/src/assets/agents/triage.mds +4 -0
- package/src/assets/commands/implement.mds +1 -1
- package/src/assets/commands/plan.mds +22 -68
- package/src/assets/mds/git/_ops_steps.mds +1 -0
- package/src/assets/skills/agent-references/references/commands/plan-levels.md +3 -3
- package/src/assets/skills/agent-references/references/synthesize/planning.md +9 -9
- package/src/assets/skills/design-review/SKILL.md +9 -12
- package/src/assets/skills/design-review/references/anti-patterns.md +4 -1
- package/src/assets/skills/gap-analysis/SKILL.md +2 -2
- package/src/assets/skills/git/references/patterns.md +3 -0
- package/src/assets/skills/patterns/SKILL.md +2 -4
- package/src/assets/skills/quality-gates/SKILL.md +2 -2
- package/src/assets/skills/quality-gates/references/patterns.md +3 -3
- package/src/assets/skills/quality-gates/references/report-template.md +2 -2
- package/src/assets/skills/quality-gates/references/violations.md +5 -5
- package/src/assets/skills/test-driven-development/SKILL.md +5 -5
- package/src/assets/skills/test-driven-development/references/rationalization-prevention.md +1 -1
- package/src/assets/skills/testing/SKILL.md +9 -9
- package/src/assets/skills/testing/references/report-template.md +1 -1
- package/src/assets/skills/testing/references/violations.md +1 -1
package/dist/core/assets.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { join } from 'path';
|
|
2
2
|
import { getPackageRoot } from './paths.js';
|
|
3
3
|
import { COMPLIANCE_OFF_SKILL_REFS_OUTPUT_DIR, SKILL_REFS_OUTPUT_DIR, workflowFolderKey, workflowFoldersRootRelPath, } from './mds-variants.js';
|
|
4
|
-
import { ALL_SWITCHES_ON,
|
|
4
|
+
import { ALL_SWITCHES_ON, OFF_OUTPUT_DIRS, consultedOffKeys, } from './curation-variants.js';
|
|
5
5
|
/**
|
|
6
6
|
* Flat skills source directory: src/assets/skills/{name}/
|
|
7
7
|
* All plugins' skills live here directly (no per-plugin subdirectory).
|
|
@@ -47,6 +47,20 @@ export function scriptsDir() {
|
|
|
47
47
|
export function commandsDir() {
|
|
48
48
|
return join(getPackageRoot(), 'dist', 'commands');
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* The variant directory of one off key: <the key's tree>/{commands,agents}/
|
|
52
|
+
*
|
|
53
|
+
* The spelling comes from OFF_OUTPUT_DIRS in curation-variants.ts, the build's own
|
|
54
|
+
* destinations, rather than being retyped here. Absent until the build has run, so every
|
|
55
|
+
* reader tolerates its absence.
|
|
56
|
+
*
|
|
57
|
+
* @param key - The off variant key.
|
|
58
|
+
* @param kind - Which prompt kind's variants.
|
|
59
|
+
* @param root - Package root to resolve against (see agentsDir).
|
|
60
|
+
*/
|
|
61
|
+
export function variantOffDir(key, kind, root = getPackageRoot()) {
|
|
62
|
+
return join(root, ...OFF_OUTPUT_DIRS[key].split('/'), kind);
|
|
63
|
+
}
|
|
50
64
|
/**
|
|
51
65
|
* Curation-off variant directory: dist/curation-off/{commands,agents}/
|
|
52
66
|
*
|
|
@@ -54,14 +68,11 @@ export function commandsDir() {
|
|
|
54
68
|
* (D-LEARNING-VARIANTS), and only those. Absent until the build has run, so every
|
|
55
69
|
* reader tolerates its absence.
|
|
56
70
|
*
|
|
57
|
-
* The spelling comes from CURATION_OFF_OUTPUT_DIR in curation-variants.ts, the
|
|
58
|
-
* build's own destination, rather than being retyped here.
|
|
59
|
-
*
|
|
60
71
|
* @param kind - Which prompt kind's variants.
|
|
61
72
|
* @param root - Package root to resolve against (see agentsDir).
|
|
62
73
|
*/
|
|
63
74
|
export function curationOffDir(kind, root = getPackageRoot()) {
|
|
64
|
-
return
|
|
75
|
+
return variantOffDir('curation-off', kind, root);
|
|
65
76
|
}
|
|
66
77
|
/**
|
|
67
78
|
* Compliance-off variant directory: dist/compliance-off/{commands,agents}/
|
|
@@ -75,7 +86,7 @@ export function curationOffDir(kind, root = getPackageRoot()) {
|
|
|
75
86
|
* @param root - Package root to resolve against (see agentsDir).
|
|
76
87
|
*/
|
|
77
88
|
export function complianceOffDir(kind, root = getPackageRoot()) {
|
|
78
|
-
return
|
|
89
|
+
return variantOffDir('compliance-off', kind, root);
|
|
79
90
|
}
|
|
80
91
|
/**
|
|
81
92
|
* Both-off variant directory: dist/curation-compliance-off/{commands,agents}/
|
|
@@ -87,28 +98,23 @@ export function complianceOffDir(kind, root = getPackageRoot()) {
|
|
|
87
98
|
* @param root - Package root to resolve against (see agentsDir).
|
|
88
99
|
*/
|
|
89
100
|
export function bothOffDir(kind, root = getPackageRoot()) {
|
|
90
|
-
return
|
|
101
|
+
return variantOffDir('both-off', kind, root);
|
|
91
102
|
}
|
|
92
103
|
/**
|
|
93
104
|
* The off-variant directories of one kind that a machine with these switches consults,
|
|
94
|
-
* MOST-PREFERRED FIRST. Empty when
|
|
95
|
-
*
|
|
96
|
-
* The one place the
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (!compliance)
|
|
108
|
-
return [complianceOffDir(kind, root)];
|
|
109
|
-
if (!curation)
|
|
110
|
-
return [curationOffDir(kind, root)];
|
|
111
|
-
return [];
|
|
105
|
+
* MOST-PREFERRED FIRST. Empty when every switch is on.
|
|
106
|
+
*
|
|
107
|
+
* The one place the order is spelled; commandSourceDirs and agentSourceDirs both take it
|
|
108
|
+
* from here. It is generated from the variant table (consultedOffKeys): every off tree
|
|
109
|
+
* whose axes are all off on the machine, the tree with the most axes off first. The first
|
|
110
|
+
* tree that holds a host's file is therefore the file for the machine's off axes that the
|
|
111
|
+
* host carries an arm on. A machine with one switch off consults one tree. This order does
|
|
112
|
+
* not decide which file a compliance-armed host must have. The installer requires the
|
|
113
|
+
* file built for the machine's exact switches, so on a both-off machine a host with both
|
|
114
|
+
* arms needs its both-off file and never its curation-off file.
|
|
115
|
+
*/
|
|
116
|
+
function offVariantDirs(kind, switches, root) {
|
|
117
|
+
return consultedOffKeys(switches).map(key => variantOffDir(key, kind, root));
|
|
112
118
|
}
|
|
113
119
|
/**
|
|
114
120
|
* Prepend the off-variant directories to the normal lookup, keeping the list non-empty
|
|
@@ -122,10 +128,10 @@ function preferOff(off, normal) {
|
|
|
122
128
|
*
|
|
123
129
|
* D-LEARNING-VARIANT-INSTALL: with an axis off, the off-variant directories come
|
|
124
130
|
* first, so a host with an arm installs its off variant and a host with none falls
|
|
125
|
-
* through to the one compiled file. With
|
|
131
|
+
* through to the one compiled file. With every axis on the order is the normal
|
|
126
132
|
* single-directory lookup.
|
|
127
133
|
*
|
|
128
|
-
* @param switches - The machine's settled
|
|
134
|
+
* @param switches - The machine's settled switches (one per axis).
|
|
129
135
|
* @param root - Package root to resolve against (see agentsDir).
|
|
130
136
|
*/
|
|
131
137
|
export function commandSourceDirs(switches, root = getPackageRoot()) {
|
|
@@ -203,14 +209,15 @@ export function compiledWorkflowFoldersDir(key, root = getPackageRoot()) {
|
|
|
203
209
|
/**
|
|
204
210
|
* The workflow folders source root for a machine's switches: the tree built for exactly
|
|
205
211
|
* those switches. One function, so the install, the converge and the status read cannot
|
|
206
|
-
* pick different roots for one
|
|
212
|
+
* pick different roots for one set of switches.
|
|
207
213
|
*
|
|
208
214
|
* D-WORKFLOW-FOLDER-BUILD: a SINGLE root, never a preference list. A folder is complete
|
|
209
|
-
* under every key, so no reader merges two trees,
|
|
210
|
-
* through to a tree that still carries the text of an
|
|
211
|
-
* commandSourceDirs' consumers guard against with an
|
|
215
|
+
* under every key it is built under (FOLDER_VARIANT_KEYS), so no reader merges two trees,
|
|
216
|
+
* and a both-off machine never falls through to a tree that still carries the text of an
|
|
217
|
+
* axis it switched off (the failure commandSourceDirs' consumers guard against with an
|
|
218
|
+
* exact-key check). A folder has no tdd arm, so the tdd switch does not pick a root.
|
|
212
219
|
*
|
|
213
|
-
* @param switches - The machine's settled
|
|
220
|
+
* @param switches - The machine's settled switches (one per axis).
|
|
214
221
|
* @param root - Package root to resolve against (see agentsDir).
|
|
215
222
|
*/
|
|
216
223
|
export function workflowFoldersSourceDir(switches, root = getPackageRoot()) {
|
|
@@ -241,7 +248,7 @@ export function workflowFoldersSourceDir(switches, root = getPackageRoot()) {
|
|
|
241
248
|
* reading the both-on tree.
|
|
242
249
|
*
|
|
243
250
|
* @param root - Package root to resolve against (see agentsDir).
|
|
244
|
-
* @param switches - The machine's settled
|
|
251
|
+
* @param switches - The machine's settled switches (one per axis); every switch on by default.
|
|
245
252
|
*/
|
|
246
253
|
export function agentSourceDirs(root = getPackageRoot(), switches = ALL_SWITCHES_ON) {
|
|
247
254
|
return preferOff(offVariantDirs('agents', switches, root), [compiledAgentsDir(root), agentsDir(root)]);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Variant splitter: one compiled prompt body in, its variants out, on
|
|
3
|
-
* axes: curation and
|
|
2
|
+
* Variant splitter: one compiled prompt body in, its variants out, on the marker
|
|
3
|
+
* axes: curation, compliance and tdd.
|
|
4
4
|
*
|
|
5
5
|
* Pure module, zero I/O. Every refusal is a Result; the exiting shells are
|
|
6
6
|
* scripts/build-mds.ts, which renders the split errors into the build's single exit,
|
|
@@ -12,11 +12,15 @@
|
|
|
12
12
|
* the curation-on variant at today's path (dist/commands/x.md, dist/agents/x.md)
|
|
13
13
|
* and the curation-off variant at dist/curation-off/{commands,agents}/x.md, only
|
|
14
14
|
* for a host that has an arm. A prompt with no arm has one variant and no
|
|
15
|
-
* curation-off file. The
|
|
16
|
-
*
|
|
17
|
-
* `
|
|
18
|
-
*
|
|
19
|
-
*
|
|
15
|
+
* curation-off file. The other axes work the same way: a body is split on every
|
|
16
|
+
* axis into one variant per combination of axes turned off (VARIANT_KEYS: `on`,
|
|
17
|
+
* `curation-off`, `compliance-off` and `both-off` for curation and compliance, and
|
|
18
|
+
* `tdd-off`, `curation-tdd-off`, `compliance-tdd-off` and `curation-compliance-tdd-off`
|
|
19
|
+
* for the same four with tdd off), and the build writes a variant only for a host
|
|
20
|
+
* that has an arm on each axis that variant turns off. The off variants go to the
|
|
21
|
+
* trees of OFF_OUTPUT_DIRS (dist/curation-off/, dist/compliance-off/,
|
|
22
|
+
* dist/curation-compliance-off/, dist/tdd-off/, dist/curation-tdd-off/,
|
|
23
|
+
* dist/compliance-tdd-off/ and dist/curation-compliance-tdd-off/), and the manifest
|
|
20
24
|
* dist/variant-axes.json records which axes each host carries an arm on. The install
|
|
21
25
|
* order of the trees per machine is owned by src/core/assets.ts, and the install refusal
|
|
22
26
|
* for a compliance-off machine by src/targets/claude-code/installer.ts.
|
|
@@ -30,7 +34,8 @@
|
|
|
30
34
|
* <!-- curation:end --> closes the arm
|
|
31
35
|
*
|
|
32
36
|
* and `<!-- compliance:on -->`, `<!-- compliance:off -->` and
|
|
33
|
-
* `<!-- compliance:end -->` do the same for the compliance variants
|
|
37
|
+
* `<!-- compliance:end -->` do the same for the compliance variants, and
|
|
38
|
+
* `<!-- tdd:on -->`, `<!-- tdd:off -->` and `<!-- tdd:end -->` for the tdd variants.
|
|
34
39
|
*
|
|
35
40
|
* A marker line matches VARIANT_MARKER_RE. It may be indented and may sit inside
|
|
36
41
|
* a fenced block, a list, a define body or an agent's second frontmatter block,
|
|
@@ -43,9 +48,10 @@
|
|
|
43
48
|
* A line goes into a variant only when every arm open at that line matches the
|
|
44
49
|
* variant, so a body with no compliance marker splits on the curation axis to the
|
|
45
50
|
* same bytes it always did, and the compliance state of the variant changes nothing.
|
|
51
|
+
* The same holds for a body with no tdd marker.
|
|
46
52
|
*
|
|
47
|
-
* One axis may nest fully inside an arm of
|
|
48
|
-
* nest, and arms of
|
|
53
|
+
* One axis may nest fully inside an arm of another. Two arms of one axis never
|
|
54
|
+
* nest, and arms of different axes never overlap: at most one arm per axis is open, and
|
|
49
55
|
* the arm opened last is the one that closes first.
|
|
50
56
|
*
|
|
51
57
|
* The markers are INTERIM. MDS 0.4.4 emits an `@if` inside a code fence or on an
|
|
@@ -56,31 +62,33 @@
|
|
|
56
62
|
* @mdscript/mds upgrade it rides on proven byte-identical in dist/ and this module
|
|
57
63
|
* deleted, is dean0x/devflow#439; the compliance axis rides the same swap
|
|
58
64
|
* (dean0x/devflow#648). The mapping is one to one, per axis: `<!-- curation:on -->`
|
|
59
|
-
* becomes `@if curation
|
|
65
|
+
* becomes `@if curation:`, `<!-- compliance:on -->` becomes `@if compliance:` and
|
|
66
|
+
* `<!-- tdd:on -->` becomes `@if tdd:`,
|
|
60
67
|
* `off` becomes `@else:` and `end` becomes `@end`. A standalone `off` arm, with no
|
|
61
68
|
* on arm before it, becomes an `@if <axis>:` with an empty body followed by
|
|
62
|
-
* `@else:`. One axis nested inside
|
|
69
|
+
* `@else:`. One axis nested inside another becomes one `@if` inside the other,
|
|
63
70
|
* and an interleaving, which no `@if` nesting expresses, is a refusal here.
|
|
64
71
|
*
|
|
65
72
|
* What the splitter refuses, each as a VariantSplitError with the source line:
|
|
66
73
|
* - nesting: an `on` marker while an arm of its axis is open, or a second `off`
|
|
67
74
|
* in one arm;
|
|
68
|
-
* - an interleaving: an `off` or `end` marker for an arm while an arm of
|
|
75
|
+
* - an interleaving: an `off` or `end` marker for an arm while an arm of another
|
|
69
76
|
* axis, opened after it, is still open;
|
|
70
77
|
* - an `end` with no open arm of its axis;
|
|
71
78
|
* - an arm still open at the end of the body;
|
|
72
79
|
* - an empty arm (no line with content between its marker and the next);
|
|
73
80
|
* - a retired marker: a line that names the pre-rename spelling of the marker, in
|
|
74
81
|
* any spacing or case, which would otherwise ship as prompt text;
|
|
75
|
-
* - a stray marker: a line that names `<!-- curation
|
|
76
|
-
* is not a whole-line marker, which would otherwise ship as
|
|
82
|
+
* - a stray marker: a line that names `<!-- curation:`, `<!-- compliance:` or
|
|
83
|
+
* `<!-- tdd:` but is not a whole-line marker, which would otherwise ship as
|
|
84
|
+
* prompt text.
|
|
77
85
|
*
|
|
78
86
|
* The retired spelling is never accepted as an alias: the build fails on it, so a
|
|
79
87
|
* host that still carries it cannot ship a prompt with both variants silently equal.
|
|
80
88
|
*
|
|
81
89
|
* Scope guarantee: this module answers one question (what are the variants of this
|
|
82
90
|
* body, and where do they land?) and a cheap one per axis for the build to ask of a
|
|
83
|
-
* body that must carry no arms (
|
|
91
|
+
* body that must carry no arms (containsAxisMarker). It
|
|
84
92
|
* also renders the variant manifest the build writes beside the trees
|
|
85
93
|
* (renderVariantManifest) and parses it for the installer (parseVariantManifest), so the
|
|
86
94
|
* build and the installer share one layout. It decides
|
|
@@ -115,6 +123,19 @@ export const COMPLIANCE_OFF_OUTPUT_DIR = 'dist/compliance-off';
|
|
|
115
123
|
* tree cannot reach the files of another.
|
|
116
124
|
*/
|
|
117
125
|
export const BOTH_OFF_OUTPUT_DIR = 'dist/curation-compliance-off';
|
|
126
|
+
/**
|
|
127
|
+
* Repo-relative root of the tdd-off variants: tdd off, curation and compliance on.
|
|
128
|
+
*
|
|
129
|
+
* The three trees below are the same three off combinations of curation and compliance
|
|
130
|
+
* with tdd off as well. All are siblings under dist/, none inside another.
|
|
131
|
+
*/
|
|
132
|
+
export const TDD_OFF_OUTPUT_DIR = 'dist/tdd-off';
|
|
133
|
+
/** Repo-relative root of the variants with curation and tdd off, compliance on. */
|
|
134
|
+
export const CURATION_TDD_OFF_OUTPUT_DIR = 'dist/curation-tdd-off';
|
|
135
|
+
/** Repo-relative root of the variants with compliance and tdd off, curation on. */
|
|
136
|
+
export const COMPLIANCE_TDD_OFF_OUTPUT_DIR = 'dist/compliance-tdd-off';
|
|
137
|
+
/** Repo-relative root of the variants with every axis off: curation, compliance and tdd. */
|
|
138
|
+
export const ALL_OFF_OUTPUT_DIR = 'dist/curation-compliance-tdd-off';
|
|
118
139
|
/**
|
|
119
140
|
* Repo-relative root of the retired off tree, from before the rename.
|
|
120
141
|
*
|
|
@@ -132,6 +153,10 @@ export const OFF_OUTPUT_DIRS = {
|
|
|
132
153
|
'curation-off': CURATION_OFF_OUTPUT_DIR,
|
|
133
154
|
'compliance-off': COMPLIANCE_OFF_OUTPUT_DIR,
|
|
134
155
|
'both-off': BOTH_OFF_OUTPUT_DIR,
|
|
156
|
+
'tdd-off': TDD_OFF_OUTPUT_DIR,
|
|
157
|
+
'curation-tdd-off': CURATION_TDD_OFF_OUTPUT_DIR,
|
|
158
|
+
'compliance-tdd-off': COMPLIANCE_TDD_OFF_OUTPUT_DIR,
|
|
159
|
+
'curation-compliance-tdd-off': ALL_OFF_OUTPUT_DIR,
|
|
135
160
|
};
|
|
136
161
|
/**
|
|
137
162
|
* POSIX repo-relative path of an off variant of an output file: the kind's folder
|
|
@@ -146,44 +171,85 @@ export function variantOffRelPath(key, kind, fileName) {
|
|
|
146
171
|
// ---------------------------------------------------------------------------
|
|
147
172
|
// Markers
|
|
148
173
|
// ---------------------------------------------------------------------------
|
|
149
|
-
/**
|
|
150
|
-
|
|
174
|
+
/**
|
|
175
|
+
* The marker axes. Curation is listed first: it is the older axis and the first one a stray line is tested against.
|
|
176
|
+
*
|
|
177
|
+
* The per-axis records below are built from this list. A new axis also needs its own entries in
|
|
178
|
+
* VARIANT_KEYS and VARIANT_STATES (every combination), the off-tree constants and OFF_OUTPUT_DIRS,
|
|
179
|
+
* PRUNE_REASONS, MachineSwitchFeatures with machineSwitches, and FOLDER_ARMS. The type checker
|
|
180
|
+
* flags a missing per-axis record. A missing VARIANT_KEYS combination it does not flag: the table
|
|
181
|
+
* test over `2 ** VARIANT_AXES.length` rows pins that VARIANT_KEYS covers every combination.
|
|
182
|
+
*/
|
|
183
|
+
export const VARIANT_AXES = ['curation', 'compliance', 'tdd'];
|
|
184
|
+
/** A record with one entry per member of `keys`, built by `valueOf`. The one place a total record is cast from a loop. */
|
|
185
|
+
export function recordOf(keys, valueOf) {
|
|
186
|
+
const record = {};
|
|
187
|
+
for (const key of keys)
|
|
188
|
+
record[key] = valueOf(key);
|
|
189
|
+
return record;
|
|
190
|
+
}
|
|
191
|
+
/** A record with one entry per axis, built by `valueOf`. */
|
|
192
|
+
export function perAxis(valueOf) {
|
|
193
|
+
return recordOf(VARIANT_AXES, valueOf);
|
|
194
|
+
}
|
|
151
195
|
/** Every switch on: the machine whose prompts come from the normal compiled files. */
|
|
152
|
-
export const ALL_SWITCHES_ON = Object.freeze(
|
|
196
|
+
export const ALL_SWITCHES_ON = Object.freeze(perAxis(() => true));
|
|
197
|
+
/** True when two switch records agree on every axis. */
|
|
198
|
+
export function sameSwitches(a, b) {
|
|
199
|
+
return VARIANT_AXES.every(axis => a[axis] === b[axis]);
|
|
200
|
+
}
|
|
153
201
|
/**
|
|
154
|
-
*
|
|
202
|
+
* The machine's settled switch on each axis, from its features.
|
|
203
|
+
*
|
|
204
|
+
* The ONE place the switches are built from the machine state. Every consumer takes the
|
|
205
|
+
* returned value whole (the installer, the converge, the folders, the install record) and
|
|
206
|
+
* never spells a switch per axis, so a new axis changes this function and the
|
|
207
|
+
* VariantSwitches type and no call site.
|
|
208
|
+
*/
|
|
209
|
+
export function machineSwitches(features) {
|
|
210
|
+
return { curation: features.curation, compliance: features.compliance.enabled, tdd: features.tdd };
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* A whole-line marker of any axis. Group 1 captures the axis; group 2 captures
|
|
155
214
|
* `on`, `off` or `end`.
|
|
156
215
|
*
|
|
157
216
|
* Anchored at both ends with no nested quantifier, so a match is linear in the
|
|
158
217
|
* line length. Leading and trailing space or tab is allowed; anything else on the
|
|
159
|
-
* line is not a marker.
|
|
218
|
+
* line is not a marker. The axis names are fixed words, never input.
|
|
160
219
|
*/
|
|
161
|
-
export const VARIANT_MARKER_RE =
|
|
220
|
+
export const VARIANT_MARKER_RE = new RegExp(`^[ \\t]*<!-- (${VARIANT_AXES.join('|')}):(on|off|end) -->[ \\t]*$`);
|
|
162
221
|
/**
|
|
163
222
|
* The fragment that makes a line a marker ATTEMPT on one axis: loose on spacing and
|
|
164
223
|
* case so a malformed marker is caught rather than shipped. Every valid marker of
|
|
165
224
|
* the axis contains it. None is global, so `test` keeps no state between lines.
|
|
166
225
|
*/
|
|
167
|
-
const MARKER_FRAGMENT_RES = {
|
|
168
|
-
curation: /<!--\s*curation\s*:/i,
|
|
169
|
-
compliance: /<!--\s*compliance\s*:/i,
|
|
170
|
-
};
|
|
226
|
+
const MARKER_FRAGMENT_RES = perAxis(axis => new RegExp(`<!--\\s*${axis}\\s*:`, 'i'));
|
|
171
227
|
/**
|
|
172
228
|
* The same attempt test for the retired spelling of the marker. No spelling of it is
|
|
173
229
|
* valid, so a line that matches is always refused.
|
|
174
230
|
*/
|
|
175
231
|
const LEGACY_MARKER_FRAGMENT_RE = /<!--\s*learning\s*:/i;
|
|
232
|
+
/** The axis the retired spelling belonged to. */
|
|
233
|
+
const LEGACY_MARKER_AXIS = 'curation';
|
|
234
|
+
/** True when the text names a marker of the axis anywhere, valid or not; for curation also the retired spelling. */
|
|
235
|
+
export function containsAxisMarker(axis, text) {
|
|
236
|
+
return MARKER_FRAGMENT_RES[axis].test(text) || (axis === LEGACY_MARKER_AXIS && LEGACY_MARKER_FRAGMENT_RE.test(text));
|
|
237
|
+
}
|
|
176
238
|
/** True when the text names a curation marker anywhere, in the current or the retired spelling, valid or not. */
|
|
177
239
|
export function containsCurationMarker(text) {
|
|
178
|
-
return
|
|
240
|
+
return containsAxisMarker('curation', text);
|
|
179
241
|
}
|
|
180
242
|
/** True when the text names a compliance marker anywhere, valid or not. */
|
|
181
243
|
export function containsComplianceMarker(text) {
|
|
182
|
-
return
|
|
244
|
+
return containsAxisMarker('compliance', text);
|
|
183
245
|
}
|
|
184
|
-
/** True when the text names a marker
|
|
246
|
+
/** True when the text names a tdd marker anywhere, valid or not. */
|
|
247
|
+
export function containsTddMarker(text) {
|
|
248
|
+
return containsAxisMarker('tdd', text);
|
|
249
|
+
}
|
|
250
|
+
/** True when the text names a marker of any axis, or the retired spelling, valid or not. */
|
|
185
251
|
export function containsVariantMarker(text) {
|
|
186
|
-
return
|
|
252
|
+
return VARIANT_AXES.some(axis => containsAxisMarker(axis, text));
|
|
187
253
|
}
|
|
188
254
|
function markerOf(line) {
|
|
189
255
|
// A CRLF body carries the carriage return on the line; it is not part of the marker.
|
|
@@ -199,34 +265,65 @@ function attemptedAxisOf(line) {
|
|
|
199
265
|
}
|
|
200
266
|
return null;
|
|
201
267
|
}
|
|
202
|
-
/**
|
|
203
|
-
|
|
268
|
+
/**
|
|
269
|
+
* The variants of a body, one per combination of axes turned off, named by the axes
|
|
270
|
+
* they turn off. The four that keep tdd on come first and keep the names and trees
|
|
271
|
+
* they had before the tdd axis; the four that turn tdd off follow.
|
|
272
|
+
*/
|
|
273
|
+
export const VARIANT_KEYS = [
|
|
274
|
+
'on',
|
|
275
|
+
'curation-off',
|
|
276
|
+
'compliance-off',
|
|
277
|
+
'both-off',
|
|
278
|
+
'tdd-off',
|
|
279
|
+
'curation-tdd-off',
|
|
280
|
+
'compliance-tdd-off',
|
|
281
|
+
'curation-compliance-tdd-off',
|
|
282
|
+
];
|
|
204
283
|
/** Every variant that turns an axis off, in VARIANT_KEYS order. */
|
|
205
284
|
export const OFF_VARIANT_KEYS = VARIANT_KEYS.filter((key) => key !== 'on');
|
|
206
|
-
/** The arm each axis keeps in each variant. */
|
|
285
|
+
/** The arm each axis keeps in each variant. Total over VARIANT_KEYS, with one row per combination of axes. */
|
|
207
286
|
const VARIANT_STATES = {
|
|
208
|
-
on: { curation: 'on', compliance: 'on' },
|
|
209
|
-
'curation-off': { curation: 'off', compliance: 'on' },
|
|
210
|
-
'compliance-off': { curation: 'on', compliance: 'off' },
|
|
211
|
-
'both-off': { curation: 'off', compliance: 'off' },
|
|
287
|
+
on: { curation: 'on', compliance: 'on', tdd: 'on' },
|
|
288
|
+
'curation-off': { curation: 'off', compliance: 'on', tdd: 'on' },
|
|
289
|
+
'compliance-off': { curation: 'on', compliance: 'off', tdd: 'on' },
|
|
290
|
+
'both-off': { curation: 'off', compliance: 'off', tdd: 'on' },
|
|
291
|
+
'tdd-off': { curation: 'on', compliance: 'on', tdd: 'off' },
|
|
292
|
+
'curation-tdd-off': { curation: 'off', compliance: 'on', tdd: 'off' },
|
|
293
|
+
'compliance-tdd-off': { curation: 'on', compliance: 'off', tdd: 'off' },
|
|
294
|
+
'curation-compliance-tdd-off': { curation: 'off', compliance: 'off', tdd: 'off' },
|
|
212
295
|
};
|
|
296
|
+
/** The arm an axis keeps in a variant. */
|
|
297
|
+
export function variantArm(key, axis) {
|
|
298
|
+
return VARIANT_STATES[key][axis];
|
|
299
|
+
}
|
|
300
|
+
/** The axes a variant turns off, in VARIANT_AXES order. Empty for `on`. */
|
|
301
|
+
export function offAxesOf(key) {
|
|
302
|
+
return VARIANT_AXES.filter(axis => VARIANT_STATES[key][axis] === 'off');
|
|
303
|
+
}
|
|
213
304
|
/**
|
|
214
|
-
* The variants
|
|
305
|
+
* The off variants a machine with these switches can read, MOST-PREFERRED FIRST: every
|
|
306
|
+
* off key whose off axes are all off on the machine, the key with the most axes off
|
|
307
|
+
* first, and keys with as many in VARIANT_KEYS order. The first key a host has a file
|
|
308
|
+
* for is the file for the machine's off axes that the host carries an arm on. Empty
|
|
309
|
+
* when every switch is on.
|
|
310
|
+
*/
|
|
311
|
+
export function consultedOffKeys(machine) {
|
|
312
|
+
return OFF_VARIANT_KEYS
|
|
313
|
+
.filter(key => offAxesOf(key).every(axis => !machine[axis]))
|
|
314
|
+
.sort((a, b) => offAxesOf(b).length - offAxesOf(a).length);
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* The variants the build writes for a body, in VARIANT_KEYS order, `on` first.
|
|
215
318
|
*
|
|
216
319
|
* A variant is written only for a host that has an arm on each axis the variant turns
|
|
217
320
|
* off: `curation-off` when the body carries a curation arm, `compliance-off` when it
|
|
218
|
-
* carries a compliance arm,
|
|
219
|
-
*
|
|
321
|
+
* carries a compliance arm, `both-off` when it carries both, and the same for the
|
|
322
|
+
* variants that also turn tdd off. A body with no arm has the one variant, because
|
|
323
|
+
* every key would give the same bytes.
|
|
220
324
|
*/
|
|
221
325
|
export function emittedVariantKeys(hasArms) {
|
|
222
|
-
|
|
223
|
-
if (hasArms.curation)
|
|
224
|
-
keys.push('curation-off');
|
|
225
|
-
if (hasArms.compliance)
|
|
226
|
-
keys.push('compliance-off');
|
|
227
|
-
if (hasArms.curation && hasArms.compliance)
|
|
228
|
-
keys.push('both-off');
|
|
229
|
-
return keys;
|
|
326
|
+
return VARIANT_KEYS.filter(key => VARIANT_AXES.every(axis => VARIANT_STATES[key][axis] === 'on' || hasArms[axis]));
|
|
230
327
|
}
|
|
231
328
|
/** The longest stretch of a stray line a refusal quotes. */
|
|
232
329
|
const STRAY_QUOTE_MAX = 80;
|
|
@@ -325,7 +422,7 @@ function applyMarker(marker, lineNo, open) {
|
|
|
325
422
|
}
|
|
326
423
|
}
|
|
327
424
|
/**
|
|
328
|
-
* Split a compiled body into its
|
|
425
|
+
* Split a compiled body into its variants (VARIANT_KEYS).
|
|
329
426
|
*
|
|
330
427
|
* Total and pure: the same input gives the same output, and a refusal is a
|
|
331
428
|
* Result. The loop is one pass over the body's lines, so its bound is the body's
|
|
@@ -340,8 +437,8 @@ function applyMarker(marker, lineNo, open) {
|
|
|
340
437
|
*/
|
|
341
438
|
export function splitVariants(body) {
|
|
342
439
|
const lines = body.split('\n');
|
|
343
|
-
const kept =
|
|
344
|
-
const seen =
|
|
440
|
+
const kept = recordOf(VARIANT_KEYS, () => []);
|
|
441
|
+
const seen = perAxis(() => false);
|
|
345
442
|
const open = [];
|
|
346
443
|
for (let index = 0; index < lines.length; index++) {
|
|
347
444
|
const line = lines[index];
|
|
@@ -359,12 +456,7 @@ export function splitVariants(body) {
|
|
|
359
456
|
return Err({ kind: 'unclosed-arm', axis: unclosed.axis, arm: unclosed.arm, openedAt: unclosed.openedAt });
|
|
360
457
|
}
|
|
361
458
|
return Ok({
|
|
362
|
-
bodies:
|
|
363
|
-
on: kept.on.join('\n'),
|
|
364
|
-
'curation-off': kept['curation-off'].join('\n'),
|
|
365
|
-
'compliance-off': kept['compliance-off'].join('\n'),
|
|
366
|
-
'both-off': kept['both-off'].join('\n'),
|
|
367
|
-
},
|
|
459
|
+
bodies: recordOf(VARIANT_KEYS, key => kept[key].join('\n')),
|
|
368
460
|
hasArms: seen,
|
|
369
461
|
});
|
|
370
462
|
}
|
|
@@ -383,7 +475,7 @@ export function describeVariantSplitError(error) {
|
|
|
383
475
|
case 'interleaved-arm':
|
|
384
476
|
return (`line ${error.line}: a ${error.axis}:${error.marker} marker ends the ${error.axis} arm opened at line ` +
|
|
385
477
|
`${error.openedAt} while the ${error.innerAxis} arm opened at line ${error.innerOpenedAt} is still ` +
|
|
386
|
-
`open — arms of
|
|
478
|
+
`open — arms of different axes may nest but not overlap; close the ${error.innerAxis} arm with ` +
|
|
387
479
|
`${error.innerAxis}:end first`);
|
|
388
480
|
case 'repeated-off':
|
|
389
481
|
return (`line ${error.line}: a second ${error.axis}:off marker inside the off arm opened at line ` +
|
|
@@ -413,8 +505,8 @@ export function describeVariantSplitError(error) {
|
|
|
413
505
|
/**
|
|
414
506
|
* Repo-relative path of the variant manifest, written beside the variant trees.
|
|
415
507
|
*
|
|
416
|
-
* The installer reads it to learn which compiled
|
|
417
|
-
*
|
|
508
|
+
* The installer reads it to learn which axes each compiled prompt carries an arm on
|
|
509
|
+
* (curation, compliance, tdd), so it never has to guess from the files it finds.
|
|
418
510
|
*/
|
|
419
511
|
export const VARIANT_MANIFEST_REL_PATH = 'dist/variant-axes.json';
|
|
420
512
|
/** The manifest format version. A change to the shape of the file raises it. */
|
|
@@ -444,7 +536,7 @@ export function renderVariantManifest(hosts) {
|
|
|
444
536
|
const key = manifestKeyOf(host);
|
|
445
537
|
if (entries.has(key))
|
|
446
538
|
return Err({ kind: 'duplicate-host', host: key });
|
|
447
|
-
entries.set(key,
|
|
539
|
+
entries.set(key, perAxis(axis => host.axes[axis]));
|
|
448
540
|
}
|
|
449
541
|
const sorted = {};
|
|
450
542
|
for (const key of [...entries.keys()].sort(byCodeUnit)) {
|
|
@@ -505,17 +597,38 @@ export function parseVariantManifest(text) {
|
|
|
505
597
|
const match = MANIFEST_HOST_KEY_RE.exec(key);
|
|
506
598
|
if (match === null)
|
|
507
599
|
return Err({ kind: 'bad-host-key', key });
|
|
508
|
-
if (!isPlainRecord(value)
|
|
600
|
+
if (!isPlainRecord(value))
|
|
509
601
|
return Err({ kind: 'bad-host-axes', host: key });
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
kind:
|
|
513
|
-
|
|
514
|
-
axes: { curation: value.curation, compliance: value.compliance },
|
|
515
|
-
});
|
|
602
|
+
const axes = parseManifestAxes(value);
|
|
603
|
+
if (axes === null)
|
|
604
|
+
return Err({ kind: 'bad-host-axes', host: key });
|
|
605
|
+
hosts.push({ kind: match[1], name: match[2], axes });
|
|
516
606
|
}
|
|
517
607
|
return Ok(hosts);
|
|
518
608
|
}
|
|
609
|
+
/**
|
|
610
|
+
* The axes that existed before any later axis was added. A variant manifest or an install
|
|
611
|
+
* record written before a later axis existed has no entry for it; these axes stay required
|
|
612
|
+
* in both. An absent later axis reads as no arm in the manifest and as on in the record.
|
|
613
|
+
*/
|
|
614
|
+
export const ORIGINAL_AXES = ['curation', 'compliance'];
|
|
615
|
+
/**
|
|
616
|
+
* The axes of one host entry, or null when an axis is not a boolean. An axis that is not
|
|
617
|
+
* in ORIGINAL_AXES may be absent, and then it is false.
|
|
618
|
+
*/
|
|
619
|
+
function parseManifestAxes(entry) {
|
|
620
|
+
const read = {};
|
|
621
|
+
for (const axis of VARIANT_AXES) {
|
|
622
|
+
const value = entry[axis];
|
|
623
|
+
if (typeof value === 'boolean')
|
|
624
|
+
read[axis] = value;
|
|
625
|
+
else if (value === undefined && !ORIGINAL_AXES.includes(axis))
|
|
626
|
+
read[axis] = false;
|
|
627
|
+
else
|
|
628
|
+
return null;
|
|
629
|
+
}
|
|
630
|
+
return perAxis(axis => read[axis] === true);
|
|
631
|
+
}
|
|
519
632
|
/** Render a manifest parse refusal as one line a person can act on. */
|
|
520
633
|
export function describeVariantManifestParseError(error) {
|
|
521
634
|
switch (error.kind) {
|
|
@@ -532,7 +645,7 @@ export function describeVariantManifestParseError(error) {
|
|
|
532
645
|
case 'bad-host-key':
|
|
533
646
|
return `the host key "${error.key}" is not <commands|agents>/<name>`;
|
|
534
647
|
case 'bad-host-axes':
|
|
535
|
-
return `the host ${error.host} does not carry a boolean "curation" and a boolean "compliance"`;
|
|
648
|
+
return `the host ${error.host} does not carry a boolean "curation" and a boolean "compliance" (and, when present, a boolean "tdd")`;
|
|
536
649
|
default: {
|
|
537
650
|
const unhandled = error;
|
|
538
651
|
return `unhandled variant manifest parse error ${JSON.stringify(unhandled)}`;
|
|
@@ -550,13 +663,15 @@ export function describeVariantManifestParseError(error) {
|
|
|
550
663
|
* have and never the on file.
|
|
551
664
|
*/
|
|
552
665
|
export function machineVariantKey(machine, hasArms) {
|
|
553
|
-
const
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
666
|
+
const off = VARIANT_AXES.filter(axis => !machine[axis] && hasArms[axis]);
|
|
667
|
+
// VARIANT_STATES has one row per combination of axes, so exactly one off key matches a
|
|
668
|
+
// non-empty set (the table test pins that for every machine state and arm set), and
|
|
669
|
+
// `on` is the answer when nothing is off.
|
|
670
|
+
const found = OFF_VARIANT_KEYS.find(key => {
|
|
671
|
+
const turnedOff = offAxesOf(key);
|
|
672
|
+
return turnedOff.length === off.length && turnedOff.every(axis => off.includes(axis));
|
|
673
|
+
});
|
|
674
|
+
return found ?? 'on';
|
|
560
675
|
}
|
|
561
676
|
/**
|
|
562
677
|
* The repo-relative off files a machine must find for the hosts that carry a compliance
|
|
@@ -18,6 +18,7 @@ const CURRENT_KEYS = {
|
|
|
18
18
|
memory: ['memory'],
|
|
19
19
|
curation: ['curation', 'learning'],
|
|
20
20
|
knowledge: ['knowledge'],
|
|
21
|
+
tdd: ['tdd'],
|
|
21
22
|
};
|
|
22
23
|
/**
|
|
23
24
|
* The pre-rename key each machine feature was stored under, where one exists:
|
|
@@ -179,8 +180,8 @@ export async function readOptInFeature(devflowDir, feature) {
|
|
|
179
180
|
* D-NOOP-TOGGLE: a toggle to the value the manifest already records writes
|
|
180
181
|
* nothing. The only bytes such a write could change are `updatedAt`, and that
|
|
181
182
|
* field means "the manifest's content last changed", so a no-op must not move
|
|
182
|
-
* it. This is the one write point of `devflow memory|curation|knowledge|behavior
|
|
183
|
-
* --enable/--disable`, so the rule holds for all
|
|
183
|
+
* it. This is the one write point of `devflow memory|curation|knowledge|behavior|tdd
|
|
184
|
+
* --enable/--disable`, so the rule holds for all five at once.
|
|
184
185
|
*
|
|
185
186
|
* The read and the write run under the manifest lock (manifest-lock.ts), so the
|
|
186
187
|
* heal in readManifest cannot write an older snapshot over this write. The wait is
|