devflow-kit 3.0.1 → 3.2.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 +49 -0
- package/README.md +1 -1
- package/dist/agents/git.md +2 -2
- package/dist/cli/agents-view/index.js +1 -1
- package/dist/cli/agents-view/render.js +71 -17
- package/dist/cli/agents-view/state.js +42 -16
- package/dist/cli/agents-view/terminal.js +5 -5
- package/dist/cli/commands/agents.js +142 -51
- package/dist/cli/commands/ambient.js +1 -1
- package/dist/cli/commands/attribution-prompts.js +8 -8
- package/dist/cli/commands/capture.js +1 -1
- package/dist/cli/commands/compliance-prompts.js +8 -8
- package/dist/cli/commands/compliance.js +8 -7
- package/dist/cli/commands/flags.js +33 -31
- package/dist/cli/commands/hud.js +1 -1
- package/dist/cli/commands/init-seed.js +9 -9
- package/dist/cli/commands/init.js +162 -85
- package/dist/cli/commands/install-report.js +10 -10
- package/dist/cli/commands/learning.js +302 -136
- package/dist/cli/commands/memory.js +36 -15
- package/dist/cli/commands/proxy.js +23 -23
- package/dist/cli/commands/rules.js +6 -5
- package/dist/cli/commands/tracker-prompts.js +6 -6
- package/dist/cli/commands/tracker.js +9 -9
- package/dist/cli/commands/uninstall.js +183 -59
- package/dist/cli/flags-view/render.js +5 -5
- package/dist/cli/flags-view/state.js +9 -9
- package/dist/cli/flags-view/terminal.js +4 -4
- package/dist/cli/tui/cells.js +1 -1
- package/dist/cli/tui/terminal.js +6 -6
- package/dist/commands/code-review.md +0 -2
- package/dist/commands/debug.md +14 -11
- package/dist/commands/dynamic-build.md +51 -47
- package/dist/commands/dynamic-plan.md +27 -7
- package/dist/commands/dynamic-profile.md +17 -3
- package/dist/commands/dynamic-tickets.md +18 -4
- package/dist/commands/explore.md +9 -3
- package/dist/commands/implement.md +20 -16
- package/dist/commands/plan.md +13 -9
- package/dist/commands/release.md +23 -3
- package/dist/commands/research.md +9 -3
- package/dist/commands/resolve.md +9 -12
- package/dist/commands/self-review.md +0 -2
- package/dist/core/agent-frontmatter.js +28 -3
- package/dist/core/agent-models.js +204 -42
- package/dist/core/agent-state.js +28 -6
- package/dist/core/ansi.js +2 -2
- package/dist/core/assets.js +1 -1
- package/dist/core/cache.js +7 -8
- package/dist/core/codex-auth-inspect.js +4 -4
- package/dist/core/compliance-compose.js +3 -3
- package/dist/core/compliance.js +3 -4
- package/dist/core/evidence-policy.js +14 -13
- package/dist/core/external-models.js +1 -1
- package/dist/core/feature-config.js +71 -13
- package/dist/core/feature-switch.js +3 -3
- package/dist/core/flags.js +49 -25
- package/dist/core/fs-atomic.js +6 -7
- package/dist/core/learning-queue-cleanup.js +16 -81
- package/dist/core/learning-store.js +61 -0
- package/dist/core/linked-path.js +46 -0
- package/dist/core/manifest.js +5 -5
- package/dist/core/mds-variants.js +13 -13
- package/dist/core/model-discovery.js +8 -8
- package/dist/core/observations.js +17 -101
- package/dist/core/orphan-sweep.js +4 -4
- package/dist/core/plugins.js +13 -8
- package/dist/core/project-paths.js +9 -13
- package/dist/core/proxy-log.js +8 -8
- package/dist/core/proxy-state.js +3 -3
- package/dist/core/queue-drain.js +31 -0
- package/dist/core/reference-sweep.js +6 -6
- package/dist/core/teammate-mode-cleanup.js +1 -1
- package/dist/core/tracker.js +14 -14
- package/dist/hud/colors.js +2 -2
- package/dist/hud/components/learning-counts.js +54 -22
- package/dist/hud/components/version-badge.js +1 -1
- package/dist/skills/git/references/pr/resolve-review-threads.md +2 -2
- package/dist/skills/git/references/tracker/github/create-release.md +2 -2
- package/dist/skills/git/references/tracker/jira/create-release.md +2 -2
- package/dist/skills/git/references/tracker/linear/create-release.md +2 -2
- package/dist/targets/claude-code/compliance-install.js +17 -15
- package/dist/targets/claude-code/hooks.js +2 -2
- package/dist/targets/claude-code/installer.js +59 -32
- package/dist/targets/claude-code/legacy.js +1 -1
- package/dist/targets/claude-code/post-install.js +135 -45
- package/dist/targets/claude-code/tracker-install.js +2 -2
- package/package.json +1 -1
- package/src/assets/agents/code.md +15 -21
- package/src/assets/agents/design.md +4 -2
- package/src/assets/agents/diagnose.md +3 -1
- package/src/assets/agents/evaluate.md +4 -0
- package/src/assets/agents/git.mds +2 -2
- package/src/assets/agents/knowledge.md +5 -3
- package/src/assets/agents/learning.md +281 -196
- package/src/assets/agents/research.md +3 -1
- package/src/assets/agents/review.md +5 -3
- package/src/assets/agents/scrutinize.md +5 -1
- package/src/assets/agents/simplify.md +4 -0
- package/src/assets/agents/skim.md +4 -2
- package/src/assets/agents/synthesize.md +6 -0
- package/src/assets/agents/test.md +18 -10
- package/src/assets/agents/triage.md +11 -9
- package/src/assets/agents/validate.md +14 -10
- package/src/assets/commands/_partials/_decisions.mds +8 -3
- package/src/assets/commands/_partials/_docs_root.mds +3 -3
- package/src/assets/commands/_partials/_engine.mds +16 -32
- package/src/assets/commands/_partials/_knowledge.mds +0 -2
- package/src/assets/commands/_partials/_preamble.mds +6 -2
- package/src/assets/commands/_partials/_settings.mds +2 -2
- package/src/assets/commands/_partials/_tracker.mds +1 -1
- package/src/assets/commands/code-review.mds +0 -2
- package/src/assets/commands/debug.mds +13 -8
- package/src/assets/commands/dynamic-build.mds +18 -12
- package/src/assets/commands/dynamic-plan.mds +10 -4
- package/src/assets/commands/dynamic-profile.mds +1 -1
- package/src/assets/commands/dynamic-tickets.mds +2 -2
- package/src/assets/commands/explore.mds +9 -1
- package/src/assets/commands/implement.mds +19 -13
- package/src/assets/commands/plan.mds +12 -8
- package/src/assets/commands/release.md +23 -3
- package/src/assets/commands/research.mds +9 -3
- package/src/assets/commands/resolve.mds +9 -10
- package/src/assets/mds/git/_pr.mds +3 -3
- package/src/assets/mds/tracker/_common.mds +1 -1
- package/src/assets/mds/tracker/_github.mds +3 -3
- package/src/assets/mds/tracker/_jira.mds +3 -3
- package/src/assets/mds/tracker/_linear.mds +3 -3
- package/src/assets/mds/tracker/_mcp.mds +6 -5
- package/src/assets/scripts/hooks/assets/orchestrator-charter.md +4 -2
- package/src/assets/scripts/hooks/background-memory-update +97 -33
- package/src/assets/scripts/hooks/capture-prompt +4 -3
- package/src/assets/scripts/hooks/capture-question +4 -3
- package/src/assets/scripts/hooks/capture-turn +5 -20
- package/src/assets/scripts/hooks/ensure-devflow-init +14 -2
- package/src/assets/scripts/hooks/ensure-proxy +5 -6
- package/src/assets/scripts/hooks/ensure-root-gitignore +123 -11
- package/src/assets/scripts/hooks/git-marker +71 -0
- package/src/assets/scripts/hooks/is-hex-sha +1 -1
- package/src/assets/scripts/hooks/json-helper.cjs +345 -944
- package/src/assets/scripts/hooks/json-parse +25 -129
- package/src/assets/scripts/hooks/lib/decisions-format.cjs +205 -156
- package/src/assets/scripts/hooks/lib/learning-store.cjs +3207 -0
- package/src/assets/scripts/hooks/lib/mkdir-lock.cjs +7 -5
- package/src/assets/scripts/hooks/lib/project-paths.cjs +13 -19
- package/src/assets/scripts/hooks/lib/render-decisions.cjs +253 -226
- package/src/assets/scripts/hooks/memory-worker +10 -0
- package/src/assets/scripts/hooks/pre-compact-memory +66 -14
- package/src/assets/scripts/hooks/preamble +9 -1
- package/src/assets/scripts/hooks/queue-append +55 -23
- package/src/assets/scripts/hooks/resolve-project-root +3 -4
- package/src/assets/scripts/hooks/session-start-context +146 -45
- package/src/assets/scripts/hooks/session-start-memory +33 -11
- package/src/assets/scripts/lib/project-config.cjs +2 -2
- package/src/assets/scripts/pr-evidence.cjs +3 -3
- package/src/assets/scripts/redact-secrets.cjs +20 -20
- package/src/assets/scripts/release-trace.cjs +1 -1
- package/src/assets/scripts/resolve-evidence-policy.cjs +3 -3
- package/src/assets/scripts/resolve-settings.cjs +3 -3
- package/src/assets/scripts/verify-evidence.cjs +2 -2
- package/src/assets/skills/apply-decisions/SKILL.md +37 -17
- package/src/assets/skills/docs-framework/SKILL.md +2 -2
- package/src/assets/skills/feature-knowledge/SKILL.md +6 -5
- package/src/assets/skills/test-driven-development/SKILL.md +6 -4
- package/dist/core/observation-io.js +0 -50
- package/src/assets/scripts/hooks/decisions-usage-scan.cjs +0 -131
|
@@ -2,14 +2,16 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Shared pure formatting helpers for decisions.md and pitfalls.md output.
|
|
4
4
|
//
|
|
5
|
-
// DESIGN: Shared pure formatting helpers
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
5
|
+
// DESIGN: Shared pure formatting helpers for render-decisions.cjs, the one
|
|
6
|
+
// renderer: its CLI and every learning-store writer render through it, so all of
|
|
7
|
+
// them share the EXACT same format functions. This is the single source of truth
|
|
8
|
+
// for the byte-compat output strings — any drift here will break the
|
|
9
|
+
// renderer/session-start-context TL;DR parser.
|
|
9
10
|
//
|
|
10
11
|
// BYTE-COMPAT CONTRACT (must not change without updating all consumers):
|
|
12
|
+
// v1 entries — ledger rows without schema 2 — keep these bytes (D-V1-BYTE-STABLE):
|
|
11
13
|
// Decision heading: \n## {anchorId}: {title}\n
|
|
12
|
-
// Decision fields: - **Date**: YYYY-MM-DD\n (empty string when absent — render purity
|
|
14
|
+
// Decision fields: - **Date**: YYYY-MM-DD\n (empty string when absent — render purity: never clock-read in a formatter)
|
|
13
15
|
// - **Status**: Accepted\n
|
|
14
16
|
// - **Context**: ...\n
|
|
15
17
|
// - **Decision**: ...\n
|
|
@@ -24,10 +26,22 @@
|
|
|
24
26
|
// - **Status**: Active\n
|
|
25
27
|
// - **Source**: self-learning:{obsId}\n
|
|
26
28
|
// - **Amendments**: text1; text2\n (omitted when absent or empty)
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
29
|
+
// v2 entries — rows with schema 2 — render from their fields (formatEntryBodyV2):
|
|
30
|
+
// \n## {anchorId}: {title}\n\n
|
|
31
|
+
// - **Status**: {Accepted|Active}\n (+ " · verified {last_verified}" when the row has one)
|
|
32
|
+
// - **Scope**: `{scope1}`, `{scope2}`\n
|
|
33
|
+
// - **{Decision|Rule}**: {rule}\n (Decision for decisions, Rule for pitfalls)
|
|
34
|
+
// - **Why**: {why}\n
|
|
35
|
+
// - **Source**: {provenance}\n
|
|
36
|
+
// Inactive table, after the active bodies and omitted when no entry is inactive:
|
|
37
|
+
// \n## Inactive\n\n| ID | Status | Note |\n|---|---|---|\n then "| {anchorId} | {status} | {note} |\n" per entry
|
|
38
|
+
// TL;DR line: <!-- TL;DR: N {decisions|pitfalls} --> (N = the file's active entries)
|
|
39
|
+
// File headers (the renderer swaps in the real TL;DR line):
|
|
40
|
+
// decisions.md: "<!-- TL;DR: 0 decisions -->\n# Architectural Decisions\n\n{GENERATED_NOTICE}\n"
|
|
41
|
+
// pitfalls.md: "<!-- TL;DR: 0 pitfalls -->\n# Known Pitfalls\n\n{GENERATED_NOTICE}\n"
|
|
42
|
+
// Index lines:
|
|
43
|
+
// v1: " {anchorId} {title cut to 60} [{status}]" (+ " — {area cut to 80}" when it has one)
|
|
44
|
+
// v2: " {anchorId} {title}" (+ " — {scope joined ', ' cut to 80}" when it has one)
|
|
31
45
|
//
|
|
32
46
|
// Field parsing: both formatters use segmentDetails() which splits on ';' and
|
|
33
47
|
// anchors key detection to the START of each trimmed segment — so 'reissue:'
|
|
@@ -36,31 +50,40 @@
|
|
|
36
50
|
// Recovery pass: for any key the anchored pass left unset, an unanchored
|
|
37
51
|
// regex ('(?:^|[.;\\s])key:\\s*([^;]+)') is tried against the full details
|
|
38
52
|
// string — handles legacy corpus rows written before the ';'-delimited grammar
|
|
39
|
-
// was documented, where fields are separated by '. ' rather than ';'
|
|
40
|
-
//
|
|
53
|
+
// was documented, where fields are separated by '. ' rather than ';'.
|
|
54
|
+
// The recovery pass never overrides an anchored match.
|
|
41
55
|
// LineTerminators (\r, \n, \u2028, \u2029) in field values are collapsed to a
|
|
42
56
|
// single space at all five collapse sites (segmentDetails ×2, amendmentToString
|
|
43
57
|
// ×3) — guards the single-line field contract against the full JS LineTerminator
|
|
44
|
-
// set, not just \n.
|
|
58
|
+
// set, not just \n. A v2 field is structured, never parsed: each run of control
|
|
59
|
+
// characters in it collapses to one space (the store's singleLine), so no field
|
|
60
|
+
// value can add a line, a heading or a table row.
|
|
45
61
|
//
|
|
46
62
|
// Index extraction: extractEntryFromBlock uses line-anchored regexes
|
|
47
63
|
// (/^- \*\*Status\*\*:/m, /^- \*\*Area\*\*:/m) to guard against amendment
|
|
48
|
-
// text that accidentally contains those patterns as substrings.
|
|
64
|
+
// text that accidentally contains those patterns as substrings. It reads v1
|
|
65
|
+
// blocks only; a v2 index line is built from the row's fields.
|
|
49
66
|
//
|
|
50
|
-
// Amendments shape:
|
|
51
|
-
// { date, note } objects
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
// which would emit `[object Object]` for the schema-declared shape.
|
|
67
|
+
// Amendments shape: a v1 row's `amendments` array accepts BOTH the
|
|
68
|
+
// { date, note } objects the v1 corpus holds (rendered as `[date] note`) and
|
|
69
|
+
// pre-rendered strings. formatAmendmentsLine normalises per entry — never a
|
|
70
|
+
// bare join, which would emit `[object Object]` for the object shape.
|
|
55
71
|
//
|
|
56
72
|
// Consumers of these strings:
|
|
57
|
-
// - session-start-context (
|
|
73
|
+
// - session-start-context (Section 1): injects the TL;DR comment's text via sed
|
|
58
74
|
// - devflow:apply-decisions: reads ## ADR-NNN: / ## PF-NNN: headings
|
|
59
|
-
// -
|
|
60
|
-
// - buildIndexContent (below): parses ## heading, - **Status**:, - **Area**: lines from rendered blocks
|
|
75
|
+
// - buildIndexContent (below): parses ## heading, - **Status**:, - **Area**: lines from rendered v1 blocks
|
|
61
76
|
|
|
62
77
|
'use strict';
|
|
63
78
|
|
|
79
|
+
const {
|
|
80
|
+
ACTIVE_STATUSES,
|
|
81
|
+
activeStatusFor,
|
|
82
|
+
isV2,
|
|
83
|
+
singleLine,
|
|
84
|
+
inactiveNote,
|
|
85
|
+
} = require('./learning-store.cjs');
|
|
86
|
+
|
|
64
87
|
/** JS LineTerminator set — /m `^` matches after each of these and `.` excludes them. */
|
|
65
88
|
const LINE_TERMINATORS = /[\r\n\u2028\u2029]/g;
|
|
66
89
|
|
|
@@ -89,12 +112,11 @@ const LINE_TERMINATORS = /[\r\n\u2028\u2029]/g;
|
|
|
89
112
|
* that legacy corpus rows written before the ';'-delimited grammar was
|
|
90
113
|
* documented (which embed field keys mid-segment after '. ') are still
|
|
91
114
|
* parsed correctly. The recovery pass never overrides a value the anchored
|
|
92
|
-
* pass already set.
|
|
93
|
-
* written under the old contract that embedded keys after '. ').
|
|
115
|
+
* pass already set.
|
|
94
116
|
*
|
|
95
117
|
* D002 (details-parsing): This is the SINGLE parser for structured details
|
|
96
118
|
* strings — both formatDecisionBody and formatPitfallBody delegate here.
|
|
97
|
-
*
|
|
119
|
+
* A per-field delimiter regex would silently truncate any value containing ';'.
|
|
98
120
|
*
|
|
99
121
|
* @param {string} detailsStr - raw details string from an observation row
|
|
100
122
|
* @param {readonly string[]} keys - recognised field names
|
|
@@ -138,7 +160,7 @@ function segmentDetails(detailsStr, keys) {
|
|
|
138
160
|
// ';'. The unanchored regex requires the key to be preceded by a
|
|
139
161
|
// word-boundary character (^, '.', ';', or whitespace) so that 'reissue:'
|
|
140
162
|
// still does NOT match 'issue:', and it only fills keys the anchored pass
|
|
141
|
-
// left unset — never overrides an anchored match.
|
|
163
|
+
// left unset — never overrides an anchored match.
|
|
142
164
|
for (const key of keys) {
|
|
143
165
|
if (result[key] !== undefined) continue;
|
|
144
166
|
const m = detailsStr.match(new RegExp('(?:^|[.;\\s])' + key + ':\\s*([^;]+)', 'i'));
|
|
@@ -151,11 +173,9 @@ function segmentDetails(detailsStr, keys) {
|
|
|
151
173
|
/**
|
|
152
174
|
* Normalise one amendment entry to its rendered string form.
|
|
153
175
|
*
|
|
154
|
-
* TWO SHAPES are accepted
|
|
155
|
-
* - `{ date, note }` — the shape
|
|
156
|
-
*
|
|
157
|
-
* isLearningObservation type guard accepts. Renders as `[date] note`
|
|
158
|
-
* (bare `note` when date is absent/blank).
|
|
176
|
+
* TWO SHAPES are accepted:
|
|
177
|
+
* - `{ date, note }` — the shape every amendment in the v1 corpus has.
|
|
178
|
+
* Renders as `[date] note` (bare `note` when date is absent/blank).
|
|
159
179
|
* - `string` — a pre-rendered `[date] note` line, the convenience form.
|
|
160
180
|
*
|
|
161
181
|
* A plain `join` over the object shape would emit `[object Object]`, so the
|
|
@@ -198,44 +218,29 @@ function formatAmendmentsLine(amendments) {
|
|
|
198
218
|
return `- **Amendments**: ${parts.join('; ')}\n`;
|
|
199
219
|
}
|
|
200
220
|
|
|
201
|
-
/**
|
|
202
|
-
* Guard against raw_body payloads that could forge a second entry heading or
|
|
203
|
-
* claim a different anchor ID. Accepts only a string whose `^## (ADR|PF)-\d+:`
|
|
204
|
-
* headings number exactly one AND match `## ${anchorId}:`.
|
|
205
|
-
*
|
|
206
|
-
* A rejected raw_body is DROPPED from the row — the entry then renders through
|
|
207
|
-
* the sanitised formatDecisionBody/formatPitfallBody — the sanctioned fallback when raw_body is absent or rejected (ADR-022).
|
|
208
|
-
*
|
|
209
|
-
* Per PF-023: validate at the sink so all callers (assign-anchor, refresh-anchor,
|
|
210
|
-
* any future op) inherit the guard without repeating it.
|
|
211
|
-
*
|
|
212
|
-
* @param {unknown} body
|
|
213
|
-
* @param {string} anchorId - e.g. 'ADR-001' or 'PF-023'
|
|
214
|
-
* @returns {boolean}
|
|
215
|
-
*/
|
|
216
|
-
function isSafeRawBody(body, anchorId) {
|
|
217
|
-
if (typeof body !== 'string') return false;
|
|
218
|
-
const headings = body.match(/^## (?:ADR|PF)-\d+:/gm) || [];
|
|
219
|
-
return headings.length === 1 && headings[0] === `## ${anchorId}:`;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
221
|
/** Recognised field keys for decision entries. */
|
|
223
222
|
const ADR_KEYS = /** @type {const} */ (['context', 'decision', 'rationale']);
|
|
224
223
|
|
|
225
224
|
/** Recognised field keys for pitfall entries. */
|
|
226
225
|
const PF_KEYS = /** @type {const} */ (['area', 'issue', 'impact', 'resolution']);
|
|
227
226
|
|
|
227
|
+
/** The line under each rendered file's title: who writes the file and what it lists. */
|
|
228
|
+
const GENERATED_NOTICE =
|
|
229
|
+
'Generated from the local learning ledger by devflow; do not edit. ' +
|
|
230
|
+
'Active entries follow; retired ones are listed under Inactive.';
|
|
231
|
+
|
|
228
232
|
/**
|
|
229
|
-
* Return the
|
|
230
|
-
*
|
|
233
|
+
* Return the header of a rendered decisions or pitfalls file: the zero-count
|
|
234
|
+
* TL;DR line, the title and the generated-file notice. It is the whole file of an
|
|
235
|
+
* empty corpus; the renderer swaps in the file's real TL;DR line.
|
|
231
236
|
*
|
|
232
237
|
* @param {'decision'|'pitfall'} kind
|
|
233
238
|
* @returns {string}
|
|
234
239
|
*/
|
|
235
240
|
function initDecisionsContent(kind) {
|
|
236
241
|
return kind === 'decision'
|
|
237
|
-
? '
|
|
238
|
-
: '
|
|
242
|
+
? `${buildTldrLine('decisions', [])}\n# Architectural Decisions\n\n${GENERATED_NOTICE}\n`
|
|
243
|
+
: `${buildTldrLine('pitfalls', [])}\n# Known Pitfalls\n\n${GENERATED_NOTICE}\n`;
|
|
239
244
|
}
|
|
240
245
|
|
|
241
246
|
/**
|
|
@@ -249,7 +254,7 @@ function initDecisionsContent(kind) {
|
|
|
249
254
|
function formatDecisionBody(row) {
|
|
250
255
|
const detailsStr = row.details || '';
|
|
251
256
|
const obsId = row.id || 'unknown';
|
|
252
|
-
// Render purity
|
|
257
|
+
// Render purity: never clock-read inside a formatter. Absent date
|
|
253
258
|
// renders as an empty string so the output is deterministic and idempotent.
|
|
254
259
|
const artDate = row.date || '';
|
|
255
260
|
const anchorId = row.anchor_id || '';
|
|
@@ -297,88 +302,95 @@ function formatPitfallBody(row) {
|
|
|
297
302
|
);
|
|
298
303
|
}
|
|
299
304
|
|
|
305
|
+
// ---------------------------------------------------------------------------
|
|
306
|
+
// v2 entries and the Inactive table
|
|
307
|
+
// ---------------------------------------------------------------------------
|
|
308
|
+
|
|
309
|
+
/** Per-type labels of a v2 body: the active status it shows and the name of its rule field. */
|
|
310
|
+
const V2_LABELS = Object.freeze({
|
|
311
|
+
decision: Object.freeze({ status: activeStatusFor('decision'), rule: 'Decision' }),
|
|
312
|
+
pitfall: Object.freeze({ status: activeStatusFor('pitfall'), rule: 'Rule' }),
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
/** A row field on one line: a string with each control-character run collapsed to a space, '' for anything else. */
|
|
316
|
+
function oneLine(value) {
|
|
317
|
+
return typeof value === 'string' ? singleLine(value) : '';
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** A v2 row's scope entries, each on one line; an entry that is not a non-empty string is skipped. */
|
|
321
|
+
function scopeEntries(row) {
|
|
322
|
+
return Array.isArray(row.scope)
|
|
323
|
+
? row.scope.filter(entry => typeof entry === 'string' && entry !== '').map(singleLine)
|
|
324
|
+
: [];
|
|
325
|
+
}
|
|
326
|
+
|
|
300
327
|
/**
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
* { id, type, pattern, details, anchor_id, decisions_status, date?, raw_body?, amendments? }
|
|
328
|
+
* Format the body block of a v2 entry (schema 2) from its row fields. Returns the
|
|
329
|
+
* block starting with a leading newline, like the v1 formatters.
|
|
304
330
|
*
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
*
|
|
331
|
+
* The Status line shows the active status of the entry's type — Accepted for a
|
|
332
|
+
* decision, Active for a pitfall; only active entries render — and then
|
|
333
|
+
* ` · verified {last_verified}` when the row has that date. Scope entries are code
|
|
334
|
+
* spans joined by ', ', and the rule is labelled Decision or Rule by type. A field
|
|
335
|
+
* that is not a string renders empty: a row is validated when it is written, but a
|
|
336
|
+
* hand edit can leave anything, and a formatter that runs under the lock must not
|
|
337
|
+
* throw.
|
|
308
338
|
*
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
|
|
312
|
-
|
|
339
|
+
* @param {object} row - a v2 ledger row
|
|
340
|
+
* @returns {string}
|
|
341
|
+
*/
|
|
342
|
+
function formatEntryBodyV2(row) {
|
|
343
|
+
const labels = row.type === 'pitfall' ? V2_LABELS.pitfall : V2_LABELS.decision;
|
|
344
|
+
const verified = oneLine(row.last_verified);
|
|
345
|
+
const scope = scopeEntries(row).map(entry => `\`${entry}\``).join(', ');
|
|
346
|
+
return (
|
|
347
|
+
`\n## ${oneLine(row.anchor_id)}: ${oneLine(row.title)}\n\n` +
|
|
348
|
+
`- **Status**: ${labels.status}${verified ? ` · verified ${verified}` : ''}\n` +
|
|
349
|
+
`- **Scope**: ${scope}\n` +
|
|
350
|
+
`- **${labels.rule}**: ${oneLine(row.rule)}\n` +
|
|
351
|
+
`- **Why**: ${oneLine(row.why)}\n` +
|
|
352
|
+
`- **Source**: ${oneLine(row.provenance)}\n`
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** A table cell: one line, trimmed, with `|` escaped so a value adds no column. */
|
|
357
|
+
function tableCell(value) {
|
|
358
|
+
return oneLine(value).trim().replace(/\|/g, '\\|');
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Format the Inactive table of a rendered file: one row per inactive entry, v1 or
|
|
363
|
+
* v2, in the order given (the renderer sorts them by number), or '' when there are
|
|
364
|
+
* none.
|
|
313
365
|
*
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
*
|
|
317
|
-
*
|
|
318
|
-
* single-line by construction; a newline in pattern would forge '- **Status**:'
|
|
319
|
-
* lines or second '## ADR-NNN:' headings that line-anchored index regexes match first.
|
|
320
|
-
* - raw_body: gated by isSafeRawBody — accepts only a body with exactly one heading
|
|
321
|
-
* matching anchorId; a rejected body is dropped so the entry renders through the
|
|
322
|
-
* sanitised formatDecisionBody/formatPitfallBody instead.
|
|
366
|
+
* The Note column says why the entry is inactive, as `list` does (inactiveNote):
|
|
367
|
+
* `encoded in {path}`, else `superseded by {anchor}`, else the status note, else
|
|
368
|
+
* `—`. Every cell is one line with `|` escaped as `\|`, so no value adds a line or
|
|
369
|
+
* a column. index.md never carries this table.
|
|
323
370
|
*
|
|
324
|
-
* @param {object}
|
|
325
|
-
* @
|
|
326
|
-
* @returns {object} Canonical ledger row
|
|
371
|
+
* @param {object[]} rows - inactive ledger rows
|
|
372
|
+
* @returns {string}
|
|
327
373
|
*/
|
|
328
|
-
function
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
);
|
|
335
|
-
}
|
|
336
|
-
/** @type {Record<string, unknown>} */
|
|
337
|
-
const row = {
|
|
338
|
-
id: obs.id,
|
|
339
|
-
type: obs.type,
|
|
340
|
-
// Heading is single-line by construction — collapse any LLM-injected line terminators
|
|
341
|
-
// so a newline in pattern cannot forge '- **Status**:' lines or second '## ADR-NNN:'
|
|
342
|
-
// headings inside the rendered body (those would be matched first by the line-anchored
|
|
343
|
-
// index regexes in extractEntryFromBlock). applies PF-023.
|
|
344
|
-
pattern: typeof obs.pattern === 'string' ? obs.pattern.replace(LINE_TERMINATORS, ' ').trim() : obs.pattern,
|
|
345
|
-
details: obs.details, // segmentDetails already collapses line terminators at read time
|
|
346
|
-
anchor_id: anchorId,
|
|
347
|
-
decisions_status: status,
|
|
348
|
-
};
|
|
349
|
-
// Optional fields — include only when present in the observation or explicitly provided
|
|
350
|
-
if (date !== undefined) row.date = date;
|
|
351
|
-
// log-sourced raw_body (ADR-022) — a log row that lost raw_body un-freezes the
|
|
352
|
-
// entry to formatter-rendered output by design. Gate through isSafeRawBody (PF-023).
|
|
353
|
-
if (obs.raw_body !== undefined && isSafeRawBody(obs.raw_body, anchorId)) {
|
|
354
|
-
row.raw_body = obs.raw_body;
|
|
355
|
-
}
|
|
356
|
-
if (obs.amendments !== undefined) row.amendments = obs.amendments;
|
|
357
|
-
return row;
|
|
374
|
+
function formatInactiveTable(rows) {
|
|
375
|
+
if (rows.length === 0) return '';
|
|
376
|
+
const lines = rows.map(row =>
|
|
377
|
+
`| ${tableCell(row.anchor_id)} | ${tableCell(row.decisions_status)} | ${tableCell(inactiveNote(row)) || '—'} |`
|
|
378
|
+
);
|
|
379
|
+
return `\n## Inactive\n\n| ID | Status | Note |\n|---|---|---|\n${lines.join('\n')}\n`;
|
|
358
380
|
}
|
|
359
381
|
|
|
360
382
|
/**
|
|
361
|
-
* Build the TL;DR comment line
|
|
362
|
-
*
|
|
363
|
-
*
|
|
364
|
-
*
|
|
365
|
-
* numeric anchor ascending — same order as the rendered file).
|
|
366
|
-
* When rows is empty, Key is empty string (no trailing space before -->).
|
|
383
|
+
* Build the TL;DR comment line of a rendered decisions or pitfalls file:
|
|
384
|
+
* `<!-- TL;DR: N {decisions|pitfalls} -->`, N being the active entries the file
|
|
385
|
+
* renders. It names no entry: session-start-context injects its text into every
|
|
386
|
+
* session, and the index lists the entries.
|
|
367
387
|
*
|
|
368
388
|
* @param {'decisions'|'pitfalls'} kind - label used in the comment
|
|
369
|
-
* @param {object[]} rows - active
|
|
389
|
+
* @param {object[]} rows - the file's active rows
|
|
370
390
|
* @returns {string} complete TL;DR comment line (no trailing newline)
|
|
371
391
|
*/
|
|
372
392
|
function buildTldrLine(kind, rows) {
|
|
373
|
-
|
|
374
|
-
const last5 = rows.slice(-5).map(r => r.anchor_id);
|
|
375
|
-
const keyStr = last5.join(', ');
|
|
376
|
-
// Byte-compat: an empty key list must render `Key: -->` (single space) so the
|
|
377
|
-
// empty-corpus render is byte-identical to initDecisionsContent's header. A
|
|
378
|
-
// trailing space before `-->` would diverge from the documented contract and
|
|
379
|
-
// break the assertion that the render is the SOLE format authority.
|
|
380
|
-
if (!keyStr) return `<!-- TL;DR: ${count} ${kind}. Key: -->`;
|
|
381
|
-
return `<!-- TL;DR: ${count} ${kind}. Key: ${keyStr} -->`;
|
|
393
|
+
return `<!-- TL;DR: ${rows.length} ${kind} -->`;
|
|
382
394
|
}
|
|
383
395
|
|
|
384
396
|
// ---------------------------------------------------------------------------
|
|
@@ -386,12 +398,11 @@ function buildTldrLine(kind, rows) {
|
|
|
386
398
|
// ---------------------------------------------------------------------------
|
|
387
399
|
|
|
388
400
|
/**
|
|
389
|
-
* Statuses
|
|
390
|
-
* [unknown].
|
|
391
|
-
*
|
|
392
|
-
* before writing.
|
|
401
|
+
* Statuses a v1 index line tags as they are — everything else renders as
|
|
402
|
+
* [unknown]. They are the store's active statuses: the index lists active
|
|
403
|
+
* entries only.
|
|
393
404
|
*/
|
|
394
|
-
const INDEX_KNOWN_STATUSES =
|
|
405
|
+
const INDEX_KNOWN_STATUSES = ACTIVE_STATUSES;
|
|
395
406
|
|
|
396
407
|
/**
|
|
397
408
|
* Truncate a string to maxLen characters, appending '…' if truncated.
|
|
@@ -419,14 +430,43 @@ function formatIndexEntryLine(entry) {
|
|
|
419
430
|
return ` ${entry.id} ${title} ${tag}${areaSuffix}`;
|
|
420
431
|
}
|
|
421
432
|
|
|
433
|
+
/**
|
|
434
|
+
* `text` cut to its first `maxChars` characters (code points) plus '…' when it is
|
|
435
|
+
* longer, so a cut never splits a surrogate pair.
|
|
436
|
+
*
|
|
437
|
+
* @param {string} text
|
|
438
|
+
* @param {number} maxChars
|
|
439
|
+
* @returns {string}
|
|
440
|
+
*/
|
|
441
|
+
function truncateChars(text, maxChars) {
|
|
442
|
+
const chars = Array.from(text);
|
|
443
|
+
return chars.length <= maxChars ? text : chars.slice(0, maxChars).join('') + '…';
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Format the index line of a v2 entry from its row fields: ` {anchor} {title}`
|
|
448
|
+
* and, when the entry has a scope, ` — ` and its entries joined by ', ', cut to
|
|
449
|
+
* 80 characters plus '…'. The title is whole, since a v2 title is at most 120
|
|
450
|
+
* characters, and there is no status tag: the index lists active entries only.
|
|
451
|
+
*
|
|
452
|
+
* @param {object} row - a v2 ledger row
|
|
453
|
+
* @returns {string}
|
|
454
|
+
*/
|
|
455
|
+
function formatIndexEntryLineV2(row) {
|
|
456
|
+
const scope = scopeEntries(row).join(', ');
|
|
457
|
+
const scopeSuffix = scope ? ` — ${truncateChars(scope, 80)}` : '';
|
|
458
|
+
return ` ${oneLine(row.anchor_id)} ${oneLine(row.title)}${scopeSuffix}`;
|
|
459
|
+
}
|
|
460
|
+
|
|
422
461
|
/**
|
|
423
462
|
* Build the compact index content from in-memory active ledger rows.
|
|
424
463
|
* Empty corpus (both arrays empty) → '(none)'.
|
|
425
464
|
* No trailing newline (caller adds '\n' before writing).
|
|
426
465
|
*
|
|
427
|
-
* Strategy:
|
|
428
|
-
*
|
|
429
|
-
* heading/Status/Area with the
|
|
466
|
+
* Strategy: a v2 row's line is built from its fields (formatIndexEntryLineV2).
|
|
467
|
+
* For a v1 row, obtain its rendered block (pre-rendered block when provided, else
|
|
468
|
+
* truthy raw_body || format*Body(row)), then extract heading/Status/Area with the
|
|
469
|
+
* same regexes (D-V1-BYTE-STABLE).
|
|
430
470
|
* This preserves byte-compat for migrated rows that carry Area/Status only in raw_body.
|
|
431
471
|
* Note: raw_body === "" is treated as absent (falsy); both predicates align with the
|
|
432
472
|
* truthy check in renderDecisionsFile so index and body files never drift on this edge.
|
|
@@ -463,42 +503,50 @@ function buildIndexContent(activeDecisionRows, activePitfallRows, { decisionsFil
|
|
|
463
503
|
return { id, title: rawTitle, status, area };
|
|
464
504
|
}
|
|
465
505
|
|
|
466
|
-
/**
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
506
|
+
/**
|
|
507
|
+
* The index lines of one kind's active rows, in row order. A v1 row whose block
|
|
508
|
+
* has no entry heading has no line.
|
|
509
|
+
* @param {object[]} rows
|
|
510
|
+
* @param {string[]|undefined} rowBlocks - pre-rendered blocks, one per row
|
|
511
|
+
* @param {(row: object) => string} formatV1Body
|
|
512
|
+
* @returns {string[]}
|
|
513
|
+
*/
|
|
514
|
+
function indexLines(rows, rowBlocks, formatV1Body) {
|
|
515
|
+
const lines = [];
|
|
516
|
+
for (let i = 0; i < rows.length; i++) {
|
|
517
|
+
const row = rows[i];
|
|
518
|
+
if (isV2(row)) {
|
|
519
|
+
lines.push(formatIndexEntryLineV2(row));
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
const block = rowBlocks ? rowBlocks[i] : (row.raw_body || formatV1Body(row));
|
|
523
|
+
const entry = extractEntryFromBlock(block);
|
|
524
|
+
if (entry) lines.push(formatIndexEntryLine(entry));
|
|
525
|
+
}
|
|
526
|
+
return lines;
|
|
473
527
|
}
|
|
474
528
|
|
|
475
|
-
|
|
476
|
-
const
|
|
477
|
-
for (let i = 0; i < activePitfallRows.length; i++) {
|
|
478
|
-
const row = activePitfallRows[i];
|
|
479
|
-
const block = pitfallBlocks ? pitfallBlocks[i] : (row.raw_body ? row.raw_body : formatPitfallBody(row));
|
|
480
|
-
const entry = extractEntryFromBlock(block);
|
|
481
|
-
if (entry) pfEntries.push(entry);
|
|
482
|
-
}
|
|
529
|
+
const adrLines = indexLines(activeDecisionRows, decisionBlocks, formatDecisionBody);
|
|
530
|
+
const pfLines = indexLines(activePitfallRows, pitfallBlocks, formatPitfallBody);
|
|
483
531
|
|
|
484
|
-
if (
|
|
532
|
+
if (adrLines.length === 0 && pfLines.length === 0) return '(none)';
|
|
485
533
|
|
|
486
534
|
const blocks = [];
|
|
487
535
|
|
|
488
|
-
if (
|
|
489
|
-
blocks.push([`Decisions (${
|
|
536
|
+
if (adrLines.length > 0) {
|
|
537
|
+
blocks.push([`Decisions (${adrLines.length}):`, ...adrLines].join('\n'));
|
|
490
538
|
}
|
|
491
539
|
|
|
492
|
-
if (
|
|
493
|
-
blocks.push([`Pitfalls (${
|
|
540
|
+
if (pfLines.length > 0) {
|
|
541
|
+
blocks.push([`Pitfalls (${pfLines.length}):`, ...pfLines].join('\n'));
|
|
494
542
|
}
|
|
495
543
|
|
|
496
544
|
// Footer: explain how to read full bodies
|
|
497
545
|
const footerLines = [];
|
|
498
|
-
if (
|
|
546
|
+
if (adrLines.length > 0) {
|
|
499
547
|
footerLines.push(`ADR-NNN entries live in ${decisionsFilePath}`);
|
|
500
548
|
}
|
|
501
|
-
if (
|
|
549
|
+
if (pfLines.length > 0) {
|
|
502
550
|
footerLines.push(`PF-NNN entries live in ${pitfallsFilePath}`);
|
|
503
551
|
}
|
|
504
552
|
footerLines.push(
|
|
@@ -515,8 +563,9 @@ module.exports = {
|
|
|
515
563
|
formatAmendmentsLine,
|
|
516
564
|
formatDecisionBody,
|
|
517
565
|
formatPitfallBody,
|
|
566
|
+
formatEntryBodyV2,
|
|
567
|
+
formatInactiveTable,
|
|
568
|
+
formatIndexEntryLineV2,
|
|
518
569
|
buildTldrLine,
|
|
519
|
-
toLedgerRow,
|
|
520
|
-
isSafeRawBody,
|
|
521
570
|
buildIndexContent,
|
|
522
571
|
};
|