@milehimikey/em 1.14.2 → 1.16.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/.claude/agents/em-implementer.md +12 -2
- package/.claude/agents/em-validator.md +2 -1
- package/.claude/skills/event-modeling/SKILL.md +3 -3
- package/.claude/skills/event-modeling-conform/SKILL.md +1 -1
- package/.claude/skills/event-modeling-design/SKILL.md +29 -18
- package/.claude/skills/event-modeling-discover/SKILL.md +1 -1
- package/.claude/skills/event-modeling-engagement/SKILL.md +60 -56
- package/.claude/skills/event-modeling-engagement/reference/engagement.md +13 -19
- package/.claude/skills/event-modeling-implement/SKILL.md +3 -3
- package/.claude/skills/event-modeling-implement/reference/implement.md +40 -27
- package/.claude/skills/event-modeling-review/SKILL.md +13 -3
- package/.claude/skills/event-modeling-shared/reference/em-dsl.md +234 -228
- package/.claude/skills/event-modeling-shared/reference/operating-principles.md +3 -3
- package/.claude/skills/event-modeling-shared/reference/slice-doc-schema.md +69 -27
- package/.claude/skills/event-modeling-shared/templates/constitution.md +5 -7
- package/.claude-plugin/marketplace.json +2 -2
- package/dist/catalog/deferQueue.d.ts +20 -0
- package/dist/catalog/deferQueue.js +63 -0
- package/dist/catalog/deferQueue.js.map +1 -0
- package/dist/catalog/docJoin.d.ts +22 -1
- package/dist/catalog/docJoin.js +13 -4
- package/dist/catalog/docJoin.js.map +1 -1
- package/dist/catalog/docModelConsistencyValidate.js +4 -8
- package/dist/catalog/docModelConsistencyValidate.js.map +1 -1
- package/dist/catalog/driftSignal.d.ts +16 -12
- package/dist/catalog/driftSignal.js +14 -9
- package/dist/catalog/driftSignal.js.map +1 -1
- package/dist/catalog/sliceDoc.d.ts +51 -0
- package/dist/catalog/sliceDoc.js +47 -0
- package/dist/catalog/sliceDoc.js.map +1 -1
- package/dist/catalog/sliceReadyValidate.js +19 -1
- package/dist/catalog/sliceReadyValidate.js.map +1 -1
- package/dist/catalog/sliceSections.d.ts +5 -0
- package/dist/catalog/sliceSections.js +11 -2
- package/dist/catalog/sliceSections.js.map +1 -1
- package/dist/cli/conformScope.d.ts +4 -0
- package/dist/cli/conformScope.js +2 -2
- package/dist/cli/conformScope.js.map +1 -1
- package/dist/cli/coverage.js +7 -3
- package/dist/cli/coverage.js.map +1 -1
- package/dist/cli/defer.d.ts +106 -2
- package/dist/cli/defer.js +357 -24
- package/dist/cli/defer.js.map +1 -1
- package/dist/cli/engagement.d.ts +26 -5
- package/dist/cli/engagement.js +55 -28
- package/dist/cli/engagement.js.map +1 -1
- package/dist/cli/engagementFile.d.ts +2 -0
- package/dist/cli/engagementFile.js +5 -3
- package/dist/cli/engagementFile.js.map +1 -1
- package/dist/cli/findings.d.ts +2 -2
- package/dist/cli/findings.js +2 -2
- package/dist/cli/frontmatterSurgery.d.ts +3 -3
- package/dist/cli/frontmatterSurgery.js +3 -3
- package/dist/cli/frontmatterSurgery.js.map +1 -1
- package/dist/cli/markImplemented.d.ts +23 -18
- package/dist/cli/markImplemented.js +99 -15
- package/dist/cli/markImplemented.js.map +1 -1
- package/dist/cli/ratify.d.ts +36 -6
- package/dist/cli/ratify.js +130 -35
- package/dist/cli/ratify.js.map +1 -1
- package/dist/cli/review.js +3 -3
- package/dist/cli/review.js.map +1 -1
- package/dist/cli/revise.d.ts +67 -0
- package/dist/cli/revise.js +229 -0
- package/dist/cli/revise.js.map +1 -0
- package/dist/cli/sliceConform.d.ts +4 -3
- package/dist/cli/sliceConform.js +29 -12
- package/dist/cli/sliceConform.js.map +1 -1
- package/dist/cli/status.d.ts +30 -6
- package/dist/cli/status.js +59 -19
- package/dist/cli/status.js.map +1 -1
- package/dist/cli/upgrade.js +17 -12
- package/dist/cli/upgrade.js.map +1 -1
- package/dist/cli.js +173 -85
- package/dist/cli.js.map +1 -1
- package/dist/emit/engagementJson.d.ts +7 -4
- package/dist/emit/engagementJson.js +11 -4
- package/dist/emit/engagementJson.js.map +1 -1
- package/dist/emit/json.d.ts +1 -1
- package/dist/emit/json.js +18 -5
- package/dist/emit/json.js.map +1 -1
- package/dist/emit/statusJson.d.ts +1 -1
- package/dist/emit/statusJson.js +6 -1
- package/dist/emit/statusJson.js.map +1 -1
- package/dist/mcp/server.js +5 -5
- package/dist/mcp/server.js.map +1 -1
- package/dist/model/continuation.d.ts +11 -1
- package/dist/model/continuation.js +29 -0
- package/dist/model/continuation.js.map +1 -1
- package/dist/model/resolveSlice.d.ts +19 -0
- package/dist/model/resolveSlice.js +69 -0
- package/dist/model/resolveSlice.js.map +1 -0
- package/dist/model/rules.d.ts +8 -0
- package/dist/model/rules.js +8 -0
- package/dist/model/rules.js.map +1 -1
- package/dist/model/sliceDocIndex.d.ts +5 -0
- package/dist/model/sliceDocIndex.js +6 -4
- package/dist/model/sliceDocIndex.js.map +1 -1
- package/dist/model/validate.js +5 -2
- package/dist/model/validate.js.map +1 -1
- package/dist/parser/parser.js +55 -1
- package/dist/parser/parser.js.map +1 -1
- package/dist/templates.d.ts +1 -1
- package/dist/templates.js +5 -7
- package/dist/templates.js.map +1 -1
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/agents/em-implementer.md +12 -2
- package/plugin/agents/em-validator.md +2 -1
- package/plugin/shared/reference/em-dsl.md +234 -228
- package/plugin/shared/reference/operating-principles.md +3 -3
- package/plugin/shared/reference/slice-doc-schema.md +69 -27
- package/plugin/shared/templates/constitution.md +5 -7
- package/plugin/skills/conform/SKILL.md +1 -1
- package/plugin/skills/design/SKILL.md +29 -18
- package/plugin/skills/discover/SKILL.md +1 -1
- package/plugin/skills/engagement/SKILL.md +60 -56
- package/plugin/skills/engagement/reference/engagement.md +13 -19
- package/plugin/skills/event-modeling/SKILL.md +3 -3
- package/plugin/skills/implement/SKILL.md +3 -3
- package/plugin/skills/implement/reference/implement.md +40 -27
- package/plugin/skills/review/SKILL.md +13 -3
- package/dist/cli/reratify.d.ts +0 -58
- package/dist/cli/reratify.js +0 -238
- package/dist/cli/reratify.js.map +0 -1
package/dist/cli/defer.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { NormalizedModel } from "../model/model.js";
|
|
2
2
|
import { RefsResult } from "../model/refs.js";
|
|
3
|
+
import { QueuedItem } from "../catalog/deferQueue.js";
|
|
3
4
|
/** `v<int>` → int, else null. */
|
|
4
5
|
export declare function parseUntil(s: string): number | null;
|
|
5
6
|
export interface DeferInput {
|
|
@@ -25,6 +26,12 @@ export type ApplyDeferResult = {
|
|
|
25
26
|
* Already deferred to the same version → `{ok: true, changed: false}` (idempotent no-op).
|
|
26
27
|
*/
|
|
27
28
|
export declare function applyDefer(raw: string, input: DeferInput): ApplyDeferResult;
|
|
29
|
+
/** Appends `bullet` at the end of the section under `heading` (before the next `## ` heading or
|
|
30
|
+
* EOF, after the section's last non-blank line) unless an identical bullet line exists. */
|
|
31
|
+
export declare function appendBullet(lines: string[], heading: RegExp, bullet: string, eol: string): {
|
|
32
|
+
lines: string[];
|
|
33
|
+
changed: boolean;
|
|
34
|
+
} | null;
|
|
28
35
|
export interface StateMirrorInput {
|
|
29
36
|
question: string;
|
|
30
37
|
sliceKey: string;
|
|
@@ -36,20 +43,117 @@ export interface StateMirrorInput {
|
|
|
36
43
|
}
|
|
37
44
|
/** Pure: mirrors the deferral into the state file's parking lot and Decisions log. */
|
|
38
45
|
export declare function applyDeferStateFile(raw: string, input: StateMirrorInput): ApplyDeferResult;
|
|
39
|
-
/** Pure (
|
|
46
|
+
/** Pure (revise): re-opens every `- [x] … — v<old>: …; deferred to v<newVersion> (…)` item under
|
|
40
47
|
* `## Open Questions` as `- [ ] <original text>`. Items deferred to a later version are untouched. */
|
|
41
48
|
export declare function reopenDeferred(raw: string, newVersion: number): {
|
|
42
49
|
content: string;
|
|
43
50
|
count: number;
|
|
44
51
|
};
|
|
52
|
+
/** True when the doc's body is a signed spec that defer must not touch: `status: implemented`, or
|
|
53
|
+
* `status: ready-to-implement` with a `ratifiedBy:`. A `draft`/`reviewed` doc — including the
|
|
54
|
+
* next version of a shipped slice — is still being written, so defer edits it as ever. */
|
|
55
|
+
export declare function isLockedDoc(raw: string): boolean;
|
|
56
|
+
/** Pure: mirrors a QUEUED deferral into the state file — the queued parking-lot bullet and a
|
|
57
|
+
* dated Decisions-log bullet. Idempotent on an identical bullet. */
|
|
58
|
+
export declare function applyQueueStateFile(raw: string, input: StateMirrorInput): ApplyDeferResult;
|
|
59
|
+
/** Pure (revise / mark-implemented): appends every queued item to `## Open Questions` of the
|
|
60
|
+
* freshly opened draft (creating the section at EOF when the doc has none), skipping a line
|
|
61
|
+
* already present. Returns how many items were written. */
|
|
62
|
+
export declare function injectQueuedQuestions(raw: string, items: QueuedItem[]): {
|
|
63
|
+
content: string;
|
|
64
|
+
count: number;
|
|
65
|
+
};
|
|
66
|
+
/** Pure: "clears" the queue for `<sliceKey>@v<targetVersion>` in the state file — each queued
|
|
67
|
+
* parking-lot bullet becomes the ordinary `- <q> (from slice <key>, deferred to v<until>)`
|
|
68
|
+
* mirror a draft-doc defer writes. Other slices' and other versions' bullets are untouched. */
|
|
69
|
+
export declare function drainQueueInState(raw: string, sliceKey: string, targetVersion: number): {
|
|
70
|
+
content: string;
|
|
71
|
+
count: number;
|
|
72
|
+
};
|
|
73
|
+
/** Reads the queue for `<sliceKey>@v<targetVersion>` from `<baseDir>/.event-modeling.md`; empty
|
|
74
|
+
* when the file is absent. */
|
|
75
|
+
export declare function readQueue(baseDir: string, sliceKey: string, targetVersion: number): {
|
|
76
|
+
items: QueuedItem[];
|
|
77
|
+
statePath: string;
|
|
78
|
+
stateRaw: string | null;
|
|
79
|
+
};
|
|
80
|
+
/** Writes the drained state file once the draft carrying the queue is on disk. */
|
|
81
|
+
export declare function clearQueue(baseDir: string, sliceKey: string, targetVersion: number): void;
|
|
82
|
+
/** An open `issue "..."` clause on one of the slice's own elements. */
|
|
83
|
+
export interface IssueCandidate {
|
|
84
|
+
kind: string;
|
|
85
|
+
name: string;
|
|
86
|
+
/** The element's declaration line in the `.em` (1-based) — where the clause search starts. */
|
|
87
|
+
line: number;
|
|
88
|
+
issue: string;
|
|
89
|
+
}
|
|
90
|
+
/** Open issues on the slice's own elements whose text contains `text` (case-sensitive substring,
|
|
91
|
+
* the same matching the positional path applies to Open Questions). An issue on a sibling
|
|
92
|
+
* slice's element is never a candidate — the deferral lands on THIS slice's doc. */
|
|
93
|
+
export declare function findIssueCandidates(model: NormalizedModel, sliceIndex: number, text: string): IssueCandidate[];
|
|
94
|
+
export declare function formatIssueCandidate(c: IssueCandidate): string;
|
|
95
|
+
export type RemoveIssueResult = {
|
|
96
|
+
ok: true;
|
|
97
|
+
content: string;
|
|
98
|
+
removedLine: number;
|
|
99
|
+
} | {
|
|
100
|
+
ok: false;
|
|
101
|
+
message: string;
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* Pure: removes the one `issue "<issueText>"` clause that belongs to the element declared at
|
|
105
|
+
* `startLine` (1-based). The clause may sit on the declaration line or trail the element's
|
|
106
|
+
* `{ … }` field block (parser.ts's `extractClauses` accepts both), so the scan runs forward from
|
|
107
|
+
* `startLine` and stops at the first line carrying an `issue` clause that decodes to
|
|
108
|
+
* `issueText`. The clause span (plus the one whitespace run before it) is cut out of that line;
|
|
109
|
+
* when nothing but whitespace is left, the whole line goes, EOL included. Everything else —
|
|
110
|
+
* other clauses on the line, every other line, the file's EOL style — is copied through
|
|
111
|
+
* verbatim. The caller re-parses the result before accepting it (`runDeferFromIssue`).
|
|
112
|
+
*/
|
|
113
|
+
export declare function removeIssueClause(raw: string, startLine: number, issueText: string): RemoveIssueResult;
|
|
114
|
+
/**
|
|
115
|
+
* Pure: records `issueText` on the doc as an already-deferred Open Question. If an unchecked
|
|
116
|
+
* item containing the text already exists (someone wrote it down by hand), that item is deferred
|
|
117
|
+
* in place via `applyDefer` — never a duplicate. Otherwise the `[x]` item is appended to
|
|
118
|
+
* `## Open Questions`, creating the section at EOF when the doc has none (a pre-template doc).
|
|
119
|
+
* An identical item already present → `changed: false`.
|
|
120
|
+
*/
|
|
121
|
+
export declare function applyDeferFromIssueDoc(raw: string, issueText: string, input: Omit<DeferInput, "question">): ApplyDeferResult;
|
|
45
122
|
export type RunDeferResult = {
|
|
46
123
|
ok: true;
|
|
47
124
|
path: string;
|
|
48
125
|
changed: boolean;
|
|
126
|
+
queued: QueuedNote | null;
|
|
49
127
|
} | {
|
|
50
128
|
ok: false;
|
|
51
129
|
message: string;
|
|
52
130
|
};
|
|
53
|
-
/**
|
|
131
|
+
/** GH #236: set when defer QUEUED the question (a ratified/shipped doc) instead of editing the
|
|
132
|
+
* doc — `forVersion` is the draft that will carry it, `docVersion` the version left untouched. */
|
|
133
|
+
export interface QueuedNote {
|
|
134
|
+
forVersion: number;
|
|
135
|
+
docVersion: number;
|
|
136
|
+
}
|
|
137
|
+
/** Resolves `sliceKey` to its bound doc (same join as ratify/revise), computes both edits, and
|
|
54
138
|
* writes only when every precondition holds (the state file is required and never created). */
|
|
55
139
|
export declare function runDefer(model: NormalizedModel, refs: RefsResult, baseDir: string, sliceKey: string, input: Omit<DeferInput, "currentVersion">): RunDeferResult;
|
|
140
|
+
export type RunDeferFromIssueResult = {
|
|
141
|
+
ok: true;
|
|
142
|
+
path: string;
|
|
143
|
+
changed: boolean;
|
|
144
|
+
issue: string;
|
|
145
|
+
modelLine: number | null;
|
|
146
|
+
queued: QueuedNote | null;
|
|
147
|
+
} | {
|
|
148
|
+
ok: false;
|
|
149
|
+
message: string;
|
|
150
|
+
};
|
|
151
|
+
/**
|
|
152
|
+
* `--from-issue <text>` (MIL-281): the single open `issue "..."` on one of the slice's own
|
|
153
|
+
* elements whose text contains `text` becomes a deferred Open Question on the doc, is mirrored
|
|
154
|
+
* into the state file, and its clause is removed from `modelFile`. All three edits are computed
|
|
155
|
+
* first; nothing is written unless every one succeeds. Zero candidates with the question already
|
|
156
|
+
* deferred on the doc → the idempotent no-op; zero otherwise → refuse; two or more → refuse
|
|
157
|
+
* naming each with its `.em` line.
|
|
158
|
+
*/
|
|
159
|
+
export declare function runDeferFromIssue(model: NormalizedModel, refs: RefsResult, baseDir: string, modelFile: string, sliceKey: string, text: string, input: Omit<DeferInput, "question" | "currentVersion">): RunDeferFromIssueResult;
|
package/dist/cli/defer.js
CHANGED
|
@@ -2,17 +2,41 @@
|
|
|
2
2
|
// `em slice defer` (MIL-275): the missing verb behind docs/process.md's "resolved or explicitly
|
|
3
3
|
// deferred". Rewrites ONE unchecked `## Open Questions` item as a checked, dated "deferred to
|
|
4
4
|
// v<n>" item, and mirrors it into the model's `.event-modeling.md` (the `## Open questions /
|
|
5
|
-
// parking lot` bullet and a dated `## Decisions log` bullet), idempotently. `em slice
|
|
6
|
-
// reads the `deferred to v<n>` marker back and re-opens the item (`reopenDeferred`, below).
|
|
5
|
+
// parking lot` bullet and a dated `## Decisions log` bullet), idempotently. `em slice revise`
|
|
6
|
+
// (and `mark-implemented`'s auto-open, MIL-283) reads the `deferred to v<n>` marker back and re-opens the item (`reopenDeferred`, below).
|
|
7
7
|
//
|
|
8
|
-
// Write strategy: surgical line edits (the same discipline as ratify.ts/
|
|
8
|
+
// Write strategy: surgical line edits (the same discipline as ratify.ts/revise.ts) — every
|
|
9
9
|
// byte outside the edited line / appended bullet, including the file's own EOL style, is copied
|
|
10
10
|
// through verbatim. Pure `apply*` functions here; the fs orchestration (`runDefer`) is thin.
|
|
11
|
+
//
|
|
12
|
+
// MIL-281 — `--from-issue`: the question a review session actually produces is an `issue "..."`
|
|
13
|
+
// clause in the `.em` (what the walkthrough captures live, what `em validate --list-issues`
|
|
14
|
+
// lists), not an item under the doc's `## Open Questions`; defer could only see the latter, so a
|
|
15
|
+
// room-raised question took a hand edit of the doc before it could be deferred. `--from-issue
|
|
16
|
+
// <text>` promotes the single matching open issue on the slice's own elements into the doc as an
|
|
17
|
+
// already-deferred `[x]` item, mirrors it into the state file exactly like the positional path,
|
|
18
|
+
// and REMOVES the `issue` clause from the `.em` — the deferral record replaces the red note, so
|
|
19
|
+
// the diagram stops flagging a question the doc now tracks with a return path. Three files, all
|
|
20
|
+
// computed before any is written; the `.em` edit is a single clause span (or the whole line when
|
|
21
|
+
// the clause was alone on it), re-parsed before it is accepted. Not a generic "move issue to doc"
|
|
22
|
+
// verb: the only thing a promoted issue can become is a deferred question.
|
|
23
|
+
//
|
|
24
|
+
// GH #236 — a RATIFIED or SHIPPED doc is never edited by defer. Its body is the signed spec
|
|
25
|
+
// (`ratifiedHash`), so on `status: ready-to-implement` + `ratifiedBy:` or `status: implemented`
|
|
26
|
+
// both paths QUEUE the question instead: one bullet in the state file's parking lot plus the dated
|
|
27
|
+
// Decisions-log bullet, the doc untouched (catalog/deferQueue.ts has the bullet format).
|
|
28
|
+
// `revise`/`mark-implemented` inject the queue into the v<N+1> draft they open
|
|
29
|
+
// (`injectQueuedQuestions`) and `drainQueueInState` turns the bullets back into ordinary
|
|
30
|
+
// deferral mirrors. `draft`/`reviewed` docs behave exactly as before.
|
|
11
31
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
12
32
|
import { join } from "node:path";
|
|
13
33
|
import { continuationOf } from "../model/continuation.js";
|
|
14
34
|
import { resolveSliceDocJoin } from "../catalog/docJoin.js";
|
|
35
|
+
import { decodeQuoted, matchQuote } from "../parser/lexer.js";
|
|
36
|
+
import { compile } from "../pipeline.js";
|
|
15
37
|
import { STATE_FILE_NAME } from "./stateFile.js";
|
|
38
|
+
import { fieldLineRegex, locateFrontmatterInner, normalizeFieldValue } from "./frontmatterSurgery.js";
|
|
39
|
+
import { parseQueuedBullet, queuedBullet, queuedItems } from "../catalog/deferQueue.js";
|
|
16
40
|
const OPEN_QUESTIONS_HEADING = /^##\s+Open Questions\s*$/i;
|
|
17
41
|
const TASK_ITEM = /^([ \t]*[-*]\s*)\[([ xX])\](.*)$/;
|
|
18
42
|
const PARKING_LOT_HEADING = /^##\s+Open questions \/ parking lot\s*$/i;
|
|
@@ -115,7 +139,7 @@ export function applyDefer(raw, input) {
|
|
|
115
139
|
}
|
|
116
140
|
/** Appends `bullet` at the end of the section under `heading` (before the next `## ` heading or
|
|
117
141
|
* EOF, after the section's last non-blank line) unless an identical bullet line exists. */
|
|
118
|
-
function appendBullet(lines, heading, bullet, eol) {
|
|
142
|
+
export function appendBullet(lines, heading, bullet, eol) {
|
|
119
143
|
const start = lines.findIndex((l) => heading.test(stripEol(l).body));
|
|
120
144
|
if (start === -1)
|
|
121
145
|
return null;
|
|
@@ -135,7 +159,9 @@ function appendBullet(lines, heading, bullet, eol) {
|
|
|
135
159
|
const out = lines.slice();
|
|
136
160
|
if (out[last] !== undefined && !/\n$/.test(out[last]))
|
|
137
161
|
out[last] = out[last] + eol;
|
|
138
|
-
|
|
162
|
+
// An empty section (nothing but the heading) keeps a blank line between heading and first
|
|
163
|
+
// item — the shape the slice-doc template and every hand-written section use.
|
|
164
|
+
out.splice(last + 1, 0, ...(last === start ? [eol, bullet + eol] : [bullet + eol]));
|
|
139
165
|
return { lines: out, changed: true };
|
|
140
166
|
}
|
|
141
167
|
/** Pure: mirrors the deferral into the state file's parking lot and Decisions log. */
|
|
@@ -154,7 +180,7 @@ export function applyDeferStateFile(raw, input) {
|
|
|
154
180
|
return { ok: false, message: `${STATE_FILE_NAME} has no "## Decisions log" heading` };
|
|
155
181
|
return { ok: true, content: b.lines.join(""), changed: a.changed || b.changed };
|
|
156
182
|
}
|
|
157
|
-
/** Pure (
|
|
183
|
+
/** Pure (revise): re-opens every `- [x] … — v<old>: …; deferred to v<newVersion> (…)` item under
|
|
158
184
|
* `## Open Questions` as `- [ ] <original text>`. Items deferred to a later version are untouched. */
|
|
159
185
|
export function reopenDeferred(raw, newVersion) {
|
|
160
186
|
const lines = splitKeepEol(raw);
|
|
@@ -182,9 +208,204 @@ export function reopenDeferred(raw, newVersion) {
|
|
|
182
208
|
}
|
|
183
209
|
return { content: lines.join(""), count };
|
|
184
210
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
211
|
+
// ---- GH #236: the queue for a ratified / shipped doc --------------------------------------
|
|
212
|
+
/** True when the doc's body is a signed spec that defer must not touch: `status: implemented`, or
|
|
213
|
+
* `status: ready-to-implement` with a `ratifiedBy:`. A `draft`/`reviewed` doc — including the
|
|
214
|
+
* next version of a shipped slice — is still being written, so defer edits it as ever. */
|
|
215
|
+
export function isLockedDoc(raw) {
|
|
216
|
+
const range = locateFrontmatterInner(raw);
|
|
217
|
+
if (!range)
|
|
218
|
+
return false;
|
|
219
|
+
const inner = raw.slice(range.innerStart, range.innerEnd);
|
|
220
|
+
const statusMatch = fieldLineRegex("status").exec(inner);
|
|
221
|
+
const status = statusMatch ? normalizeFieldValue(statusMatch[2])?.toLowerCase() : null;
|
|
222
|
+
if (status === "implemented")
|
|
223
|
+
return true;
|
|
224
|
+
if (status !== "ready-to-implement")
|
|
225
|
+
return false;
|
|
226
|
+
const by = fieldLineRegex("ratifiedBy").exec(inner);
|
|
227
|
+
return by !== null && normalizeFieldValue(by[2]) !== null;
|
|
228
|
+
}
|
|
229
|
+
/** Pure: mirrors a QUEUED deferral into the state file — the queued parking-lot bullet and a
|
|
230
|
+
* dated Decisions-log bullet. Idempotent on an identical bullet. */
|
|
231
|
+
export function applyQueueStateFile(raw, input) {
|
|
232
|
+
const eol = raw.includes("\r\n") ? "\r\n" : "\n";
|
|
233
|
+
const target = input.currentVersion + 1;
|
|
234
|
+
const parking = queuedBullet({
|
|
235
|
+
question: input.question,
|
|
236
|
+
sliceKey: input.sliceKey,
|
|
237
|
+
targetVersion: target,
|
|
238
|
+
until: input.until,
|
|
239
|
+
on: input.on,
|
|
240
|
+
...(input.by !== undefined ? { by: input.by.trim() } : {}),
|
|
241
|
+
decision: input.decision.trim(),
|
|
242
|
+
});
|
|
243
|
+
const decision = `- ${input.on}: queued "${input.question}" on ${input.sliceKey} for ${input.sliceKey}@v${target} (until v${input.until}) — ` +
|
|
244
|
+
`v${input.currentVersion}: ${input.decision.trim()}${input.by !== undefined ? ` — by ${input.by.trim()}` : ""}`;
|
|
245
|
+
let lines = splitKeepEol(raw);
|
|
246
|
+
const a = appendBullet(lines, PARKING_LOT_HEADING, parking, eol);
|
|
247
|
+
if (!a)
|
|
248
|
+
return { ok: false, message: `${STATE_FILE_NAME} has no "## Open questions / parking lot" heading` };
|
|
249
|
+
lines = a.lines;
|
|
250
|
+
const b = appendBullet(lines, DECISIONS_HEADING, decision, eol);
|
|
251
|
+
if (!b)
|
|
252
|
+
return { ok: false, message: `${STATE_FILE_NAME} has no "## Decisions log" heading` };
|
|
253
|
+
return { ok: true, content: b.lines.join(""), changed: a.changed || b.changed };
|
|
254
|
+
}
|
|
255
|
+
/** Pure: the Open Questions line a queued item becomes in the v<target> draft. Aimed at the
|
|
256
|
+
* draft's own version it is an unchecked `- [ ]` carrying its decision; aimed further out
|
|
257
|
+
* (`--until` beyond it) it is the ordinary checked deferral, so the draft is not asked to
|
|
258
|
+
* answer what the room already pushed past it. */
|
|
259
|
+
function injectedLine(item) {
|
|
260
|
+
const prior = item.targetVersion - 1;
|
|
261
|
+
const who = `${item.on}${item.by !== undefined ? `, ${item.by}` : ""}`;
|
|
262
|
+
return item.until > item.targetVersion
|
|
263
|
+
? `- [x] ${item.question} — v${prior}: ${item.decision}; deferred to v${item.until} (${who})`
|
|
264
|
+
: `- [ ] ${item.question} — v${prior}: ${item.decision} (queued ${who})`;
|
|
265
|
+
}
|
|
266
|
+
/** Pure (revise / mark-implemented): appends every queued item to `## Open Questions` of the
|
|
267
|
+
* freshly opened draft (creating the section at EOF when the doc has none), skipping a line
|
|
268
|
+
* already present. Returns how many items were written. */
|
|
269
|
+
export function injectQueuedQuestions(raw, items) {
|
|
270
|
+
const eol = raw.includes("\r\n") ? "\r\n" : "\n";
|
|
271
|
+
let content = raw;
|
|
272
|
+
let count = 0;
|
|
273
|
+
for (const item of items) {
|
|
274
|
+
const bullet = injectedLine(item);
|
|
275
|
+
const appended = appendBullet(splitKeepEol(content), OPEN_QUESTIONS_HEADING, bullet, eol);
|
|
276
|
+
if (appended) {
|
|
277
|
+
if (appended.changed)
|
|
278
|
+
count++;
|
|
279
|
+
content = appended.lines.join("");
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (content.length > 0 && !/\r?\n$/.test(content))
|
|
283
|
+
content += eol;
|
|
284
|
+
if (content.length > 0 && !/(\r?\n){2}$/.test(content))
|
|
285
|
+
content += eol;
|
|
286
|
+
content += `## Open Questions${eol}${eol}${bullet}${eol}`;
|
|
287
|
+
count++;
|
|
288
|
+
}
|
|
289
|
+
return { content, count };
|
|
290
|
+
}
|
|
291
|
+
/** Pure: "clears" the queue for `<sliceKey>@v<targetVersion>` in the state file — each queued
|
|
292
|
+
* parking-lot bullet becomes the ordinary `- <q> (from slice <key>, deferred to v<until>)`
|
|
293
|
+
* mirror a draft-doc defer writes. Other slices' and other versions' bullets are untouched. */
|
|
294
|
+
export function drainQueueInState(raw, sliceKey, targetVersion) {
|
|
295
|
+
const lines = splitKeepEol(raw);
|
|
296
|
+
let count = 0;
|
|
297
|
+
for (let j = 0; j < lines.length; j++) {
|
|
298
|
+
const { body, eol } = stripEol(lines[j]);
|
|
299
|
+
const item = parseQueuedBullet(body);
|
|
300
|
+
if (!item || item.sliceKey !== sliceKey || item.targetVersion !== targetVersion)
|
|
301
|
+
continue;
|
|
302
|
+
lines[j] = `- ${item.question} (from slice ${item.sliceKey}, deferred to v${item.until})${eol}`;
|
|
303
|
+
count++;
|
|
304
|
+
}
|
|
305
|
+
return { content: lines.join(""), count };
|
|
306
|
+
}
|
|
307
|
+
/** Reads the queue for `<sliceKey>@v<targetVersion>` from `<baseDir>/.event-modeling.md`; empty
|
|
308
|
+
* when the file is absent. */
|
|
309
|
+
export function readQueue(baseDir, sliceKey, targetVersion) {
|
|
310
|
+
const statePath = join(baseDir, STATE_FILE_NAME);
|
|
311
|
+
if (!existsSync(statePath))
|
|
312
|
+
return { items: [], statePath, stateRaw: null };
|
|
313
|
+
const stateRaw = readFileSync(statePath, "utf8");
|
|
314
|
+
return { items: queuedItems(stateRaw, sliceKey, targetVersion), statePath, stateRaw };
|
|
315
|
+
}
|
|
316
|
+
/** Writes the drained state file once the draft carrying the queue is on disk. */
|
|
317
|
+
export function clearQueue(baseDir, sliceKey, targetVersion) {
|
|
318
|
+
const { stateRaw, statePath } = readQueue(baseDir, sliceKey, targetVersion);
|
|
319
|
+
if (stateRaw === null)
|
|
320
|
+
return;
|
|
321
|
+
const drained = drainQueueInState(stateRaw, sliceKey, targetVersion);
|
|
322
|
+
if (drained.count > 0)
|
|
323
|
+
writeFileSync(statePath, drained.content, "utf8");
|
|
324
|
+
}
|
|
325
|
+
/** Open issues on the slice's own elements whose text contains `text` (case-sensitive substring,
|
|
326
|
+
* the same matching the positional path applies to Open Questions). An issue on a sibling
|
|
327
|
+
* slice's element is never a candidate — the deferral lands on THIS slice's doc. */
|
|
328
|
+
export function findIssueCandidates(model, sliceIndex, text) {
|
|
329
|
+
return model.elements
|
|
330
|
+
.filter((el) => el.sliceIndex === sliceIndex && el.issue !== undefined && el.issue.includes(text))
|
|
331
|
+
.map((el) => ({ kind: el.kind, name: el.name, line: el.line, issue: el.issue }));
|
|
332
|
+
}
|
|
333
|
+
export function formatIssueCandidate(c) {
|
|
334
|
+
return `${c.kind} "${c.name}" :${c.line} issue "${c.issue}"`;
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Pure: removes the one `issue "<issueText>"` clause that belongs to the element declared at
|
|
338
|
+
* `startLine` (1-based). The clause may sit on the declaration line or trail the element's
|
|
339
|
+
* `{ … }` field block (parser.ts's `extractClauses` accepts both), so the scan runs forward from
|
|
340
|
+
* `startLine` and stops at the first line carrying an `issue` clause that decodes to
|
|
341
|
+
* `issueText`. The clause span (plus the one whitespace run before it) is cut out of that line;
|
|
342
|
+
* when nothing but whitespace is left, the whole line goes, EOL included. Everything else —
|
|
343
|
+
* other clauses on the line, every other line, the file's EOL style — is copied through
|
|
344
|
+
* verbatim. The caller re-parses the result before accepting it (`runDeferFromIssue`).
|
|
345
|
+
*/
|
|
346
|
+
export function removeIssueClause(raw, startLine, issueText) {
|
|
347
|
+
const lines = splitKeepEol(raw);
|
|
348
|
+
const keyword = /(?:^|\s)issue\s+(?=")/gi;
|
|
349
|
+
for (let j = Math.max(0, startLine - 1); j < lines.length; j++) {
|
|
350
|
+
const { body, eol } = stripEol(lines[j]);
|
|
351
|
+
keyword.lastIndex = 0;
|
|
352
|
+
let m;
|
|
353
|
+
while ((m = keyword.exec(body)) !== null) {
|
|
354
|
+
const openIdx = m.index + m[0].length;
|
|
355
|
+
const closeIdx = matchQuote(body, openIdx);
|
|
356
|
+
if (closeIdx < 0)
|
|
357
|
+
break; // unterminated — the parser already refused this file
|
|
358
|
+
if (decodeQuoted(body.slice(openIdx + 1, closeIdx)) !== issueText)
|
|
359
|
+
continue;
|
|
360
|
+
const before = body.slice(0, m.index);
|
|
361
|
+
const after = body.slice(closeIdx + 1);
|
|
362
|
+
const rest = (before + after).replace(/[ \t]+$/, "");
|
|
363
|
+
const out = lines.slice();
|
|
364
|
+
if (rest.trim() === "")
|
|
365
|
+
out.splice(j, 1);
|
|
366
|
+
else
|
|
367
|
+
out[j] = rest + eol;
|
|
368
|
+
return { ok: true, content: out.join(""), removedLine: j + 1 };
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return { ok: false, message: `no \`issue "${issueText}"\` clause found at or after line ${startLine}` };
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Pure: records `issueText` on the doc as an already-deferred Open Question. If an unchecked
|
|
375
|
+
* item containing the text already exists (someone wrote it down by hand), that item is deferred
|
|
376
|
+
* in place via `applyDefer` — never a duplicate. Otherwise the `[x]` item is appended to
|
|
377
|
+
* `## Open Questions`, creating the section at EOF when the doc has none (a pre-template doc).
|
|
378
|
+
* An identical item already present → `changed: false`.
|
|
379
|
+
*/
|
|
380
|
+
export function applyDeferFromIssueDoc(raw, issueText, input) {
|
|
381
|
+
const full = { ...input, question: issueText };
|
|
382
|
+
const v = validateInput(full);
|
|
383
|
+
if (!v.ok)
|
|
384
|
+
return v;
|
|
385
|
+
const existing = applyDefer(raw, full);
|
|
386
|
+
if (existing.ok)
|
|
387
|
+
return existing;
|
|
388
|
+
if (!existing.message.startsWith("no unchecked Open Question matching"))
|
|
389
|
+
return existing; // ambiguous / already checked
|
|
390
|
+
const eol = raw.includes("\r\n") ? "\r\n" : "\n";
|
|
391
|
+
const bullet = `- [x] ${issueText} — v${input.currentVersion}: ${input.decision.trim()}; deferred to v${v.until} ` +
|
|
392
|
+
`(${input.on}${input.by !== undefined ? `, ${input.by.trim()}` : ""})`;
|
|
393
|
+
const lines = splitKeepEol(raw);
|
|
394
|
+
const appended = appendBullet(lines, OPEN_QUESTIONS_HEADING, bullet, eol);
|
|
395
|
+
if (appended)
|
|
396
|
+
return { ok: true, content: appended.lines.join(""), changed: appended.changed };
|
|
397
|
+
// No `## Open Questions` section at all: open one at the end of the doc.
|
|
398
|
+
let content = raw;
|
|
399
|
+
if (content.length > 0 && !/\r?\n$/.test(content))
|
|
400
|
+
content += eol;
|
|
401
|
+
if (content.length > 0 && !/(\r?\n){2}$/.test(content))
|
|
402
|
+
content += eol;
|
|
403
|
+
content += `## Open Questions${eol}${eol}${bullet}${eol}`;
|
|
404
|
+
return { ok: true, content, changed: true };
|
|
405
|
+
}
|
|
406
|
+
/** The doc resolution both paths share (same join as ratify/revise): continuation and
|
|
407
|
+
* binding refusals, frontmatter validity, the doc's current version. */
|
|
408
|
+
function resolveDeferDoc(model, refs, baseDir, sliceKey) {
|
|
188
409
|
const sliceIndex = refs.sliceKeys.indexOf(sliceKey);
|
|
189
410
|
if (sliceIndex === -1)
|
|
190
411
|
return { ok: false, message: `no slice with export key "${sliceKey}" in this model` };
|
|
@@ -214,31 +435,143 @@ export function runDefer(model, refs, baseDir, sliceKey, input) {
|
|
|
214
435
|
message: `slice doc "${doc.path}" has missing or invalid frontmatter — run \`em validate\` for details`,
|
|
215
436
|
};
|
|
216
437
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
return { ok: false, message: `${doc.path}: ${result.message}` };
|
|
438
|
+
return { ok: true, sliceIndex, path: doc.path, version: doc.version };
|
|
439
|
+
}
|
|
440
|
+
/** The state-file mirror both paths share; refuses (never creates) when the file is absent. */
|
|
441
|
+
function mirrorIntoStateFile(baseDir, sliceKey, question, input, currentVersion, queue = false) {
|
|
222
442
|
const statePath = join(baseDir, STATE_FILE_NAME);
|
|
223
443
|
if (!existsSync(statePath)) {
|
|
224
444
|
return { ok: false, message: `no ${STATE_FILE_NAME} beside the model — defer mirrors into it and never creates it` };
|
|
225
445
|
}
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
question: input.question,
|
|
446
|
+
const r = (queue ? applyQueueStateFile : applyDeferStateFile)(readFileSync(statePath, "utf8"), {
|
|
447
|
+
question,
|
|
229
448
|
sliceKey,
|
|
230
|
-
until,
|
|
449
|
+
until: parseUntil(input.until),
|
|
231
450
|
decision: input.decision,
|
|
232
451
|
by: input.by,
|
|
233
452
|
on: input.on,
|
|
234
|
-
currentVersion
|
|
453
|
+
currentVersion,
|
|
235
454
|
});
|
|
236
|
-
if (!
|
|
237
|
-
return
|
|
455
|
+
if (!r.ok)
|
|
456
|
+
return r;
|
|
457
|
+
return { ok: true, path: statePath, content: r.content, changed: r.changed };
|
|
458
|
+
}
|
|
459
|
+
/** Resolves `sliceKey` to its bound doc (same join as ratify/revise), computes both edits, and
|
|
460
|
+
* writes only when every precondition holds (the state file is required and never created). */
|
|
461
|
+
export function runDefer(model, refs, baseDir, sliceKey, input) {
|
|
462
|
+
const doc = resolveDeferDoc(model, refs, baseDir, sliceKey);
|
|
463
|
+
if (!doc.ok)
|
|
464
|
+
return doc;
|
|
465
|
+
const absDoc = join(baseDir, doc.path);
|
|
466
|
+
const docRaw = readFileSync(absDoc, "utf8");
|
|
467
|
+
// GH #236: a ratified / shipped doc is not edited — the question is queued for v<N+1>.
|
|
468
|
+
if (isLockedDoc(docRaw)) {
|
|
469
|
+
const v = validateInput({ ...input, currentVersion: doc.version });
|
|
470
|
+
if (!v.ok)
|
|
471
|
+
return { ok: false, message: `${doc.path}: ${v.message}` };
|
|
472
|
+
const state = mirrorIntoStateFile(baseDir, sliceKey, input.question, input, doc.version, true);
|
|
473
|
+
if (!state.ok)
|
|
474
|
+
return state;
|
|
475
|
+
if (state.changed)
|
|
476
|
+
writeFileSync(state.path, state.content, "utf8");
|
|
477
|
+
return { ok: true, path: doc.path, changed: state.changed, queued: { forVersion: doc.version + 1, docVersion: doc.version } };
|
|
478
|
+
}
|
|
479
|
+
const result = applyDefer(docRaw, { ...input, currentVersion: doc.version });
|
|
480
|
+
if (!result.ok)
|
|
481
|
+
return { ok: false, message: `${doc.path}: ${result.message}` };
|
|
482
|
+
const state = mirrorIntoStateFile(baseDir, sliceKey, input.question, input, doc.version);
|
|
483
|
+
if (!state.ok)
|
|
484
|
+
return state;
|
|
238
485
|
if (result.changed)
|
|
239
486
|
writeFileSync(absDoc, result.content, "utf8");
|
|
240
|
-
if (
|
|
241
|
-
writeFileSync(
|
|
242
|
-
return { ok: true, path: doc.path, changed: result.changed ||
|
|
487
|
+
if (state.changed)
|
|
488
|
+
writeFileSync(state.path, state.content, "utf8");
|
|
489
|
+
return { ok: true, path: doc.path, changed: result.changed || state.changed, queued: null };
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* `--from-issue <text>` (MIL-281): the single open `issue "..."` on one of the slice's own
|
|
493
|
+
* elements whose text contains `text` becomes a deferred Open Question on the doc, is mirrored
|
|
494
|
+
* into the state file, and its clause is removed from `modelFile`. All three edits are computed
|
|
495
|
+
* first; nothing is written unless every one succeeds. Zero candidates with the question already
|
|
496
|
+
* deferred on the doc → the idempotent no-op; zero otherwise → refuse; two or more → refuse
|
|
497
|
+
* naming each with its `.em` line.
|
|
498
|
+
*/
|
|
499
|
+
export function runDeferFromIssue(model, refs, baseDir, modelFile, sliceKey, text, input) {
|
|
500
|
+
if (text.trim() === "")
|
|
501
|
+
return { ok: false, message: "--from-issue needs the issue's text (a case-sensitive substring)" };
|
|
502
|
+
const doc = resolveDeferDoc(model, refs, baseDir, sliceKey);
|
|
503
|
+
if (!doc.ok)
|
|
504
|
+
return doc;
|
|
505
|
+
const absDoc = join(baseDir, doc.path);
|
|
506
|
+
const docRaw = readFileSync(absDoc, "utf8");
|
|
507
|
+
const locked = isLockedDoc(docRaw);
|
|
508
|
+
const queued = locked ? { forVersion: doc.version + 1, docVersion: doc.version } : null;
|
|
509
|
+
const candidates = findIssueCandidates(model, doc.sliceIndex, text);
|
|
510
|
+
if (candidates.length === 0) {
|
|
511
|
+
// Already promoted on an earlier run? Then the doc carries it as deferred to this version —
|
|
512
|
+
// or, on a ratified/shipped doc (GH #236), the state file carries it as queued.
|
|
513
|
+
if (locked) {
|
|
514
|
+
if (readQueue(baseDir, sliceKey, doc.version + 1).items.some((q) => q.question.includes(text))) {
|
|
515
|
+
return { ok: true, path: doc.path, changed: false, issue: text, modelLine: null, queued };
|
|
516
|
+
}
|
|
517
|
+
return { ok: false, message: `no open issue matching "${text}" on slice "${sliceKey}" (see \`em validate --list-issues\`)` };
|
|
518
|
+
}
|
|
519
|
+
const probe = applyDefer(docRaw, { ...input, question: text, currentVersion: doc.version });
|
|
520
|
+
if (probe.ok && !probe.changed)
|
|
521
|
+
return { ok: true, path: doc.path, changed: false, issue: text, modelLine: null, queued: null };
|
|
522
|
+
if (probe.ok) {
|
|
523
|
+
// The question is on the doc as a hand-written `- [ ]` item, never on the diagram: that is
|
|
524
|
+
// the positional form's job.
|
|
525
|
+
return {
|
|
526
|
+
ok: false,
|
|
527
|
+
message: `no open issue matching "${text}" on slice "${sliceKey}" — but ${doc.path} has an unchecked Open Question ` +
|
|
528
|
+
`containing it; defer that with the positional form: em slice defer <model> ${sliceKey} "${text}" --until ${input.until} --decision "..."`,
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
return { ok: false, message: `no open issue matching "${text}" on slice "${sliceKey}" (see \`em validate --list-issues\`)` };
|
|
532
|
+
}
|
|
533
|
+
if (candidates.length > 1) {
|
|
534
|
+
return { ok: false, message: `ambiguous — matches: ${candidates.map(formatIssueCandidate).join(" | ")}` };
|
|
535
|
+
}
|
|
536
|
+
const [cand] = candidates;
|
|
537
|
+
// GH #236: on a ratified/shipped doc the promoted issue is queued, not written into the body.
|
|
538
|
+
const docResult = locked
|
|
539
|
+
? (() => {
|
|
540
|
+
const v = validateInput({ ...input, question: cand.issue, currentVersion: doc.version });
|
|
541
|
+
return v.ok ? { ok: true, content: docRaw, changed: false } : v;
|
|
542
|
+
})()
|
|
543
|
+
: applyDeferFromIssueDoc(docRaw, cand.issue, { ...input, currentVersion: doc.version });
|
|
544
|
+
if (!docResult.ok)
|
|
545
|
+
return { ok: false, message: `${doc.path}: ${docResult.message}` };
|
|
546
|
+
const state = mirrorIntoStateFile(baseDir, sliceKey, cand.issue, input, doc.version, locked);
|
|
547
|
+
if (!state.ok)
|
|
548
|
+
return state;
|
|
549
|
+
const emRaw = readFileSync(modelFile, "utf8");
|
|
550
|
+
const removed = removeIssueClause(emRaw, cand.line, cand.issue);
|
|
551
|
+
if (!removed.ok)
|
|
552
|
+
return { ok: false, message: `${modelFile}: ${removed.message}` };
|
|
553
|
+
// Verify before write: the edited model must still compile, and this issue must be gone from
|
|
554
|
+
// the slice — the same discipline `em migrate` holds for its own `.em` rewrite. (The candidate
|
|
555
|
+
// was the slice's only issue containing its own text — a second one would have been ambiguous
|
|
556
|
+
// above — so "gone" is simply "no element of the slice still carries it".)
|
|
557
|
+
let clean;
|
|
558
|
+
try {
|
|
559
|
+
const after = compile(removed.content);
|
|
560
|
+
clean =
|
|
561
|
+
!after.diagnostics.some((d) => d.severity === "error") &&
|
|
562
|
+
!after.model.elements.some((el) => el.sliceIndex === doc.sliceIndex && el.issue === cand.issue);
|
|
563
|
+
}
|
|
564
|
+
catch {
|
|
565
|
+
clean = false;
|
|
566
|
+
}
|
|
567
|
+
if (!clean) {
|
|
568
|
+
return { ok: false, message: `${modelFile}: removing the issue clause at line ${removed.removedLine} would not leave a clean model — nothing written` };
|
|
569
|
+
}
|
|
570
|
+
if (docResult.changed)
|
|
571
|
+
writeFileSync(absDoc, docResult.content, "utf8");
|
|
572
|
+
if (state.changed)
|
|
573
|
+
writeFileSync(state.path, state.content, "utf8");
|
|
574
|
+
writeFileSync(modelFile, removed.content, "utf8");
|
|
575
|
+
return { ok: true, path: doc.path, changed: true, issue: cand.issue, modelLine: removed.removedLine, queued };
|
|
243
576
|
}
|
|
244
577
|
//# sourceMappingURL=defer.js.map
|