@forwardimpact/libwiki 0.2.35 → 0.3.1
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/README.md +53 -52
- package/package.json +6 -8
- package/src/active-claims.js +5 -5
- package/src/agent-roster.js +2 -2
- package/src/audit/admission.js +14 -11
- package/src/audit/conflict-markers-rule.js +9 -9
- package/src/audit/grammar.js +21 -18
- package/src/audit/rule-builders.js +20 -19
- package/src/audit/rules.js +35 -32
- package/src/audit/scopes.js +35 -34
- package/src/audit/status-row.js +14 -15
- package/src/block-renderer.js +5 -4
- package/src/boot.js +10 -8
- package/src/budget-gate.js +39 -36
- package/src/budget.js +3 -3
- package/src/cli-definition.js +35 -32
- package/src/commands/audit.js +3 -3
- package/src/commands/boot.js +2 -2
- package/src/commands/claim.js +46 -40
- package/src/commands/curate.js +34 -31
- package/src/commands/fix.js +74 -70
- package/src/commands/inbox.js +2 -2
- package/src/commands/init.js +13 -8
- package/src/commands/ledger.js +12 -12
- package/src/commands/log.js +20 -18
- package/src/commands/memo.js +5 -2
- package/src/commands/product-mix.js +18 -17
- package/src/commands/refresh.js +25 -22
- package/src/commands/rotate.js +10 -9
- package/src/commands/sync.js +26 -17
- package/src/conflict-markers.js +21 -21
- package/src/constants.js +38 -34
- package/src/gitattributes.js +10 -9
- package/src/integrity.js +29 -27
- package/src/issue-list-renderer.js +24 -16
- package/src/lane-files.js +11 -10
- package/src/ledger/anchor.js +6 -6
- package/src/ledger/projection.js +35 -32
- package/src/ledger/reader.js +4 -4
- package/src/marker-scanner.js +3 -2
- package/src/sanitize.js +12 -11
- package/src/secret-gate.js +41 -40
- package/src/status.js +12 -11
- package/src/storyboard-skeleton.js +20 -18
- package/src/util/agent-flag.js +8 -8
- package/src/util/clock.js +1 -1
- package/src/util/wiki-dir.js +7 -7
- package/src/weekly-log.js +115 -101
- package/src/wiki-sync.js +411 -379
- package/bin/fit-wiki.js +0 -94
package/src/constants.js
CHANGED
|
@@ -4,18 +4,19 @@ export const BROADCAST_TARGET = "all";
|
|
|
4
4
|
|
|
5
5
|
export const MEMORY_FILE = "MEMORY.md";
|
|
6
6
|
|
|
7
|
-
// Row-structured singleton surfaces under the sync-merge discipline
|
|
7
|
+
// Row-structured singleton surfaces under the sync-merge discipline. When a
|
|
8
8
|
// landing on one of these is contended, the resolution re-runs the row
|
|
9
|
-
// operation against the fresh remote tip
|
|
10
|
-
// lines
|
|
11
|
-
// table). STATUS.md phase rows join as their own
|
|
12
|
-
// Metrics CSV appends take the complementary
|
|
9
|
+
// operation against the fresh remote tip. It rebases the operation. It does
|
|
10
|
+
// not rebase the lines, and it never does a textual merge. Founding member:
|
|
11
|
+
// MEMORY.md (the Active Claims table). STATUS.md phase rows join as their own
|
|
12
|
+
// re-apply operations land. Metrics CSV appends take the complementary
|
|
13
|
+
// union-merge path below.
|
|
13
14
|
export const SINGLETON_PATHS = new Set([MEMORY_FILE]);
|
|
14
15
|
|
|
15
16
|
// The tracked `.gitattributes` declaration that makes concurrent appends to
|
|
16
|
-
// metrics CSVs union-merge (keep both sides' rows) on every publish path
|
|
17
|
-
//
|
|
18
|
-
// it governs every clone.
|
|
17
|
+
// metrics CSVs union-merge (keep both sides' rows) on every publish path. The
|
|
18
|
+
// appends then never conflict, and nothing picks one side. The wiki repo
|
|
19
|
+
// carries the declaration itself, so it governs every clone.
|
|
19
20
|
export const GITATTRIBUTES_FILE = ".gitattributes";
|
|
20
21
|
export const METRICS_CSV_MERGE_ATTRIBUTE = "metrics/**/*.csv merge=union";
|
|
21
22
|
export const ACTIVE_CLAIMS_HEADING = "## Active Claims";
|
|
@@ -25,9 +26,9 @@ export const ACTIVE_CLAIMS_TABLE_SEPARATOR =
|
|
|
25
26
|
"| --- | --- | --- | --- | --- | --- |";
|
|
26
27
|
|
|
27
28
|
// Match a rendered pipe-table row (header or separator) line-anchored and
|
|
28
|
-
// whitespace-tolerant between cells.
|
|
29
|
-
// literal
|
|
30
|
-
// (audit/rules.js)
|
|
29
|
+
// whitespace-tolerant between cells. The matcher comes from the rendered
|
|
30
|
+
// literal, so the claims parser (active-claims.js) and the audit
|
|
31
|
+
// (audit/rules.js) cannot drift on the column set. One literal, one matcher.
|
|
31
32
|
function pipeRowRe(literal, flags) {
|
|
32
33
|
const cells = literal
|
|
33
34
|
.split("|")
|
|
@@ -51,8 +52,8 @@ export const DECISION_HEADING = "### Decision";
|
|
|
51
52
|
// Unified budgets for the audited surfaces (summary, weekly-log, storyboard,
|
|
52
53
|
// memory). They keep per-surface rule pairs so the limits can diverge as the
|
|
53
54
|
// context-tax model says one surface should be looser or tighter. MEMORY.md is
|
|
54
|
-
// the tightest
|
|
55
|
-
//
|
|
55
|
+
// the tightest. Every boot reads it, and it holds settled cross-cutting state
|
|
56
|
+
// and no history. So its budget keeps the on-boot read cheap.
|
|
56
57
|
export const SUMMARY_LINE_BUDGET = 496;
|
|
57
58
|
export const SUMMARY_WORD_BUDGET = 2048;
|
|
58
59
|
export const WEEKLY_LOG_LINE_BUDGET = 496;
|
|
@@ -70,36 +71,39 @@ export const WEEKLY_LOG_NAME_RE = /^([a-z][a-z-]*)-(\d{4})-W(\d{2})\.md$/;
|
|
|
70
71
|
export const WEEKLY_LOG_PART_NAME_RE =
|
|
71
72
|
/^([a-z][a-z-]*)-(\d{4})-W(\d{2})-part\d+\.md$/;
|
|
72
73
|
|
|
73
|
-
// Day-section seam: `## YYYY-MM-DD` at line start
|
|
74
|
-
// (e.g. `## 2026-05-19 (third activation)`). One home so the
|
|
75
|
-
// seam-finder (weekly-log.js), the `log` command's last-entry probe
|
|
76
|
-
// (commands/log.js), and the audit's heading-grammar-drift rule
|
|
77
|
-
// cannot disagree on what a conforming entry heading is.
|
|
78
|
-
//
|
|
74
|
+
// Day-section seam: `## YYYY-MM-DD` at line start. The matcher tolerates a
|
|
75
|
+
// trailing suffix (e.g. `## 2026-05-19 (third activation)`). One home so the
|
|
76
|
+
// rotation seam-finder (weekly-log.js), the `log` command's last-entry probe
|
|
77
|
+
// (commands/log.js), and the audit's heading-grammar-drift rule
|
|
78
|
+
// (audit/rules.js) cannot disagree on what a conforming entry heading is. The
|
|
79
|
+
// source has no flags. Call sites add `g`/`m` as needed with
|
|
80
|
+
// `new RegExp(WEEKLY_LOG_SEAM_RE.source, …)`.
|
|
79
81
|
export const WEEKLY_LOG_SEAM_RE = /^## (\d{4}-\d{2}-\d{2})/;
|
|
80
82
|
|
|
81
|
-
//
|
|
82
|
-
// than this delimit sessions in the wiki history. 30 minutes.
|
|
83
|
+
// Idle-gap for the tier-2 integrity sweep. Lane-authored commits with a gap
|
|
84
|
+
// larger than this value delimit sessions in the wiki history. 30 minutes.
|
|
83
85
|
export const SESSION_GAP_MS = 30 * 60 * 1000;
|
|
84
86
|
|
|
85
87
|
// Carry-surface filename and H1 convention: `<agent>-carries.md` with an H1
|
|
86
|
-
// `# <agent> — Carries`. The name capture group is the agent prefix
|
|
87
|
-
//
|
|
88
|
-
// with the `**Carry-clearance:**` marker
|
|
89
|
-
// `wiki/release-engineer.md § Message Inbox`,
|
|
90
|
-
// migration relocates
|
|
91
|
-
// (audit/scopes.js) and rules (audit/rules.js) cannot drift
|
|
88
|
+
// `# <agent> — Carries`. The name capture group is the agent prefix, which the
|
|
89
|
+
// H1↔filename agreement rule uses. A Carry entry names its clearance trigger
|
|
90
|
+
// with the `**Carry-clearance:**` marker. That is the existing live convention
|
|
91
|
+
// in `wiki/release-engineer.md § Message Inbox`, kept verbatim so the
|
|
92
|
+
// migration relocates the entry and does not re-mark it. One home so the
|
|
93
|
+
// audit's classifier (audit/scopes.js) and rules (audit/rules.js) cannot drift
|
|
94
|
+
// on the syntax.
|
|
92
95
|
export const CARRY_SURFACE_NAME_RE = /^(.+)-carries\.md$/;
|
|
93
96
|
export const CARRY_SURFACE_H1_RE = /^# (.+) — Carries$/;
|
|
94
97
|
export const CARRY_CLEARANCE_MARKER_RE = /\*\*Carry-clearance:\*\*/;
|
|
95
98
|
|
|
96
99
|
// Storyboard marker syntax. An open or close marker tolerates optional trailing
|
|
97
|
-
// text after the tag (typically an inline "Do not edit. Generated from
|
|
100
|
+
// text after the tag (typically an inline "Do not edit. Generated from gemba-wiki
|
|
98
101
|
// refresh." notice). One home so the marker scanner (marker-scanner.js) and the
|
|
99
102
|
// audit's balance check (audit/rules.js) cannot drift on the syntax.
|
|
100
103
|
// Capture groups: 1 metric, 2 csvPath, 3 optional prior-read anchor date. The
|
|
101
|
-
// `prior=YYYY-MM-DD` token sits before the trailing-text group so the
|
|
102
|
-
// edit" notice
|
|
104
|
+
// `prior=YYYY-MM-DD` token sits before the trailing-text group, so the scanner
|
|
105
|
+
// still tolerates the "Do not edit" notice and the notice does not swallow the
|
|
106
|
+
// anchor.
|
|
103
107
|
export const XMR_OPEN_RE =
|
|
104
108
|
/^<!--\s*xmr:([^:\s]+):(\S+)(?:\s+prior=(\d{4}-\d{2}-\d{2}))?(?:\s+[^>]*?)?\s*-->\s*$/;
|
|
105
109
|
export const XMR_CLOSE_RE = /^<!--\s*\/xmr(?:\s+[^>]*?)?\s*-->\s*$/;
|
|
@@ -108,9 +112,9 @@ export const ISSUE_OPEN_RE =
|
|
|
108
112
|
export const ISSUE_CLOSE_RE =
|
|
109
113
|
/^<!--\s*\/(obstacles|experiments)(?:\s+[^>]*?)?\s*-->\s*$/;
|
|
110
114
|
|
|
111
|
-
// Materialized per-agent experiments surface.
|
|
112
|
-
//
|
|
113
|
-
// last-successful-sync stamp, and
|
|
115
|
+
// Materialized per-agent experiments surface. This is a distinct marker kind
|
|
116
|
+
// from `experiments:open`. It carries attributed, sanitized items plus a
|
|
117
|
+
// last-successful-sync stamp, and `gemba-wiki boot` reads it offline. One home
|
|
114
118
|
// so the scanner (marker-scanner.js), the refresh renderer (commands/refresh.js),
|
|
115
119
|
// the boot parser (boot.js), and the audit balance check (audit/rules.js) cannot
|
|
116
120
|
// drift on the syntax.
|
|
@@ -121,7 +125,7 @@ export const AGENT_EXPERIMENTS_CLOSE_RE =
|
|
|
121
125
|
export const LAST_SYNC_RE =
|
|
122
126
|
/^<!--\s*last-successful-sync:\s*(\d{4}-\d{2}-\d{2})\s*-->\s*$/;
|
|
123
127
|
// Attributed item line: `- #<n> [<agent>] <title> (by <author>)`. The author
|
|
124
|
-
// suffix is mandatory and anchored at end
|
|
128
|
+
// suffix is mandatory and anchored at end. The title group is greedy, which is
|
|
125
129
|
// unambiguous because sanitizeTitle defuses any embedded ` (by ` token.
|
|
126
130
|
export const AGENT_EXPERIMENT_ITEM_RE =
|
|
127
131
|
/^- #(\d+) \[([a-z][a-z-]*)\] (.*) \(by (.+)\)$/;
|
package/src/gitattributes.js
CHANGED
|
@@ -5,18 +5,19 @@ import {
|
|
|
5
5
|
} from "./constants.js";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
8
|
+
* Make sure the wiki's tracked `.gitattributes` declares the metrics-CSV union
|
|
9
9
|
* merge attribute (`metrics/**\/*.csv merge=union`), idempotently.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
* appears in the file, the
|
|
13
|
-
*
|
|
14
|
-
* `.gitattributes` content
|
|
15
|
-
* `{ changed: true }
|
|
11
|
+
* A present and correct line means no write. If the exact attribute line
|
|
12
|
+
* already appears in the file, the function leaves the file byte-unchanged and
|
|
13
|
+
* returns `{ changed: false }`. Otherwise it appends the line and keeps any
|
|
14
|
+
* existing `.gitattributes` content, or it creates the file with only that
|
|
15
|
+
* line. It then returns `{ changed: true }`.
|
|
16
16
|
*
|
|
17
17
|
* The single union declaration governs every clone because it is a tracked
|
|
18
|
-
* worktree file
|
|
19
|
-
* would not propagate to the sibling sessions that
|
|
18
|
+
* worktree file. Per-clone config (`core.attributesFile`,
|
|
19
|
+
* `.git/info/attributes`) would not propagate to the sibling sessions that
|
|
20
|
+
* cause the loss.
|
|
20
21
|
*
|
|
21
22
|
* @param {string} wikiDir - The wiki clone directory.
|
|
22
23
|
* @param {import('node:fs')} fsSync - Synchronous filesystem surface (`runtime.fsSync`).
|
|
@@ -30,7 +31,7 @@ export function ensureMetricsCsvMergeAttribute(wikiDir, fsSync) {
|
|
|
30
31
|
.split("\n")
|
|
31
32
|
.some((line) => line.trim() === METRICS_CSV_MERGE_ATTRIBUTE);
|
|
32
33
|
if (present) return { changed: false };
|
|
33
|
-
// Append the line
|
|
34
|
+
// Append the line. Keep existing content and end with a newline.
|
|
34
35
|
const base = text.endsWith("\n") || text === "" ? text : `${text}\n`;
|
|
35
36
|
fsSync.writeFileSync(filePath, `${base}${METRICS_CSV_MERGE_ATTRIBUTE}\n`);
|
|
36
37
|
return { changed: true };
|
package/src/integrity.js
CHANGED
|
@@ -3,8 +3,8 @@ import { SESSION_GAP_MS } from "./constants.js";
|
|
|
3
3
|
import { isLaneFile, enumerateLaneFiles } from "./lane-files.js";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Normalize a content line for content-keyed presence
|
|
7
|
-
* trailing whitespace.
|
|
6
|
+
* Normalize a content line for content-keyed presence. Strip a trailing CR and
|
|
7
|
+
* trailing whitespace. A blank line normalizes to "". Callers drop it.
|
|
8
8
|
* @param {string} line
|
|
9
9
|
* @returns {string}
|
|
10
10
|
*/
|
|
@@ -13,10 +13,10 @@ export function normLine(line) {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* Parse `git diff --unified=0` text into per-file change records
|
|
16
|
+
* Parse `git diff --unified=0` text into per-file change records. Attribute
|
|
17
17
|
* each `+`/`-` line to the file named by the most recent `+++ b/<path>` header.
|
|
18
|
-
* The `+++`/`---`/`@@` framing lines are not content.
|
|
19
|
-
* (pure deletions)
|
|
18
|
+
* The `+++`/`---`/`@@` framing lines are not content. The parser keeps
|
|
19
|
+
* `/dev/null` targets (pure deletions) as the home for their removed lines.
|
|
20
20
|
* @param {string} diffText
|
|
21
21
|
* @returns {Array<{home: string, added: string[], removed: string[]}>}
|
|
22
22
|
*/
|
|
@@ -38,7 +38,7 @@ export function parseDiff(diffText) {
|
|
|
38
38
|
return [...byHome.values()];
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
/** Whether a diff line is framing (`---`, `@@`, `diff`, `index`)
|
|
41
|
+
/** Whether a diff line is framing (`---`, `@@`, `diff`, `index`) and carries no content. */
|
|
42
42
|
function isDiffFraming(raw) {
|
|
43
43
|
return (
|
|
44
44
|
raw.startsWith("--- ") ||
|
|
@@ -48,7 +48,7 @@ function isDiffFraming(raw) {
|
|
|
48
48
|
);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
/** Strip a diff target's `a/`/`b/` prefix
|
|
51
|
+
/** Strip a diff target's `a/`/`b/` prefix. `/dev/null` stays as-is. */
|
|
52
52
|
function stripDiffTarget(target) {
|
|
53
53
|
const t = target.trim();
|
|
54
54
|
if (t === "/dev/null") return t;
|
|
@@ -57,10 +57,11 @@ function stripDiffTarget(target) {
|
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
59
|
* Compose a window of change records (oldest→newest) into the surviving
|
|
60
|
-
* addition assertions
|
|
61
|
-
* `norm(line)
|
|
62
|
-
* assertion (additions-only, own-deletions
|
|
63
|
-
* present iff its normalized line appears
|
|
60
|
+
* addition assertions. Then return those absent from the tip. The composition
|
|
61
|
+
* is content-keyed on `norm(line)`. A later own-lane deletion of an
|
|
62
|
+
* earlier-added line cancels its assertion (additions-only, own-deletions
|
|
63
|
+
* cancel). A surviving assertion is present iff its normalized line appears
|
|
64
|
+
* anywhere in `tipText`.
|
|
64
65
|
*
|
|
65
66
|
* @param {Array<{home: string, added: string[], removed: string[]}>} changes
|
|
66
67
|
* Window changes, oldest→newest.
|
|
@@ -78,7 +79,7 @@ export function findAbsent(changes, tipText, norm) {
|
|
|
78
79
|
return absent;
|
|
79
80
|
}
|
|
80
81
|
|
|
81
|
-
/** Compose window changes into surviving additions
|
|
82
|
+
/** Compose window changes into surviving additions. A later removal cancels its key. */
|
|
82
83
|
function composeAssertions(changes, norm) {
|
|
83
84
|
const asserted = new Map(); // norm(line) -> { contentId, pushHome }
|
|
84
85
|
for (const change of changes) {
|
|
@@ -108,10 +109,10 @@ function normalizedKeySet(text, norm) {
|
|
|
108
109
|
|
|
109
110
|
/**
|
|
110
111
|
* Resolve the previous-session push set from lane-authored commits (newest
|
|
111
|
-
* first) by idle-gap. Tier 2 runs at boot before the current session
|
|
112
|
-
*
|
|
113
|
-
* only for empty history
|
|
114
|
-
*
|
|
112
|
+
* first) by idle-gap. Tier 2 runs at boot before the current session pushes,
|
|
113
|
+
* so the most recent contiguous run is the previous session. The result is
|
|
114
|
+
* vacuous only for empty history. {@link sweepTier2} raises the degenerate
|
|
115
|
+
* (content-unresolvable) case. This function does not raise it.
|
|
115
116
|
*
|
|
116
117
|
* @param {Array<{sha: string, when: number}>} commits - Newest first.
|
|
117
118
|
* @param {number} gapMs - Idle-gap threshold (ms).
|
|
@@ -121,7 +122,7 @@ export function previousSessionWindow(commits, gapMs) {
|
|
|
121
122
|
if (commits.length === 0) return { kind: "vacuous" };
|
|
122
123
|
const tipRun = [commits[0]];
|
|
123
124
|
for (let i = 1; i < commits.length; i++) {
|
|
124
|
-
// commits are newest-first
|
|
125
|
+
// commits are newest-first. `when` is seconds. The gap threshold is ms.
|
|
125
126
|
const gap = (commits[i - 1].when - commits[i].when) * 1000;
|
|
126
127
|
if (gap > gapMs) break;
|
|
127
128
|
tipRun.push(commits[i]);
|
|
@@ -130,8 +131,8 @@ export function previousSessionWindow(commits, gapMs) {
|
|
|
130
131
|
}
|
|
131
132
|
|
|
132
133
|
/**
|
|
133
|
-
* Build a detection record. `detectedAt` is the binding wall-clock stamp
|
|
134
|
-
*
|
|
134
|
+
* Build a detection record. `detectedAt` is the binding wall-clock stamp
|
|
135
|
+
* (ISO). An exposure figure derived from commit timestamps carries the labeled
|
|
135
136
|
* `commit-timestamp` fallback basis.
|
|
136
137
|
*
|
|
137
138
|
* @param {object} d
|
|
@@ -166,8 +167,9 @@ export function makeDetection({
|
|
|
166
167
|
|
|
167
168
|
/**
|
|
168
169
|
* Render detections to flow output text. Empty input renders the empty string
|
|
169
|
-
* (clean-path silence).
|
|
170
|
-
* content identity
|
|
170
|
+
* (clean-path silence). Each detection gets one line that names the tier, the
|
|
171
|
+
* push-time home, and the content identity. The line labels exposure with its
|
|
172
|
+
* fallback basis when exposure is present.
|
|
171
173
|
* @param {object[]} detections
|
|
172
174
|
* @returns {string}
|
|
173
175
|
*/
|
|
@@ -186,10 +188,10 @@ export function renderDetections(detections) {
|
|
|
186
188
|
}
|
|
187
189
|
|
|
188
190
|
/**
|
|
189
|
-
* Tier-2 boot sweep
|
|
190
|
-
* content-present at the fetched (rebased) origin tip
|
|
191
|
+
* Tier-2 boot sweep. Verify the lane's previous-session push set is still
|
|
192
|
+
* content-present at the fetched (rebased) origin tip. Return detections for
|
|
191
193
|
* any absence. Reads git and lane files from `wikiDir` (the rebased tree).
|
|
192
|
-
* Never writes
|
|
194
|
+
* Never writes. Never throws into the flow (the caller wraps it).
|
|
193
195
|
*
|
|
194
196
|
* @param {object} ctx
|
|
195
197
|
* @param {import('@forwardimpact/libutil/runtime').Runtime} ctx.runtime
|
|
@@ -202,7 +204,7 @@ export function renderDetections(detections) {
|
|
|
202
204
|
export async function sweepTier2({ runtime, gitClient, wikiDir, agent, now }) {
|
|
203
205
|
const email = await gitClient.configGet("user.email", { cwd: wikiDir });
|
|
204
206
|
if (!email) {
|
|
205
|
-
// Lane identity unresolvable
|
|
207
|
+
// Lane identity unresolvable. Never a silent vacuous pass.
|
|
206
208
|
return [
|
|
207
209
|
makeDetection({
|
|
208
210
|
tier: 2,
|
|
@@ -221,7 +223,7 @@ export async function sweepTier2({ runtime, gitClient, wikiDir, agent, now }) {
|
|
|
221
223
|
|
|
222
224
|
const detections = [];
|
|
223
225
|
const changes = [];
|
|
224
|
-
// Oldest→newest so own-
|
|
226
|
+
// Oldest→newest so own-deletions cancel in commit order.
|
|
225
227
|
const ordered = [...window.commits].reverse();
|
|
226
228
|
for (const commit of ordered) {
|
|
227
229
|
const diff = await gitClient.diffRange(`${commit.sha}~1 ${commit.sha}`, {
|
|
@@ -250,7 +252,7 @@ export async function sweepTier2({ runtime, gitClient, wikiDir, agent, now }) {
|
|
|
250
252
|
|
|
251
253
|
for (const absent of findAbsent(changes, tipText, normLine)) {
|
|
252
254
|
// Exposure runs from the LAST window commit that added this exact line
|
|
253
|
-
// (its most recent assertion at origin)
|
|
255
|
+
// (its most recent assertion at origin). A same-home commit does not count.
|
|
254
256
|
const when = lastAssertionTime(changes, absent.contentId);
|
|
255
257
|
const exposureSeconds =
|
|
256
258
|
when != null ? Math.round(now / 1000 - when) : undefined;
|
|
@@ -3,10 +3,11 @@ import { createLogger } from "@forwardimpact/libtelemetry";
|
|
|
3
3
|
import { sanitizeCrossingField, sanitizeTitle } from "./sanitize.js";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
* (non-zero exit or unparseable JSON).
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* `renderAgentExperiments` throws this when the tracker query for the
|
|
7
|
+
* agent-experiments materialization fails (non-zero exit or unparseable JSON).
|
|
8
|
+
* This error differs from a `[]` return. It lets the refresh command keep the
|
|
9
|
+
* previously materialized block when the tracker is briefly unavailable. The
|
|
10
|
+
* command does not wipe the routing surface.
|
|
10
11
|
*/
|
|
11
12
|
export class TrackerQueryError extends Error {
|
|
12
13
|
/** @param {string} reason */
|
|
@@ -18,7 +19,12 @@ export class TrackerQueryError extends Error {
|
|
|
18
19
|
|
|
19
20
|
const AGENT_LABEL_RE = /^agent:([a-z][a-z-]*)$/;
|
|
20
21
|
|
|
21
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* Parse `owner/repo` from a git origin URL. Tolerates http(s), ssh, and
|
|
24
|
+
* proxy-rewritten URLs (e.g. `http://host/git/owner/repo`). It strips `.git`.
|
|
25
|
+
* Then it takes the last two path segments. Returns null when it finds nothing
|
|
26
|
+
* parseable.
|
|
27
|
+
*/
|
|
22
28
|
export function parseRepoSlug(originUrl) {
|
|
23
29
|
if (!originUrl) return null;
|
|
24
30
|
const stripped = originUrl.trim().replace(/\.git$/, "");
|
|
@@ -29,11 +35,12 @@ export function parseRepoSlug(originUrl) {
|
|
|
29
35
|
|
|
30
36
|
/**
|
|
31
37
|
* Render an issue-list block for an obstacles/experiments marker. Returns
|
|
32
|
-
* markdown lines. `cwd`
|
|
33
|
-
* resolves the correct origin
|
|
34
|
-
*
|
|
35
|
-
* is the resolved GH token (e.g.
|
|
36
|
-
* through `runtime.subprocess
|
|
38
|
+
* markdown lines. Set `cwd` to the parent monorepo's project root so `gh`
|
|
39
|
+
* resolves the correct origin. `repo` is an explicit `owner/name` slug. Use it
|
|
40
|
+
* when `gh` cannot parse the origin remote (e.g. sandbox proxy URLs). `token`
|
|
41
|
+
* is the resolved GH token (e.g. through `Config.ghToken()`). The `gh` command
|
|
42
|
+
* runs through `runtime.subprocess`. Stderr warnings run through
|
|
43
|
+
* `runtime.proc`.
|
|
37
44
|
*
|
|
38
45
|
* @param {object} options
|
|
39
46
|
* @param {string} options.topic
|
|
@@ -107,12 +114,13 @@ export async function renderIssueList({
|
|
|
107
114
|
}
|
|
108
115
|
|
|
109
116
|
/**
|
|
110
|
-
* Render the attributed per-agent experiments surface. Fetches open
|
|
111
|
-
*
|
|
112
|
-
* `agent:{name}` label
|
|
113
|
-
* `- #<number> [<agent>] <title> (by <author>)`.
|
|
114
|
-
* Throws {@link TrackerQueryError} on tracker failure so the
|
|
115
|
-
* previously materialized block
|
|
117
|
+
* Render the attributed per-agent experiments surface. Fetches open issues
|
|
118
|
+
* labeled `experiment`. Keeps only the issues that also carry an
|
|
119
|
+
* `agent:{name}` label. Emits one sanitized, body-free line per issue:
|
|
120
|
+
* `- #<number> [<agent>] <title> (by <author>)`. This function never reads
|
|
121
|
+
* issue bodies. Throws {@link TrackerQueryError} on tracker failure so the
|
|
122
|
+
* caller can keep the previously materialized block. Never returns `[]` on
|
|
123
|
+
* failure.
|
|
116
124
|
*
|
|
117
125
|
* @param {object} options
|
|
118
126
|
* @param {string} options.cwd
|
package/src/lane-files.js
CHANGED
|
@@ -4,12 +4,13 @@ import { WEEKLY_LOG_NAME_RE, WEEKLY_LOG_PART_NAME_RE } from "./constants.js";
|
|
|
4
4
|
const METRICS_CSV_RE = /^metrics\/[^/]+\/\d{4}\.csv$/;
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* Whether a wiki-root-relative path is one of the lane's own files
|
|
8
|
-
* agent's summary (`<agent>.md`), a weekly log or sealed
|
|
9
|
-
* (
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
7
|
+
* Whether a wiki-root-relative path is one of the lane's own files. The lane's
|
|
8
|
+
* own files are the agent's summary (`<agent>.md`), a weekly log or sealed
|
|
9
|
+
* part, and a metrics CSV (`metrics/<skill>/<year>.csv`). A weekly log or
|
|
10
|
+
* sealed part is `<agent>-YYYY-Www.md` or `<agent>-YYYY-Www-partN.md`, matched
|
|
11
|
+
* on the captured agent token. Metrics CSVs match by path for every agent. The
|
|
12
|
+
* tier-2 sweep's author filter enforces lane ownership of a metrics CSV at the
|
|
13
|
+
* commit level. This function does not.
|
|
13
14
|
*
|
|
14
15
|
* @param {string} relPath - Path relative to the wiki root (POSIX or native).
|
|
15
16
|
* @param {string} agent - Agent profile id (e.g. "staff-engineer").
|
|
@@ -27,9 +28,9 @@ export function isLaneFile(relPath, agent) {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
|
-
* Enumerate the lane's own files present under `wikiRoot
|
|
31
|
-
* summary and weekly-log files, plus every
|
|
32
|
-
* Returns wiki-root-relative POSIX paths.
|
|
31
|
+
* Enumerate the lane's own files present under `wikiRoot`. The list holds the
|
|
32
|
+
* top-level summary and weekly-log files that match, plus every
|
|
33
|
+
* `metrics/<skill>/<year>.csv`. Returns wiki-root-relative POSIX paths.
|
|
33
34
|
*
|
|
34
35
|
* @param {string} wikiRoot
|
|
35
36
|
* @param {string} agent
|
|
@@ -45,7 +46,7 @@ export function enumerateLaneFiles(wikiRoot, agent, fsSync) {
|
|
|
45
46
|
return out;
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
/** Wiki-root-relative `metrics/<skill>/<year>.csv` paths
|
|
49
|
+
/** Wiki-root-relative `metrics/<skill>/<year>.csv` paths that match the lane. */
|
|
49
50
|
function enumerateMetricsCsvs(wikiRoot, agent, fsSync) {
|
|
50
51
|
const metricsDir = path.join(wikiRoot, "metrics");
|
|
51
52
|
if (!fsSync.existsSync(metricsDir)) return [];
|
package/src/ledger/anchor.js
CHANGED
|
@@ -13,11 +13,11 @@
|
|
|
13
13
|
* ```
|
|
14
14
|
* ```
|
|
15
15
|
*
|
|
16
|
-
* The
|
|
17
|
-
* adds no parser dependency. `kind` is one of `occ`, `nm`,
|
|
18
|
-
* `ids` is a list of display labels
|
|
19
|
-
* prior anchor id)
|
|
20
|
-
* display only, so
|
|
16
|
+
* The parser reads the block by structure. It does not use a general YAML
|
|
17
|
+
* engine, so libwiki adds no parser dependency. `kind` is one of `occ`, `nm`,
|
|
18
|
+
* `fold`, `meta`. `ids` is a list of display labels. `event` is the durable
|
|
19
|
+
* key (a SHA or a prior anchor id). `note` is free text. The durable key is
|
|
20
|
+
* `event`. Labels are display only, so a relabel loses nothing.
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
const FENCE_OPEN = "```yaml alloc";
|
|
@@ -59,7 +59,7 @@ export function parseAnchor(body) {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
/**
|
|
62
|
-
* Render the canonical anchor body
|
|
62
|
+
* Render the canonical anchor body to post.
|
|
63
63
|
*
|
|
64
64
|
* @param {{kind: string, ids: string[], event: string, note?: string}} anchor
|
|
65
65
|
* @returns {string}
|
package/src/ledger/projection.js
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Fold the ordered allocation-anchor sequence into id assignments
|
|
3
|
-
* the two derived projections
|
|
4
|
-
* cross-cutting row. The anchor record is authoritative
|
|
5
|
-
* no sole-copy state and
|
|
6
|
-
* a cache miss
|
|
2
|
+
* Fold the ordered allocation-anchor sequence into id assignments. Then render
|
|
3
|
+
* the two derived projections: the ledger page body and the MEMORY
|
|
4
|
+
* cross-cutting row. The anchor record is authoritative. These projections
|
|
5
|
+
* hold no sole-copy state, and a rebuild recreates them from the record. So an
|
|
6
|
+
* erased projection is a cache miss that a rebuild repairs. It is not a loss
|
|
7
|
+
* event.
|
|
7
8
|
*
|
|
8
|
-
* Identity is the `event` key
|
|
9
|
-
* resolves first-published-wins
|
|
10
|
-
*
|
|
11
|
-
* default
|
|
12
|
-
* and re-mints the loser at the next
|
|
13
|
-
* label never moves.
|
|
9
|
+
* Identity is the `event` key. Labels are display output. So a
|
|
10
|
+
* double-allocation resolves first-published-wins. The loser takes a new
|
|
11
|
+
* label, and no record is lost. The `labelMode` parameter sets the label
|
|
12
|
+
* policy. `renumber` is the default and matches the team's established
|
|
13
|
+
* convention. It keeps the labels dense and re-mints the loser at the next
|
|
14
|
+
* free index. `gapped` leaves a gap so a label never moves. The code supports
|
|
15
|
+
* both. It forces neither.
|
|
14
16
|
*/
|
|
15
17
|
|
|
16
18
|
/**
|
|
@@ -37,7 +39,7 @@ export function foldAnchors(anchors) {
|
|
|
37
39
|
assignments.set(label, record);
|
|
38
40
|
continue;
|
|
39
41
|
}
|
|
40
|
-
// existing
|
|
42
|
+
// the existing record is older (anchors are id-ordered), so it wins.
|
|
41
43
|
if (!contested.has(label)) contested.set(label, []);
|
|
42
44
|
contested.get(label).push(record);
|
|
43
45
|
}
|
|
@@ -51,13 +53,13 @@ export function foldAnchors(anchors) {
|
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
/**
|
|
54
|
-
* Render the ledger page body from a fold.
|
|
55
|
-
*
|
|
56
|
-
* `<!-- anchor:ID -->`-cited blocks
|
|
57
|
-
* anchor that does not exist
|
|
58
|
-
* never silently
|
|
59
|
-
* double-allocation
|
|
60
|
-
* `gapped` leaves the loser's index as a gap.
|
|
56
|
+
* Render the ledger page body from a fold. The renderer groups entries by kind
|
|
57
|
+
* and orders them by the id of the anchor that won. It re-emits authored prose
|
|
58
|
+
* from `<!-- anchor:ID -->`-cited blocks in anchor-id order. The returned
|
|
59
|
+
* `missingProse` list names any cited anchor that does not exist. The renderer
|
|
60
|
+
* never drops one silently. `labelMode` selects the re-mint guidance for the
|
|
61
|
+
* loser of a double-allocation. `renumber` (default) re-mints at the next free
|
|
62
|
+
* index. `gapped` leaves the loser's index as a gap.
|
|
61
63
|
*
|
|
62
64
|
* @param {{assignments: Map, conflicts: Array}} fold
|
|
63
65
|
* @param {Array<{anchorId: number, text: string}>} [prose] - Anchor-cited prose blocks.
|
|
@@ -72,7 +74,7 @@ export function renderLedgerPage(
|
|
|
72
74
|
const lines = [
|
|
73
75
|
"# Parallel-Collision Ledger",
|
|
74
76
|
"",
|
|
75
|
-
"Derived projection of the allocation-anchor record.
|
|
77
|
+
"Derived projection of the allocation-anchor record. `gemba-wiki ledger rebuild` rebuilds it. Do not hand-edit identifiers here. Allocate at an anchor.",
|
|
76
78
|
"",
|
|
77
79
|
...renderKindSections(fold),
|
|
78
80
|
...renderConflicts(fold, labelMode),
|
|
@@ -123,8 +125,8 @@ function renderConflicts(fold, labelMode) {
|
|
|
123
125
|
|
|
124
126
|
/**
|
|
125
127
|
* Extract `<!-- anchor:ID -->`-cited prose blocks from an existing ledger-page
|
|
126
|
-
* body so a rebuild re-emits them
|
|
127
|
-
* from its citation marker to the next marker or end of input.
|
|
128
|
+
* body so a rebuild re-emits them. A rebuild does not drop them. Each block
|
|
129
|
+
* runs from its citation marker to the next marker or end of input.
|
|
128
130
|
*
|
|
129
131
|
* @param {string} pageBody - The current ledger-page text.
|
|
130
132
|
* @returns {Array<{anchorId: number, text: string}>}
|
|
@@ -160,8 +162,8 @@ function appendProse(lines, fold, prose) {
|
|
|
160
162
|
}
|
|
161
163
|
|
|
162
164
|
/**
|
|
163
|
-
* Render the MEMORY cross-cutting row counters from a fold
|
|
164
|
-
* kind, plus the total assigned count.
|
|
165
|
+
* Render the MEMORY cross-cutting row counters from a fold. The counters are
|
|
166
|
+
* the next-free index per kind, plus the total assigned count.
|
|
165
167
|
*
|
|
166
168
|
* @param {{assignments: Map}} fold
|
|
167
169
|
* @returns {string}
|
|
@@ -176,7 +178,7 @@ export function renderMemoryRow(fold) {
|
|
|
176
178
|
return (
|
|
177
179
|
`Parallel-collision allocation (derived from the anchor record): ` +
|
|
178
180
|
`${fold.assignments.size} ids assigned; next free #${next.occ}, NM${next.nm}, ` +
|
|
179
|
-
`n=${next.fold}, M${next.meta}. Allocate at an anchor
|
|
181
|
+
`n=${next.fold}, M${next.meta}. Allocate at an anchor. Never allocate by editing this row.`
|
|
180
182
|
);
|
|
181
183
|
}
|
|
182
184
|
|
|
@@ -188,11 +190,12 @@ const MEMORY_REGION_RE = new RegExp(
|
|
|
188
190
|
|
|
189
191
|
/**
|
|
190
192
|
* Write the derived MEMORY-row counters into a delimited region of a MEMORY.md
|
|
191
|
-
* body
|
|
192
|
-
*
|
|
193
|
-
* only sole-copy-free surface
|
|
194
|
-
* outside
|
|
195
|
-
* under a heading
|
|
193
|
+
* body. The row is then a rebuildable projection of the anchor record. The
|
|
194
|
+
* write does not overwrite the surrounding authored narrative. The region is
|
|
195
|
+
* the only sole-copy-free surface. This function regenerates its interior in
|
|
196
|
+
* full. It preserves everything outside the region byte-for-byte. If the
|
|
197
|
+
* region is absent, this function appends it under a heading. If the region is
|
|
198
|
+
* present, this function replaces only its interior.
|
|
196
199
|
*
|
|
197
200
|
* @param {string} memoryBody - Current `MEMORY.md` text.
|
|
198
201
|
* @param {{assignments: Map}} fold
|
|
@@ -210,8 +213,8 @@ export function writeMemoryRowRegion(memoryBody, fold) {
|
|
|
210
213
|
|
|
211
214
|
/**
|
|
212
215
|
* Extract the derived MEMORY-row region interior from a MEMORY.md body, or
|
|
213
|
-
* `null` if the region is absent.
|
|
214
|
-
* surface alone
|
|
216
|
+
* `null` if the region is absent. `verify` uses this to diff the projection
|
|
217
|
+
* surface alone. It never diffs the surrounding narrative.
|
|
215
218
|
*
|
|
216
219
|
* @param {string} memoryBody
|
|
217
220
|
* @returns {string|null}
|
package/src/ledger/reader.js
CHANGED
|
@@ -2,16 +2,16 @@ import { parseAnchor } from "./anchor.js";
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* The obstacle issue whose comment thread is the allocation-anchor surface.
|
|
5
|
-
* GitHub serializes comment creation and assigns a monotonic `id
|
|
5
|
+
* GitHub serializes comment creation and assigns a monotonic `id`. So the `id`
|
|
6
6
|
* order is the allocation serialization no merge can erase.
|
|
7
7
|
*/
|
|
8
8
|
export const DEFAULT_ANCHOR_ISSUE = 1564;
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Read every allocation anchor from the obstacle issue's comment thread, in
|
|
12
|
-
* server `id` order ascending. The lowest comment `id`
|
|
13
|
-
* is its winner (first published wins).
|
|
14
|
-
*
|
|
12
|
+
* server `id` order ascending. The lowest comment `id` that claims a given
|
|
13
|
+
* label is its winner (first published wins). This function skips comments
|
|
14
|
+
* that carry no anchor block.
|
|
15
15
|
*
|
|
16
16
|
* @param {object} ghClient - A GhClient (or mock) exposing `apiGetPaginated`.
|
|
17
17
|
* @param {object} opts
|
package/src/marker-scanner.js
CHANGED
|
@@ -84,8 +84,9 @@ function matchClose(line, open) {
|
|
|
84
84
|
|
|
85
85
|
/**
|
|
86
86
|
* Scan text for paired marker blocks (xmr or issue-list). Returns positions and
|
|
87
|
-
* metadata.
|
|
88
|
-
* callback (default: discard)
|
|
87
|
+
* metadata. The scanner reports dangling open markers through the injected
|
|
88
|
+
* `warn` callback (default: discard). It does not write to the process
|
|
89
|
+
* directly.
|
|
89
90
|
* @param {string} text - The storyboard text to scan.
|
|
90
91
|
* @param {{warn?: (message: string) => void}} [options]
|
|
91
92
|
* @returns {Array<object>} The paired marker blocks.
|