@hone-ai/cli 1.4.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/bin/hone.js +2 -0
- package/hone-cli.js +4006 -0
- package/lib/README.md +119 -0
- package/lib/adversarial-negative-lint.js +149 -0
- package/lib/audit.js +156 -0
- package/lib/auto-detect.js +213 -0
- package/lib/autofix-guardrails.js +124 -0
- package/lib/branch-protection.js +256 -0
- package/lib/ci-classifier.js +150 -0
- package/lib/ci-failures.js +173 -0
- package/lib/claude-md-tokens.js +71 -0
- package/lib/compliance-check.js +62 -0
- package/lib/config-augment.js +133 -0
- package/lib/config-update.js +70 -0
- package/lib/dependency-audit.js +108 -0
- package/lib/derive-domain.js +185 -0
- package/lib/doc-registry.js +63 -0
- package/lib/doctor-admin-merge.js +185 -0
- package/lib/doctor-bind-default.js +118 -0
- package/lib/doctor-docs.js +205 -0
- package/lib/doctor-placeholders.js +144 -0
- package/lib/doctor-skill-staleness.js +122 -0
- package/lib/domain-skill-template.md +114 -0
- package/lib/editor-detect.js +169 -0
- package/lib/fast-track-ratify.js +133 -0
- package/lib/git-helpers.js +109 -0
- package/lib/hook-templates/pre-commit.sh +54 -0
- package/lib/hook-templates/pre-push.sh +72 -0
- package/lib/install-hooks.js +205 -0
- package/lib/knowledge-graph.js +188 -0
- package/lib/learnings-audit.js +254 -0
- package/lib/learnings-parse.js +331 -0
- package/lib/learnings-sync.js +75 -0
- package/lib/mcp-detect.js +154 -0
- package/lib/metrics-collect.js +214 -0
- package/lib/overlay-merge.js +267 -0
- package/lib/performance-analyzer.js +142 -0
- package/lib/pipeline-config.js +83 -0
- package/lib/pipeline-status.js +207 -0
- package/lib/pipeline-validate.js +322 -0
- package/lib/platform-detect.js +86 -0
- package/lib/platform-discover.js +334 -0
- package/lib/publish-learning.js +160 -0
- package/lib/python-install.js +84 -0
- package/lib/refresh-check.js +67 -0
- package/lib/refresh-knowledge.js +360 -0
- package/lib/rule-resolver.js +146 -0
- package/lib/security-scanner.js +168 -0
- package/lib/setup-grounding.js +138 -0
- package/lib/skill-assertions.js +276 -0
- package/lib/skill-audit-render.js +158 -0
- package/lib/skill-audit.js +391 -0
- package/lib/stack-detect.js +170 -0
- package/lib/stack-paths.js +285 -0
- package/lib/story-classifier-extract.js +203 -0
- package/lib/story-classifier.js +282 -0
- package/lib/sync-overwrite.js +47 -0
- package/lib/synthetic-pipeline.js +299 -0
- package/lib/validate-metadata.js +175 -0
- package/package.json +41 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* story-classifier.js — SC-001 (#user-request 2026-05-05): pure helper
|
|
4
|
+
* that recommends SDLC track + agent invocation per story.
|
|
5
|
+
*
|
|
6
|
+
* Inputs (9 fields per architect plan):
|
|
7
|
+
* - type: bug | enhancement | feature | refactor | docs | chore | meta-epic
|
|
8
|
+
* - adopter_blast_radius: zero | low | medium | high | critical
|
|
9
|
+
* - surface_area: single-file | single-module | multi-module | cross-package | external-dep
|
|
10
|
+
* - estimate: XS | S | M | L | XL
|
|
11
|
+
* - design_options: "1" | "2-3" | "many"
|
|
12
|
+
* - recently_modified_overlap: bool
|
|
13
|
+
* - cross_repo_dependency: bool
|
|
14
|
+
* - has_test_matrix: simple | parametrized | exhaustive
|
|
15
|
+
* - has_security_implications: bool
|
|
16
|
+
* - is_first_of_its_kind: bool
|
|
17
|
+
*
|
|
18
|
+
* Output:
|
|
19
|
+
* { track, architect: { engage, axes }, agents, artifacts, rationale, inputs_echo }
|
|
20
|
+
*
|
|
21
|
+
* Pipeline-of-rules architecture (per AU-001-L1):
|
|
22
|
+
* Pass 1: shouldEngageArchitect
|
|
23
|
+
* Pass 2: determineTrack
|
|
24
|
+
* Pass 3: recommendAgents
|
|
25
|
+
*
|
|
26
|
+
* Pure-helper-with-injected-IO style (Category B per cli/lib/README.md):
|
|
27
|
+
* - No filesystem, no network, no process.exit
|
|
28
|
+
* - All inputs explicit; no environment lookups
|
|
29
|
+
* - Same shape as cli/lib/skill-audit.js, cli/lib/learnings-audit.js
|
|
30
|
+
*
|
|
31
|
+
* Issue: user-request 2026-05-05 (SC-001 family — Phase 1).
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
// ─── Enums (frozen, exported for test-shape assertions) ───
|
|
35
|
+
|
|
36
|
+
const TRACKS = Object.freeze({
|
|
37
|
+
HOT_FIX: 'hot-fix',
|
|
38
|
+
FULL_SDLC: 'full-sdlc',
|
|
39
|
+
FAST_TRACK: 'fast-track',
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const ARCHITECT_AXES = Object.freeze({
|
|
43
|
+
CROSS_PACKAGE: 'cross-package',
|
|
44
|
+
MULTIPLE_DESIGN_OPTIONS: 'multiple-design-options',
|
|
45
|
+
META_OR_EPIC: 'meta-or-epic',
|
|
46
|
+
RECENTLY_MODIFIED: 'recently-modified-overlap',
|
|
47
|
+
FIRST_OF_ITS_KIND: 'first-of-its-kind',
|
|
48
|
+
EXTERNAL_COORDINATION: 'external-coordination',
|
|
49
|
+
CROSS_REPO_DEPENDENCY: 'cross-repo-dependency',
|
|
50
|
+
SECURITY_IMPLICATIONS: 'security-implications',
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Estimate ordering for threshold comparisons
|
|
54
|
+
const ESTIMATE_ORDER = ['XS', 'S', 'M', 'L', 'XL'];
|
|
55
|
+
function estimateGreaterOrEqual(a, b) {
|
|
56
|
+
const ai = ESTIMATE_ORDER.indexOf(String(a || '').toUpperCase());
|
|
57
|
+
const bi = ESTIMATE_ORDER.indexOf(String(b || '').toUpperCase());
|
|
58
|
+
if (ai < 0 || bi < 0) return false;
|
|
59
|
+
return ai >= bi;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Default thresholds (some configurable via opts; SAFETY-CRITICAL ones not)
|
|
63
|
+
const DEFAULT_THRESHOLDS = Object.freeze({
|
|
64
|
+
estimate_full_sdlc: 'L', // configurable; team risk tolerance
|
|
65
|
+
recently_modified_window_days: 14, // configurable
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// ─── Pass 1: Architect engagement (any-of) ───
|
|
69
|
+
|
|
70
|
+
function shouldEngageArchitect(input) {
|
|
71
|
+
const axes = [];
|
|
72
|
+
// Cross-package or external-dep surface → architect for sequencing
|
|
73
|
+
if (input.surface_area === 'cross-package' || input.surface_area === 'external-dep') {
|
|
74
|
+
axes.push(ARCHITECT_AXES.CROSS_PACKAGE);
|
|
75
|
+
}
|
|
76
|
+
// Multiple design options → architect for tradeoff analysis
|
|
77
|
+
if (input.design_options === 'many' || input.design_options === '2-3') {
|
|
78
|
+
axes.push(ARCHITECT_AXES.MULTIPLE_DESIGN_OPTIONS);
|
|
79
|
+
}
|
|
80
|
+
// Meta / epic → architect for decomposition
|
|
81
|
+
if (input.type === 'meta-epic') {
|
|
82
|
+
axes.push(ARCHITECT_AXES.META_OR_EPIC);
|
|
83
|
+
}
|
|
84
|
+
// Recently-modified file overlap → architect for gap analysis vs prior PR
|
|
85
|
+
if (input.recently_modified_overlap === true) {
|
|
86
|
+
axes.push(ARCHITECT_AXES.RECENTLY_MODIFIED);
|
|
87
|
+
}
|
|
88
|
+
// First-of-its-kind pattern → architect for novel-pattern review
|
|
89
|
+
if (input.is_first_of_its_kind === true) {
|
|
90
|
+
axes.push(ARCHITECT_AXES.FIRST_OF_ITS_KIND);
|
|
91
|
+
}
|
|
92
|
+
// Cross-repo dependency → architect for upstream coordination
|
|
93
|
+
if (input.cross_repo_dependency === true) {
|
|
94
|
+
axes.push(ARCHITECT_AXES.CROSS_REPO_DEPENDENCY);
|
|
95
|
+
}
|
|
96
|
+
// Security implications → architect for risk review
|
|
97
|
+
if (input.has_security_implications === true) {
|
|
98
|
+
axes.push(ARCHITECT_AXES.SECURITY_IMPLICATIONS);
|
|
99
|
+
}
|
|
100
|
+
return { engage: axes.length > 0, axes };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ─── Pass 2: Track determination (priority order) ───
|
|
104
|
+
|
|
105
|
+
function determineTrack(input, thresholds) {
|
|
106
|
+
const reasons = [];
|
|
107
|
+
|
|
108
|
+
// ── SAFETY-CRITICAL RULES (not configurable) ──
|
|
109
|
+
|
|
110
|
+
// R1: security implications → full-sdlc, never fast-track or hot-fix.
|
|
111
|
+
// HC-009: moved above the hot-fix rule. Security bugs need full-sdlc
|
|
112
|
+
// review even when blast radius + surface area would qualify for hot-fix.
|
|
113
|
+
if (input.has_security_implications === true) {
|
|
114
|
+
return {
|
|
115
|
+
track: TRACKS.FULL_SDLC,
|
|
116
|
+
reason: 'has_security_implications=true → full-sdlc (safety-critical, framework-fixed)',
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// R2: bug + high-or-critical adopter blast + single surface → hot-fix path.
|
|
121
|
+
// Architect's "100% on hot-fix safety axis" target.
|
|
122
|
+
// HC-009: now checked AFTER R1 (security). A security bug always gets
|
|
123
|
+
// full-sdlc regardless of blast radius or surface area.
|
|
124
|
+
if (input.type === 'bug'
|
|
125
|
+
&& (input.adopter_blast_radius === 'high' || input.adopter_blast_radius === 'critical')
|
|
126
|
+
&& (input.surface_area === 'single-file' || input.surface_area === 'single-module')) {
|
|
127
|
+
return {
|
|
128
|
+
track: TRACKS.HOT_FIX,
|
|
129
|
+
reason: `bug + adopter_blast_radius=${input.adopter_blast_radius} + ${input.surface_area} → hot-fix path (safety-critical, framework-fixed)`,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// R3: first-of-its-kind → full-sdlc.
|
|
134
|
+
if (input.is_first_of_its_kind === true) {
|
|
135
|
+
return {
|
|
136
|
+
track: TRACKS.FULL_SDLC,
|
|
137
|
+
reason: 'is_first_of_its_kind=true → full-sdlc (architect engagement axis)',
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ── CONFIGURABLE RULES ──
|
|
142
|
+
|
|
143
|
+
// R4: large estimate → full-sdlc (configurable threshold)
|
|
144
|
+
const fullSdlcEstimate = thresholds.estimate_full_sdlc || DEFAULT_THRESHOLDS.estimate_full_sdlc;
|
|
145
|
+
if (estimateGreaterOrEqual(input.estimate, fullSdlcEstimate)) {
|
|
146
|
+
return {
|
|
147
|
+
track: TRACKS.FULL_SDLC,
|
|
148
|
+
reason: `estimate=${input.estimate} ≥ ${fullSdlcEstimate} → full-sdlc (configurable threshold)`,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// R5: recently-modified overlap → full-sdlc, BUT ONLY when the work
|
|
153
|
+
// has real blast radius. Tiny framework-tooling iterations
|
|
154
|
+
// (RP-002..005 style: XS, blast_radius=zero, no adopter impact) get
|
|
155
|
+
// multiple sequential PRs touching the same file but don't need full
|
|
156
|
+
// SDLC ceremony each time. Calibration finding from SC-001 corpus.
|
|
157
|
+
// Architect engagement still triggers (axes include "recently-modified");
|
|
158
|
+
// the team just doesn't need full step-0..5 artifacts for an XS iteration.
|
|
159
|
+
if (input.recently_modified_overlap === true
|
|
160
|
+
&& input.adopter_blast_radius !== 'zero'
|
|
161
|
+
&& input.estimate !== 'XS') {
|
|
162
|
+
return {
|
|
163
|
+
track: TRACKS.FULL_SDLC,
|
|
164
|
+
reason: 'recently_modified_overlap=true (with non-zero blast or non-XS estimate) → full-sdlc (parametrized regression coverage)',
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// R6: exhaustive test matrix → full-sdlc
|
|
169
|
+
if (input.has_test_matrix === 'exhaustive') {
|
|
170
|
+
return {
|
|
171
|
+
track: TRACKS.FULL_SDLC,
|
|
172
|
+
reason: 'has_test_matrix=exhaustive → full-sdlc',
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// R7: cross-package surface area + non-trivial → full-sdlc
|
|
177
|
+
if (input.surface_area === 'cross-package' && input.type !== 'chore') {
|
|
178
|
+
return {
|
|
179
|
+
track: TRACKS.FULL_SDLC,
|
|
180
|
+
reason: 'surface_area=cross-package + non-chore → full-sdlc',
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// R8 (default): fast-track
|
|
185
|
+
return {
|
|
186
|
+
track: TRACKS.FAST_TRACK,
|
|
187
|
+
reason: 'no escalation rules matched → fast-track (default)',
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ─── Pass 3: Per-step agent + artifact recommendation ───
|
|
192
|
+
|
|
193
|
+
function recommendAgents(input, track) {
|
|
194
|
+
const agents = [];
|
|
195
|
+
const artifacts = [];
|
|
196
|
+
|
|
197
|
+
// HC-013b: hot-fix is MINIMAL — speed is critical, skip grooming + plan.
|
|
198
|
+
if (track === TRACKS.HOT_FIX) {
|
|
199
|
+
agents.push('unit-test-writer', 'code-builder', 'code-reviewer');
|
|
200
|
+
artifacts.push('step-2-tests.md', 'step-4-implementation.md', 'step-5-review.md');
|
|
201
|
+
return { agents, artifacts };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// HC-013b: fast-track gets LIGHTWEIGHT grooming + plan (not skip them).
|
|
205
|
+
// Without a plan, the test-writer doesn't know WHAT to test. Without
|
|
206
|
+
// grooming, acceptance criteria are implicit. The cost of a lightweight
|
|
207
|
+
// plan (30 seconds) is far less than the cost of rework from skipping it.
|
|
208
|
+
//
|
|
209
|
+
// full-sdlc gets FULL grooming + plan (unchanged).
|
|
210
|
+
agents.push('story-groomer', 'implementation-planner', 'unit-test-writer');
|
|
211
|
+
artifacts.push('step-0-grooming.md', 'step-1-plan.md', 'step-2-tests.md');
|
|
212
|
+
|
|
213
|
+
// Skip E2E for CLI-internal / pure-helper / single-module surfaces.
|
|
214
|
+
// Add E2E for cross-package + non-doc/chore types.
|
|
215
|
+
const surface = input && input.surface_area;
|
|
216
|
+
const type = input && input.type;
|
|
217
|
+
if ((surface === 'multi-module' || surface === 'cross-package')
|
|
218
|
+
&& type !== 'docs' && type !== 'chore') {
|
|
219
|
+
agents.push('e2e-qa-planner');
|
|
220
|
+
artifacts.push('step-3-e2e-plan.md');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
agents.push('code-builder', 'code-reviewer');
|
|
224
|
+
artifacts.push('step-4-implementation.md', 'step-5-review.md');
|
|
225
|
+
|
|
226
|
+
return { agents, artifacts };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ─── Public API ───
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Classify a story by SDLC track + architect engagement axes.
|
|
233
|
+
*
|
|
234
|
+
* @param {object} input - 9 classification fields (see file header)
|
|
235
|
+
* @param {object} [thresholds] - optional adopter overrides (estimate_full_sdlc,
|
|
236
|
+
* recently_modified_window_days). SAFETY-CRITICAL
|
|
237
|
+
* rules ignore overrides.
|
|
238
|
+
* @returns {{
|
|
239
|
+
* track: string,
|
|
240
|
+
* architect: { engage: boolean, axes: string[] },
|
|
241
|
+
* agents: string[],
|
|
242
|
+
* artifacts: string[],
|
|
243
|
+
* rationale: string,
|
|
244
|
+
* inputs_echo: object,
|
|
245
|
+
* }}
|
|
246
|
+
*/
|
|
247
|
+
function classifyStory(input, thresholds) {
|
|
248
|
+
// Defensive: handle null/undefined/non-object inputs
|
|
249
|
+
const safeInput = (input && typeof input === 'object') ? input : {};
|
|
250
|
+
const safeThresholds = (thresholds && typeof thresholds === 'object') ? thresholds : {};
|
|
251
|
+
|
|
252
|
+
const architect = shouldEngageArchitect(safeInput);
|
|
253
|
+
const trackResult = determineTrack(safeInput, safeThresholds);
|
|
254
|
+
const { agents, artifacts } = recommendAgents(safeInput, trackResult.track);
|
|
255
|
+
|
|
256
|
+
// Build rationale: combine track decision + architect axes
|
|
257
|
+
const rationaleParts = [trackResult.reason];
|
|
258
|
+
if (architect.engage) {
|
|
259
|
+
rationaleParts.push(`architect engagement axes: [${architect.axes.join(', ')}]`);
|
|
260
|
+
}
|
|
261
|
+
const rationale = rationaleParts.join(' | ');
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
track: trackResult.track,
|
|
265
|
+
architect,
|
|
266
|
+
agents,
|
|
267
|
+
artifacts,
|
|
268
|
+
rationale,
|
|
269
|
+
inputs_echo: { ...safeInput },
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
module.exports = {
|
|
274
|
+
classifyStory,
|
|
275
|
+
shouldEngageArchitect,
|
|
276
|
+
determineTrack,
|
|
277
|
+
recommendAgents,
|
|
278
|
+
TRACKS,
|
|
279
|
+
ARCHITECT_AXES,
|
|
280
|
+
DEFAULT_THRESHOLDS,
|
|
281
|
+
estimateGreaterOrEqual,
|
|
282
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* sync-overwrite.js — H-024 pure helper for `hone sync`.
|
|
4
|
+
*
|
|
5
|
+
* Detects whether a local file has been edited away from the
|
|
6
|
+
* server-delivered version, so we can warn before overwriting and
|
|
7
|
+
* silently destroying local additions (the OPS-2 / E13-A case).
|
|
8
|
+
*
|
|
9
|
+
* Pure function — no I/O. The CLI shell handles fs reads + writes.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {string} serverContent — what `hone sync` is about to write
|
|
14
|
+
* @param {string|null} localContent — current file contents, or null if file doesn't exist
|
|
15
|
+
* @returns {{ kind: 'new'|'identical'|'modified', hasLocalEdits: boolean }}
|
|
16
|
+
*/
|
|
17
|
+
function detectLocalEdits(serverContent, localContent) {
|
|
18
|
+
if (localContent === null || localContent === undefined) {
|
|
19
|
+
return { kind: 'new', hasLocalEdits: false };
|
|
20
|
+
}
|
|
21
|
+
if (normalize(serverContent) === normalize(localContent)) {
|
|
22
|
+
return { kind: 'identical', hasLocalEdits: false };
|
|
23
|
+
}
|
|
24
|
+
return { kind: 'modified', hasLocalEdits: true };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Normalize line endings + trailing whitespace so we don't false-positive
|
|
29
|
+
* on CRLF↔LF or editor-stripped trailing spaces. Keep it simple: this is
|
|
30
|
+
* a "should we warn" gate, not a content-equality contract.
|
|
31
|
+
*/
|
|
32
|
+
function normalize(s) {
|
|
33
|
+
return String(s).replace(/\r\n/g, '\n').replace(/[ \t]+$/gm, '').trimEnd();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Build a single-line warning the CLI prints when refusing to overwrite.
|
|
38
|
+
*
|
|
39
|
+
* @param {string} relPath — relative path the user will recognize
|
|
40
|
+
* @returns {string}
|
|
41
|
+
*/
|
|
42
|
+
function formatSkipMessage(relPath) {
|
|
43
|
+
return ` ⚠ skipping ${relPath} — local edits not on server. ` +
|
|
44
|
+
`Use \`hone promote\` to upstream them, or pass \`--force\` to overwrite.`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = { detectLocalEdits, formatSkipMessage, normalize };
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* synthetic-pipeline.js — H-086 synthetic SDLC verification fixtures + verifier.
|
|
4
|
+
*
|
|
5
|
+
* The CI-grade harness for "did the editor actually APPLY rule X" verification.
|
|
6
|
+
* Pure helper: generates synthetic story fixtures with expected rule-applied
|
|
7
|
+
* markers, and verifies captured pipeline output against them.
|
|
8
|
+
*
|
|
9
|
+
* M scope:
|
|
10
|
+
* - SYNTHETIC_FIXTURES — library of synthetic stories, each with known
|
|
11
|
+
* properties + expected markers per pipeline step
|
|
12
|
+
* - generateFixture(scenario) — produces { story, expectedMarkers }
|
|
13
|
+
* - verifyMarkers({ capturedOutputs, expectedMarkers }) — asserts each
|
|
14
|
+
* expected marker appears in the matching step's captured output
|
|
15
|
+
*
|
|
16
|
+
* L scope deferred:
|
|
17
|
+
* - Per-editor runtime drivers (Claude CLI invocation, Copilot probe, etc.)
|
|
18
|
+
* - CI workflow cron template
|
|
19
|
+
* - Slack/issue notification on regression
|
|
20
|
+
* - enterprise/SDLC_VERIFICATION.md doc
|
|
21
|
+
*
|
|
22
|
+
* Closes #86 (M scope).
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Library of synthetic story fixtures. Each fixture exercises a SPECIFIC
|
|
27
|
+
* rule that should fire when the story is run through the pipeline.
|
|
28
|
+
*
|
|
29
|
+
* Fixture shape:
|
|
30
|
+
* {
|
|
31
|
+
* name: string,
|
|
32
|
+
* description: string,
|
|
33
|
+
* story_metadata: object // gets serialized into step-0 grooming
|
|
34
|
+
* expected_markers: {
|
|
35
|
+
* [step_name]: string[] // patterns (case-insensitive substrings) that should appear in that step's output
|
|
36
|
+
* }
|
|
37
|
+
* }
|
|
38
|
+
*/
|
|
39
|
+
const SYNTHETIC_FIXTURES = Object.freeze({
|
|
40
|
+
'fix-with-regression-policy': {
|
|
41
|
+
name: 'fix-with-regression-policy',
|
|
42
|
+
description: 'Fix story with fix_for set — Step 5 must apply E24-A regression policy + capture learning',
|
|
43
|
+
story_metadata: {
|
|
44
|
+
story_id: 'SYN-001',
|
|
45
|
+
story_type: 'bug_fix',
|
|
46
|
+
fix_for: 'SYN-prior-001',
|
|
47
|
+
files_touched: ['src/handler.js', 'tests/regression/SYN-001.test.js'],
|
|
48
|
+
},
|
|
49
|
+
// HS-002: structural assertions (was: substring 'regression' / 'learnings')
|
|
50
|
+
// Empirically verified against real shipped step-5-review.md files:
|
|
51
|
+
// - "regression: 7/7" / "5/5 pass" count pattern (proves TDD-Green)
|
|
52
|
+
// - "1 enterprise-candidate (...)" bullet (proves Capture Learnings)
|
|
53
|
+
// Prose-only "we wrote regression tests" without count/structure → REJECTED.
|
|
54
|
+
expected_markers: {
|
|
55
|
+
'step-5-review': [
|
|
56
|
+
'(regression|tests?)\\b.*\\d+\\s*\\/\\s*\\d+|\\d+\\s*\\/\\s*\\d+.*(regression|tests?)\\b',
|
|
57
|
+
'\\b\\d+\\s+enterprise[-\\s]candidate',
|
|
58
|
+
],
|
|
59
|
+
'step-1-plan': [
|
|
60
|
+
'tests/regression', // explicit path, not just word "regression"
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
'feature-touching-source': {
|
|
65
|
+
name: 'feature-touching-source',
|
|
66
|
+
description: 'Feature story touching source — Step 5 should not require regression test BUT must still capture learnings',
|
|
67
|
+
story_metadata: {
|
|
68
|
+
story_id: 'SYN-002',
|
|
69
|
+
story_type: 'feature_add',
|
|
70
|
+
files_touched: ['src/feature.js', 'tests/regression/SYN-002.test.js'],
|
|
71
|
+
},
|
|
72
|
+
// HS-002: structural assertion (was: substring 'learnings').
|
|
73
|
+
// Real convention: "N enterprise-candidate (X-LN: title)" bullet.
|
|
74
|
+
expected_markers: {
|
|
75
|
+
'step-5-review': [
|
|
76
|
+
'\\b\\d+\\s+enterprise[-\\s]candidate',
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
'pure-config-change': {
|
|
81
|
+
name: 'pure-config-change',
|
|
82
|
+
description: 'Pure declarative metadata story — Step 3a should tag scenario as CONFIG (not BROWSER/API)',
|
|
83
|
+
story_metadata: {
|
|
84
|
+
story_id: 'SYN-003',
|
|
85
|
+
story_type: 'feature_add',
|
|
86
|
+
files_touched: ['force-app/main/default/profiles/Admin.profile-meta.xml'],
|
|
87
|
+
},
|
|
88
|
+
// HS-002: structural assertions (was: bare 'CONFIG' + 'sandbox|deploy').
|
|
89
|
+
// CONFIG must appear in a tagging context (table/list/category), not prose.
|
|
90
|
+
// Strategy must co-locate sandbox-or-metadata with deploy/smoke/round-trip.
|
|
91
|
+
expected_markers: {
|
|
92
|
+
'step-3-e2e-plan': [
|
|
93
|
+
'(tag|type|category|classify|scenario|test.type).{0,40}\\bCONFIG\\b|\\bCONFIG\\b.{0,40}(tag|type|category|test.type)',
|
|
94
|
+
'(sandbox|metadata\\s*API).{0,80}(deploy|smoke|round.?trip|verify)|(deploy|smoke).{0,80}(sandbox|metadata)',
|
|
95
|
+
],
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
'agent-prompt-edit-needs-step-5c': {
|
|
99
|
+
name: 'agent-prompt-edit-needs-step-5c',
|
|
100
|
+
description: 'Story editing agent prompt — Step 5c CI Gate Verification must be invoked after PR creation',
|
|
101
|
+
story_metadata: {
|
|
102
|
+
story_id: 'SYN-004',
|
|
103
|
+
story_type: 'feature_add',
|
|
104
|
+
files_touched: ['scripts/seed-agent-prompts.js'],
|
|
105
|
+
},
|
|
106
|
+
// HS-004 RESOLUTION (chicken-and-egg fixed):
|
|
107
|
+
// Per the agent prompt at scripts/seed-agent-prompts.js:775, Step 5c
|
|
108
|
+
// writes its gate matrix to `.github/pipeline/<STORY-ID>/step-5c-ci.md`
|
|
109
|
+
// — a SEPARATE artifact, not step-5-review.md. The previous fixture
|
|
110
|
+
// (HS-001) asserted against the wrong file (step-5-review.md), which
|
|
111
|
+
// is why no shipped artifact ever satisfied it. Per architect plan #121,
|
|
112
|
+
// HS-004: convention establishment + backfill before re-asserting.
|
|
113
|
+
//
|
|
114
|
+
// Backfill: H-076's `.github/pipeline/H-076/step-5c-ci.md` was created
|
|
115
|
+
// (the story that introduced Step 5c per its own convention).
|
|
116
|
+
//
|
|
117
|
+
// Markers (all 3 required, structural):
|
|
118
|
+
// 1. `## Gate matrix` heading (anchored, line-start via /m flag) —
|
|
119
|
+
// the canonical artifact section per the agent-prompt template
|
|
120
|
+
// 2. `gh pr checks` polling command must be referenced
|
|
121
|
+
// 3. Concrete gate result in either word order
|
|
122
|
+
expected_markers: {
|
|
123
|
+
'step-5c-ci': [
|
|
124
|
+
'^##\\s+Gate matrix', // Canonical artifact heading (anchored via /m flag)
|
|
125
|
+
'gh pr checks', // Polling command must be referenced
|
|
126
|
+
// Concrete gate result, accepting either word order:
|
|
127
|
+
// "- gate: PASS" / "Regression suite | ✅ PASS" — gate-first
|
|
128
|
+
// "- PASS gates" — result-first (artificial but valid)
|
|
129
|
+
'(PASS|FAIL)\\b.*\\b(gate|gates|check|checks)\\b|\\b(gate|gates|check|checks)\\b.*(PASS|FAIL)\\b',
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
// SA-001 / #137 (architect plan, story 1/2): synthetic story whose implementation
|
|
134
|
+
// deliberately violates a skill assertion. Used by the Step 5b runtime
|
|
135
|
+
// harness regression once Story 2 wires the runner. Today: Story 1 ships
|
|
136
|
+
// the engine only; this fixture is the contract that Story 2 will satisfy.
|
|
137
|
+
//
|
|
138
|
+
// Assertion shape (versioned `version: 1`):
|
|
139
|
+
// - id: NO-CONSOLE-LOG-IN-SRC
|
|
140
|
+
// applies_to: "^src/"
|
|
141
|
+
// forbid_added: "console\\.log"
|
|
142
|
+
//
|
|
143
|
+
// The story metadata's `referenced_skill` lets the runner scope which
|
|
144
|
+
// skill's assertions to evaluate (LC-001 cross-reference field).
|
|
145
|
+
'story-that-violates-skill': {
|
|
146
|
+
name: 'story-that-violates-skill',
|
|
147
|
+
description: 'Story that violates a referenced skill — Step 5b must emit a BLOCKER finding tied to the skill+assertion id',
|
|
148
|
+
story_metadata: {
|
|
149
|
+
story_id: 'SYN-005',
|
|
150
|
+
story_type: 'feature_add',
|
|
151
|
+
files_touched: ['src/handler.js'],
|
|
152
|
+
referenced_skill: 'error-handling',
|
|
153
|
+
},
|
|
154
|
+
// Markers (all 3 required, structural) — what Story 2's
|
|
155
|
+
// step-5b-skill-audit.md MUST contain when this fixture is run:
|
|
156
|
+
// 1. `## Skill audit` heading (the canonical artifact section)
|
|
157
|
+
// 2. The skill id that was scanned, paired with the assertion id
|
|
158
|
+
// 3. A concrete finding block with severity + matched file path
|
|
159
|
+
expected_markers: {
|
|
160
|
+
'step-5b-skill-audit': [
|
|
161
|
+
'^##\\s+Skill audit', // Canonical artifact heading
|
|
162
|
+
'\\berror-handling\\b.*\\bNO-CONSOLE-LOG-IN-SRC\\b|\\bNO-CONSOLE-LOG-IN-SRC\\b.*\\berror-handling\\b',
|
|
163
|
+
'(BLOCKER|WARN|INFO)\\b.*\\bsrc/handler\\.js\\b',
|
|
164
|
+
],
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Generate a fixture for a named scenario. Returns the synthetic story +
|
|
171
|
+
* the expected-markers manifest the verifier will check.
|
|
172
|
+
*
|
|
173
|
+
* RR-003 (architect plan #117 story 3/4): when opts.includeOverlays is true
|
|
174
|
+
* and rule-resolver inputs are provided, adopter overlay rule_ids are added
|
|
175
|
+
* as additional markers in step-5-review (where the agent reports rules
|
|
176
|
+
* applied). Default behavior unchanged — backward compat for all existing
|
|
177
|
+
* callers.
|
|
178
|
+
*
|
|
179
|
+
* @param {string} scenarioName - key from SYNTHETIC_FIXTURES
|
|
180
|
+
* @param {object} [opts]
|
|
181
|
+
* @param {boolean} [opts.includeOverlays=false] - if true, add adopter overlay
|
|
182
|
+
* rule_ids to the fixture's expected_markers (requires repoRoot + fileExists
|
|
183
|
+
* + readFile + overlayPaths)
|
|
184
|
+
* @param {string} [opts.repoRoot]
|
|
185
|
+
* @param {(p:string)=>boolean} [opts.fileExists]
|
|
186
|
+
* @param {(p:string)=>string|null} [opts.readFile]
|
|
187
|
+
* @param {string[]} [opts.overlayPaths]
|
|
188
|
+
* @returns {{ story: object, expectedMarkers: object, description: string,
|
|
189
|
+
* adopterRulesIncluded?: string[] } | null}
|
|
190
|
+
*/
|
|
191
|
+
function generateFixture(scenarioName, opts = {}) {
|
|
192
|
+
const fixture = SYNTHETIC_FIXTURES[scenarioName];
|
|
193
|
+
if (!fixture) return null;
|
|
194
|
+
|
|
195
|
+
// Deep-copy expected_markers so the caller can't mutate the frozen table
|
|
196
|
+
const expectedMarkers = {};
|
|
197
|
+
for (const [step, markers] of Object.entries(fixture.expected_markers)) {
|
|
198
|
+
expectedMarkers[step] = [...markers];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const result = {
|
|
202
|
+
story: { ...fixture.story_metadata },
|
|
203
|
+
expectedMarkers,
|
|
204
|
+
description: fixture.description,
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// RR-003: extend with adopter overlay rule_ids if requested
|
|
208
|
+
if (opts.includeOverlays && typeof opts.fileExists === 'function' && typeof opts.readFile === 'function') {
|
|
209
|
+
try {
|
|
210
|
+
const { resolveRules } = require('./rule-resolver');
|
|
211
|
+
const resolved = resolveRules({
|
|
212
|
+
repoRoot: opts.repoRoot,
|
|
213
|
+
fileExists: opts.fileExists,
|
|
214
|
+
readFile: opts.readFile,
|
|
215
|
+
overlayPaths: Array.isArray(opts.overlayPaths) ? opts.overlayPaths : [],
|
|
216
|
+
});
|
|
217
|
+
const adopterRuleIds = resolved.overlays.map((o) => o.ruleId);
|
|
218
|
+
if (adopterRuleIds.length > 0) {
|
|
219
|
+
// Adopter rules should appear in step-5-review when the agent reports
|
|
220
|
+
// which rules it applied. Each rule_id becomes a substring marker.
|
|
221
|
+
const stepKey = 'step-5-review';
|
|
222
|
+
if (!expectedMarkers[stepKey]) expectedMarkers[stepKey] = [];
|
|
223
|
+
for (const ruleId of adopterRuleIds) {
|
|
224
|
+
// Don't duplicate if already present
|
|
225
|
+
if (!expectedMarkers[stepKey].includes(ruleId)) {
|
|
226
|
+
expectedMarkers[stepKey].push(ruleId);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
result.adopterRulesIncluded = adopterRuleIds;
|
|
230
|
+
} else {
|
|
231
|
+
result.adopterRulesIncluded = [];
|
|
232
|
+
}
|
|
233
|
+
} catch {
|
|
234
|
+
// Defensive — resolveRules failure → silent fallback to canonical fixture
|
|
235
|
+
result.adopterRulesIncluded = [];
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return result;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Verify that captured outputs contain all expected markers.
|
|
244
|
+
*
|
|
245
|
+
* @param {object} opts
|
|
246
|
+
* @param {object} opts.capturedOutputs - map: step_name → captured text
|
|
247
|
+
* @param {object} opts.expectedMarkers - map: step_name → array of regex/substring patterns
|
|
248
|
+
* @returns {{ passed: boolean, results: Array<{step, marker, found, capturedSample}> }}
|
|
249
|
+
*/
|
|
250
|
+
function verifyMarkers(opts = {}) {
|
|
251
|
+
const { capturedOutputs = {}, expectedMarkers = {} } = opts;
|
|
252
|
+
const results = [];
|
|
253
|
+
|
|
254
|
+
for (const [step, markers] of Object.entries(expectedMarkers)) {
|
|
255
|
+
const captured = capturedOutputs[step];
|
|
256
|
+
if (typeof captured !== 'string') {
|
|
257
|
+
// No output captured for this step — every marker counts as not found
|
|
258
|
+
for (const marker of markers) {
|
|
259
|
+
results.push({
|
|
260
|
+
step,
|
|
261
|
+
marker,
|
|
262
|
+
found: false,
|
|
263
|
+
reason: `no captured output for step "${step}"`,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
for (const marker of markers) {
|
|
269
|
+
// marker may be a regex string (with | alternation) or a literal substring.
|
|
270
|
+
// Flags: i = case-insensitive; m = ^/$ anchor to line-start/line-end (multiline).
|
|
271
|
+
// The /m flag is REQUIRED for markers like `^##\s+Step 5c` to match a heading
|
|
272
|
+
// that appears on a non-first line of a multiline file. Without /m, ^ anchors
|
|
273
|
+
// to string-start, which only matches if the heading starts at byte 0.
|
|
274
|
+
// Bug fix in PR following PR #120 (which forgot the /m flag).
|
|
275
|
+
let found;
|
|
276
|
+
try {
|
|
277
|
+
const re = new RegExp(marker, 'im');
|
|
278
|
+
found = re.test(captured);
|
|
279
|
+
} catch {
|
|
280
|
+
found = captured.toLowerCase().includes(marker.toLowerCase());
|
|
281
|
+
}
|
|
282
|
+
results.push({
|
|
283
|
+
step,
|
|
284
|
+
marker,
|
|
285
|
+
found,
|
|
286
|
+
capturedSample: found ? '(present)' : captured.slice(0, 100) + '...',
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const passed = results.every((r) => r.found);
|
|
292
|
+
return { passed, results };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
module.exports = {
|
|
296
|
+
SYNTHETIC_FIXTURES,
|
|
297
|
+
generateFixture,
|
|
298
|
+
verifyMarkers,
|
|
299
|
+
};
|