dld-kit 1.0.0-rc.4 → 1.0.0-rc.5
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 +4 -2
- package/dist/dld.mjs +2434 -1650
- package/package.json +1 -1
- package/skills/dld-adjust/SKILL.md +41 -24
- package/skills/dld-audit/SKILL.md +11 -4
- package/skills/dld-audit-auto/SKILL.md +20 -5
- package/skills/dld-common/SKILL.md +1 -1
- package/skills/dld-common/scripts/dld.mjs +2434 -1650
- package/skills/dld-decide/SKILL.md +21 -5
- package/skills/dld-implement/SKILL.md +28 -7
- package/skills/dld-init/SKILL.md +2 -2
- package/skills/dld-lookup/SKILL.md +27 -5
- package/skills/dld-plan/SKILL.md +21 -5
- package/skills/dld-reindex/SKILL.md +4 -2
- package/skills/dld-retrofit/SKILL.md +2 -2
- package/skills/dld-snapshot/SKILL.md +49 -18
- package/skills/dld-status/SKILL.md +1 -1
- package/templates/rules/dld-workflow.md +1 -0
- package/templates/skills/dld-adjust/SKILL.md +40 -23
- package/templates/skills/dld-audit/SKILL.md +10 -3
- package/templates/skills/dld-audit-auto/SKILL.md +19 -4
- package/templates/skills/dld-decide/SKILL.md +20 -4
- package/templates/skills/dld-implement/SKILL.md +27 -6
- package/templates/skills/dld-init/SKILL.md +1 -1
- package/templates/skills/dld-lookup/SKILL.md +26 -4
- package/templates/skills/dld-plan/SKILL.md +20 -4
- package/templates/skills/dld-reindex/SKILL.md +3 -1
- package/templates/skills/dld-retrofit/SKILL.md +1 -1
- package/templates/skills/dld-snapshot/SKILL.md +48 -17
package/dist/dld.mjs
CHANGED
|
@@ -4000,10 +4000,10 @@ var require_resolve_block_map = __commonJS({
|
|
|
4000
4000
|
let offset = bm.offset;
|
|
4001
4001
|
let commentEnd = null;
|
|
4002
4002
|
for (const collItem of bm.items) {
|
|
4003
|
-
const { start, key, sep:
|
|
4003
|
+
const { start, key, sep: sep9, value } = collItem;
|
|
4004
4004
|
const keyProps = resolveProps.resolveProps(start, {
|
|
4005
4005
|
indicator: "explicit-key-ind",
|
|
4006
|
-
next: key ??
|
|
4006
|
+
next: key ?? sep9?.[0],
|
|
4007
4007
|
offset,
|
|
4008
4008
|
onError,
|
|
4009
4009
|
parentIndent: bm.indent,
|
|
@@ -4017,7 +4017,7 @@ var require_resolve_block_map = __commonJS({
|
|
|
4017
4017
|
else if ("indent" in key && key.indent !== bm.indent)
|
|
4018
4018
|
onError(offset, "BAD_INDENT", startColMsg);
|
|
4019
4019
|
}
|
|
4020
|
-
if (!keyProps.anchor && !keyProps.tag && !
|
|
4020
|
+
if (!keyProps.anchor && !keyProps.tag && !sep9) {
|
|
4021
4021
|
commentEnd = keyProps.end;
|
|
4022
4022
|
if (keyProps.comment) {
|
|
4023
4023
|
if (map.comment)
|
|
@@ -4041,7 +4041,7 @@ var require_resolve_block_map = __commonJS({
|
|
|
4041
4041
|
ctx.atKey = false;
|
|
4042
4042
|
if (utilMapIncludes.mapIncludes(ctx, map.items, keyNode))
|
|
4043
4043
|
onError(keyStart, "DUPLICATE_KEY", "Map keys must be unique");
|
|
4044
|
-
const valueProps = resolveProps.resolveProps(
|
|
4044
|
+
const valueProps = resolveProps.resolveProps(sep9 ?? [], {
|
|
4045
4045
|
indicator: "map-value-ind",
|
|
4046
4046
|
next: value,
|
|
4047
4047
|
offset: keyNode.range[2],
|
|
@@ -4057,7 +4057,7 @@ var require_resolve_block_map = __commonJS({
|
|
|
4057
4057
|
if (ctx.options.strict && keyProps.start < valueProps.found.offset - 1024)
|
|
4058
4058
|
onError(keyNode.range, "KEY_OVER_1024_CHARS", "The : indicator must be at most 1024 chars after the start of an implicit block mapping key");
|
|
4059
4059
|
}
|
|
4060
|
-
const valueNode = value ? composeNode(ctx, value, valueProps, onError) : composeEmptyNode(ctx, offset,
|
|
4060
|
+
const valueNode = value ? composeNode(ctx, value, valueProps, onError) : composeEmptyNode(ctx, offset, sep9, null, valueProps, onError);
|
|
4061
4061
|
if (ctx.schema.compat)
|
|
4062
4062
|
utilFlowIndentCheck.flowIndentCheck(bm.indent, value, onError);
|
|
4063
4063
|
offset = valueNode.range[2];
|
|
@@ -4148,7 +4148,7 @@ var require_resolve_end = __commonJS({
|
|
|
4148
4148
|
let comment = "";
|
|
4149
4149
|
if (end) {
|
|
4150
4150
|
let hasSpace = false;
|
|
4151
|
-
let
|
|
4151
|
+
let sep9 = "";
|
|
4152
4152
|
for (const token of end) {
|
|
4153
4153
|
const { source, type } = token;
|
|
4154
4154
|
switch (type) {
|
|
@@ -4162,13 +4162,13 @@ var require_resolve_end = __commonJS({
|
|
|
4162
4162
|
if (!comment)
|
|
4163
4163
|
comment = cb;
|
|
4164
4164
|
else
|
|
4165
|
-
comment +=
|
|
4166
|
-
|
|
4165
|
+
comment += sep9 + cb;
|
|
4166
|
+
sep9 = "";
|
|
4167
4167
|
break;
|
|
4168
4168
|
}
|
|
4169
4169
|
case "newline":
|
|
4170
4170
|
if (comment)
|
|
4171
|
-
|
|
4171
|
+
sep9 += source;
|
|
4172
4172
|
hasSpace = true;
|
|
4173
4173
|
break;
|
|
4174
4174
|
default:
|
|
@@ -4211,18 +4211,18 @@ var require_resolve_flow_collection = __commonJS({
|
|
|
4211
4211
|
let offset = fc.offset + fc.start.source.length;
|
|
4212
4212
|
for (let i = 0; i < fc.items.length; ++i) {
|
|
4213
4213
|
const collItem = fc.items[i];
|
|
4214
|
-
const { start, key, sep:
|
|
4214
|
+
const { start, key, sep: sep9, value } = collItem;
|
|
4215
4215
|
const props = resolveProps.resolveProps(start, {
|
|
4216
4216
|
flow: fcName,
|
|
4217
4217
|
indicator: "explicit-key-ind",
|
|
4218
|
-
next: key ??
|
|
4218
|
+
next: key ?? sep9?.[0],
|
|
4219
4219
|
offset,
|
|
4220
4220
|
onError,
|
|
4221
4221
|
parentIndent: fc.indent,
|
|
4222
4222
|
startOnNewline: false
|
|
4223
4223
|
});
|
|
4224
4224
|
if (!props.found) {
|
|
4225
|
-
if (!props.anchor && !props.tag && !
|
|
4225
|
+
if (!props.anchor && !props.tag && !sep9 && !value) {
|
|
4226
4226
|
if (i === 0 && props.comma)
|
|
4227
4227
|
onError(props.comma, "UNEXPECTED_TOKEN", `Unexpected , in ${fcName}`);
|
|
4228
4228
|
else if (i < fc.items.length - 1)
|
|
@@ -4276,8 +4276,8 @@ var require_resolve_flow_collection = __commonJS({
|
|
|
4276
4276
|
}
|
|
4277
4277
|
}
|
|
4278
4278
|
}
|
|
4279
|
-
if (!isMap2 && !
|
|
4280
|
-
const valueNode = value ? composeNode(ctx, value, props, onError) : composeEmptyNode(ctx, props.end,
|
|
4279
|
+
if (!isMap2 && !sep9 && !props.found) {
|
|
4280
|
+
const valueNode = value ? composeNode(ctx, value, props, onError) : composeEmptyNode(ctx, props.end, sep9, null, props, onError);
|
|
4281
4281
|
coll.items.push(valueNode);
|
|
4282
4282
|
offset = valueNode.range[2];
|
|
4283
4283
|
if (isBlock(value))
|
|
@@ -4289,7 +4289,7 @@ var require_resolve_flow_collection = __commonJS({
|
|
|
4289
4289
|
if (isBlock(key))
|
|
4290
4290
|
onError(keyNode.range, "BLOCK_IN_FLOW", blockMsg);
|
|
4291
4291
|
ctx.atKey = false;
|
|
4292
|
-
const valueProps = resolveProps.resolveProps(
|
|
4292
|
+
const valueProps = resolveProps.resolveProps(sep9 ?? [], {
|
|
4293
4293
|
flow: fcName,
|
|
4294
4294
|
indicator: "map-value-ind",
|
|
4295
4295
|
next: value,
|
|
@@ -4300,8 +4300,8 @@ var require_resolve_flow_collection = __commonJS({
|
|
|
4300
4300
|
});
|
|
4301
4301
|
if (valueProps.found) {
|
|
4302
4302
|
if (!isMap2 && !props.found && ctx.options.strict) {
|
|
4303
|
-
if (
|
|
4304
|
-
for (const st of
|
|
4303
|
+
if (sep9)
|
|
4304
|
+
for (const st of sep9) {
|
|
4305
4305
|
if (st === valueProps.found)
|
|
4306
4306
|
break;
|
|
4307
4307
|
if (st.type === "newline") {
|
|
@@ -4318,7 +4318,7 @@ var require_resolve_flow_collection = __commonJS({
|
|
|
4318
4318
|
else
|
|
4319
4319
|
onError(valueProps.start, "MISSING_CHAR", `Missing , or : between ${fcName} items`);
|
|
4320
4320
|
}
|
|
4321
|
-
const valueNode = value ? composeNode(ctx, value, valueProps, onError) : valueProps.found ? composeEmptyNode(ctx, valueProps.end,
|
|
4321
|
+
const valueNode = value ? composeNode(ctx, value, valueProps, onError) : valueProps.found ? composeEmptyNode(ctx, valueProps.end, sep9, null, valueProps, onError) : null;
|
|
4322
4322
|
if (valueNode) {
|
|
4323
4323
|
if (isBlock(value))
|
|
4324
4324
|
onError(valueNode.range, "BLOCK_IN_FLOW", blockMsg);
|
|
@@ -4498,7 +4498,7 @@ var require_resolve_block_scalar = __commonJS({
|
|
|
4498
4498
|
chompStart = i + 1;
|
|
4499
4499
|
}
|
|
4500
4500
|
let value = "";
|
|
4501
|
-
let
|
|
4501
|
+
let sep9 = "";
|
|
4502
4502
|
let prevMoreIndented = false;
|
|
4503
4503
|
for (let i = 0; i < contentStart; ++i)
|
|
4504
4504
|
value += lines[i][0].slice(trimIndent) + "\n";
|
|
@@ -4515,24 +4515,24 @@ var require_resolve_block_scalar = __commonJS({
|
|
|
4515
4515
|
indent = "";
|
|
4516
4516
|
}
|
|
4517
4517
|
if (type === Scalar.Scalar.BLOCK_LITERAL) {
|
|
4518
|
-
value +=
|
|
4519
|
-
|
|
4518
|
+
value += sep9 + indent.slice(trimIndent) + content;
|
|
4519
|
+
sep9 = "\n";
|
|
4520
4520
|
} else if (indent.length > trimIndent || content[0] === " ") {
|
|
4521
|
-
if (
|
|
4522
|
-
|
|
4523
|
-
else if (!prevMoreIndented &&
|
|
4524
|
-
|
|
4525
|
-
value +=
|
|
4526
|
-
|
|
4521
|
+
if (sep9 === " ")
|
|
4522
|
+
sep9 = "\n";
|
|
4523
|
+
else if (!prevMoreIndented && sep9 === "\n")
|
|
4524
|
+
sep9 = "\n\n";
|
|
4525
|
+
value += sep9 + indent.slice(trimIndent) + content;
|
|
4526
|
+
sep9 = "\n";
|
|
4527
4527
|
prevMoreIndented = true;
|
|
4528
4528
|
} else if (content === "") {
|
|
4529
|
-
if (
|
|
4529
|
+
if (sep9 === "\n")
|
|
4530
4530
|
value += "\n";
|
|
4531
4531
|
else
|
|
4532
|
-
|
|
4532
|
+
sep9 = "\n";
|
|
4533
4533
|
} else {
|
|
4534
|
-
value +=
|
|
4535
|
-
|
|
4534
|
+
value += sep9 + content;
|
|
4535
|
+
sep9 = " ";
|
|
4536
4536
|
prevMoreIndented = false;
|
|
4537
4537
|
}
|
|
4538
4538
|
}
|
|
@@ -4715,25 +4715,25 @@ var require_resolve_flow_scalar = __commonJS({
|
|
|
4715
4715
|
trimBoth = /^[ \t]+|[ \t]+$/g;
|
|
4716
4716
|
}
|
|
4717
4717
|
let res = match[1].replace(trimEnd, "");
|
|
4718
|
-
let
|
|
4718
|
+
let sep9 = " ";
|
|
4719
4719
|
let pos = line.lastIndex;
|
|
4720
4720
|
while (match = line.exec(source)) {
|
|
4721
4721
|
const lm = match[1].replace(trimBoth, "");
|
|
4722
4722
|
if (lm === "") {
|
|
4723
|
-
if (
|
|
4724
|
-
res +=
|
|
4723
|
+
if (sep9 === "\n")
|
|
4724
|
+
res += sep9;
|
|
4725
4725
|
else
|
|
4726
|
-
|
|
4726
|
+
sep9 = "\n";
|
|
4727
4727
|
} else {
|
|
4728
|
-
res +=
|
|
4729
|
-
|
|
4728
|
+
res += sep9 + lm;
|
|
4729
|
+
sep9 = " ";
|
|
4730
4730
|
}
|
|
4731
4731
|
pos = line.lastIndex;
|
|
4732
4732
|
}
|
|
4733
4733
|
const last = /[ \t]*(.*)/sy;
|
|
4734
4734
|
last.lastIndex = pos;
|
|
4735
4735
|
match = last.exec(source);
|
|
4736
|
-
return res +
|
|
4736
|
+
return res + sep9 + (match?.[1] ?? "");
|
|
4737
4737
|
}
|
|
4738
4738
|
function doubleQuotedValue(source, onError) {
|
|
4739
4739
|
let res = "";
|
|
@@ -5543,14 +5543,14 @@ var require_cst_stringify = __commonJS({
|
|
|
5543
5543
|
}
|
|
5544
5544
|
}
|
|
5545
5545
|
}
|
|
5546
|
-
function stringifyItem({ start, key, sep:
|
|
5546
|
+
function stringifyItem({ start, key, sep: sep9, value }) {
|
|
5547
5547
|
let res = "";
|
|
5548
5548
|
for (const st of start)
|
|
5549
5549
|
res += st.source;
|
|
5550
5550
|
if (key)
|
|
5551
5551
|
res += stringifyToken(key);
|
|
5552
|
-
if (
|
|
5553
|
-
for (const st of
|
|
5552
|
+
if (sep9)
|
|
5553
|
+
for (const st of sep9)
|
|
5554
5554
|
res += st.source;
|
|
5555
5555
|
if (value)
|
|
5556
5556
|
res += stringifyToken(value);
|
|
@@ -6717,18 +6717,18 @@ var require_parser = __commonJS({
|
|
|
6717
6717
|
if (this.type === "map-value-ind") {
|
|
6718
6718
|
const prev = getPrevProps(this.peek(2));
|
|
6719
6719
|
const start = getFirstKeyStartProps(prev);
|
|
6720
|
-
let
|
|
6720
|
+
let sep9;
|
|
6721
6721
|
if (scalar.end) {
|
|
6722
|
-
|
|
6723
|
-
|
|
6722
|
+
sep9 = scalar.end;
|
|
6723
|
+
sep9.push(this.sourceToken);
|
|
6724
6724
|
delete scalar.end;
|
|
6725
6725
|
} else
|
|
6726
|
-
|
|
6726
|
+
sep9 = [this.sourceToken];
|
|
6727
6727
|
const map = {
|
|
6728
6728
|
type: "block-map",
|
|
6729
6729
|
offset: scalar.offset,
|
|
6730
6730
|
indent: scalar.indent,
|
|
6731
|
-
items: [{ start, key: scalar, sep:
|
|
6731
|
+
items: [{ start, key: scalar, sep: sep9 }]
|
|
6732
6732
|
};
|
|
6733
6733
|
this.onKeyLine = true;
|
|
6734
6734
|
this.stack[this.stack.length - 1] = map;
|
|
@@ -6881,15 +6881,15 @@ var require_parser = __commonJS({
|
|
|
6881
6881
|
} else if (isFlowToken(it.key) && !includesToken(it.sep, "newline")) {
|
|
6882
6882
|
const start2 = getFirstKeyStartProps(it.start);
|
|
6883
6883
|
const key = it.key;
|
|
6884
|
-
const
|
|
6885
|
-
|
|
6884
|
+
const sep9 = it.sep;
|
|
6885
|
+
sep9.push(this.sourceToken);
|
|
6886
6886
|
delete it.key;
|
|
6887
6887
|
delete it.sep;
|
|
6888
6888
|
this.stack.push({
|
|
6889
6889
|
type: "block-map",
|
|
6890
6890
|
offset: this.offset,
|
|
6891
6891
|
indent: this.indent,
|
|
6892
|
-
items: [{ start: start2, key, sep:
|
|
6892
|
+
items: [{ start: start2, key, sep: sep9 }]
|
|
6893
6893
|
});
|
|
6894
6894
|
} else if (start.length > 0) {
|
|
6895
6895
|
it.sep = it.sep.concat(start, this.sourceToken);
|
|
@@ -7083,13 +7083,13 @@ var require_parser = __commonJS({
|
|
|
7083
7083
|
const prev = getPrevProps(parent);
|
|
7084
7084
|
const start = getFirstKeyStartProps(prev);
|
|
7085
7085
|
fixFlowSeqItems(fc);
|
|
7086
|
-
const
|
|
7087
|
-
|
|
7086
|
+
const sep9 = fc.end.splice(1, fc.end.length);
|
|
7087
|
+
sep9.push(this.sourceToken);
|
|
7088
7088
|
const map = {
|
|
7089
7089
|
type: "block-map",
|
|
7090
7090
|
offset: fc.offset,
|
|
7091
7091
|
indent: fc.indent,
|
|
7092
|
-
items: [{ start, key: fc, sep:
|
|
7092
|
+
items: [{ start, key: fc, sep: sep9 }]
|
|
7093
7093
|
};
|
|
7094
7094
|
this.onKeyLine = true;
|
|
7095
7095
|
this.stack[this.stack.length - 1] = map;
|
|
@@ -7372,10 +7372,11 @@ import { createInterface } from "node:readline/promises";
|
|
|
7372
7372
|
import { fileURLToPath } from "node:url";
|
|
7373
7373
|
|
|
7374
7374
|
// package.json
|
|
7375
|
-
var version = "1.0.0-rc.
|
|
7375
|
+
var version = "1.0.0-rc.5";
|
|
7376
7376
|
|
|
7377
7377
|
// src/core/errors.ts
|
|
7378
7378
|
var EXIT_ERROR = 1;
|
|
7379
|
+
var EXIT_USAGE = 2;
|
|
7379
7380
|
var DldError = class extends Error {
|
|
7380
7381
|
exitCode;
|
|
7381
7382
|
constructor(message, exitCode = EXIT_ERROR) {
|
|
@@ -7423,7 +7424,6 @@ var GhCommandError = class extends DldError {
|
|
|
7423
7424
|
// src/cli/command.ts
|
|
7424
7425
|
import { parseArgs } from "node:util";
|
|
7425
7426
|
var EXIT_OK = 0;
|
|
7426
|
-
var EXIT_USAGE = 2;
|
|
7427
7427
|
var UsageError = class extends DldError {
|
|
7428
7428
|
constructor(message) {
|
|
7429
7429
|
super(message, EXIT_USAGE);
|
|
@@ -7455,121 +7455,46 @@ function requestsHelp(config) {
|
|
|
7455
7455
|
return tokens.some((token) => token.kind === "option" && token.name === "help");
|
|
7456
7456
|
}
|
|
7457
7457
|
|
|
7458
|
-
// src/core/
|
|
7459
|
-
import { join as
|
|
7458
|
+
// src/core/decision-edits.ts
|
|
7459
|
+
import { basename as basename4, join as join3, relative as relative3, sep as sep2 } from "node:path";
|
|
7460
7460
|
|
|
7461
|
-
// src/core/
|
|
7462
|
-
|
|
7463
|
-
|
|
7464
|
-
|
|
7465
|
-
|
|
7466
|
-
|
|
7467
|
-
if (!ctx.fs.exists(path)) {
|
|
7468
|
-
throw new DldError(`${CONFIG_FILE} not found. Run /dld-init first.`);
|
|
7469
|
-
}
|
|
7470
|
-
return parseConfig(ctx.fs.readFile(path));
|
|
7471
|
-
}
|
|
7472
|
-
function parseConfig(text) {
|
|
7473
|
-
const doc = (0, import_yaml.parseDocument)(text, { logLevel: "silent" });
|
|
7474
|
-
const problem = doc.errors[0] ?? doc.warnings[0];
|
|
7475
|
-
if (problem !== void 0) throw invalid(`not valid YAML: ${problem.message}`);
|
|
7476
|
-
const raw = doc.toJS();
|
|
7477
|
-
if (!isRecord(raw)) throw invalid("expected a mapping of keys to values");
|
|
7478
|
-
const mode = raw.mode;
|
|
7479
|
-
if (mode !== "flat" && mode !== "namespaced") {
|
|
7480
|
-
throw invalid("'mode' must be 'flat' or 'namespaced'");
|
|
7481
|
-
}
|
|
7482
|
-
const namespaces = optionalStringList(raw.namespaces, "namespaces");
|
|
7483
|
-
if (mode === "namespaced" && namespaces.length === 0) {
|
|
7484
|
-
throw invalid("'namespaces' must list at least one namespace when 'mode' is 'namespaced'");
|
|
7485
|
-
}
|
|
7486
|
-
return {
|
|
7487
|
-
decisionsDir: requiredString(raw.decisions_dir, "decisions_dir"),
|
|
7488
|
-
mode,
|
|
7489
|
-
namespaces,
|
|
7490
|
-
annotationPrefix: optionalString(raw.annotation_prefix, "annotation_prefix") ?? "@decision",
|
|
7491
|
-
// @decision(DL-023)
|
|
7492
|
-
annotationExclude: repoRelativePatterns(raw.annotation_exclude, "annotation_exclude"),
|
|
7493
|
-
implementReview: optionalBoolean(raw.implement_review, "implement_review") ?? true,
|
|
7494
|
-
snapshotArtifacts: snapshotArtifacts(raw.snapshot_artifacts)
|
|
7495
|
-
};
|
|
7496
|
-
}
|
|
7497
|
-
function invalid(message) {
|
|
7498
|
-
return new DldError(`${CONFIG_FILE}: ${message}`);
|
|
7499
|
-
}
|
|
7500
|
-
function isRecord(value) {
|
|
7501
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7502
|
-
}
|
|
7503
|
-
function requiredString(value, key) {
|
|
7504
|
-
const result = optionalString(value, key);
|
|
7505
|
-
if (result === void 0) throw invalid(`'${key}' is required`);
|
|
7506
|
-
return result;
|
|
7507
|
-
}
|
|
7508
|
-
function optionalString(value, key) {
|
|
7509
|
-
if (value === void 0 || value === null) return void 0;
|
|
7510
|
-
if (typeof value !== "string" || value.trim() === "") {
|
|
7511
|
-
throw invalid(`'${key}' must be a non-empty string`);
|
|
7512
|
-
}
|
|
7513
|
-
return value;
|
|
7514
|
-
}
|
|
7515
|
-
function optionalBoolean(value, key) {
|
|
7516
|
-
if (value === void 0 || value === null) return void 0;
|
|
7517
|
-
if (typeof value !== "boolean") throw invalid(`'${key}' must be true or false`);
|
|
7518
|
-
return value;
|
|
7519
|
-
}
|
|
7520
|
-
function optionalStringList(value, key) {
|
|
7521
|
-
if (value === void 0 || value === null) return [];
|
|
7522
|
-
if (!Array.isArray(value)) throw invalid(`'${key}' must be a list of strings`);
|
|
7523
|
-
return value.map((item) => {
|
|
7524
|
-
if (typeof item !== "string" || item.trim() === "") {
|
|
7525
|
-
throw invalid(`'${key}' must be a list of non-empty strings`);
|
|
7526
|
-
}
|
|
7527
|
-
return item;
|
|
7528
|
-
});
|
|
7461
|
+
// src/core/files.ts
|
|
7462
|
+
import { basename, dirname, join } from "node:path";
|
|
7463
|
+
var TEMP_PREFIX = ".dld-tmp-";
|
|
7464
|
+
function tempPathFor(path) {
|
|
7465
|
+
const suffix = Math.random().toString(36).slice(2, 10);
|
|
7466
|
+
return join(dirname(path), `${TEMP_PREFIX}${basename(path)}.${suffix}`);
|
|
7529
7467
|
}
|
|
7530
|
-
function
|
|
7531
|
-
const
|
|
7532
|
-
|
|
7533
|
-
|
|
7534
|
-
|
|
7468
|
+
function writeFileAtomic(ctx, path, content, mode) {
|
|
7469
|
+
const temp = tempPathFor(path);
|
|
7470
|
+
try {
|
|
7471
|
+
ctx.fs.writeFile(temp, content);
|
|
7472
|
+
if (mode !== void 0) ctx.fs.chmod(temp, mode);
|
|
7473
|
+
ctx.fs.rename(temp, path);
|
|
7474
|
+
} catch (error) {
|
|
7475
|
+
removeQuietly(ctx, temp);
|
|
7476
|
+
throw error;
|
|
7535
7477
|
}
|
|
7536
|
-
return patterns;
|
|
7537
|
-
}
|
|
7538
|
-
function snapshotArtifacts(value) {
|
|
7539
|
-
const key = "snapshot_artifacts";
|
|
7540
|
-
if (value === void 0 || value === null) return [];
|
|
7541
|
-
if (!Array.isArray(value)) throw invalid(`'${key}' must be a list of {title, prompt} entries`);
|
|
7542
|
-
return value.map((item) => {
|
|
7543
|
-
if (!isRecord(item)) throw invalid(`'${key}' entries must have 'title' and 'prompt'`);
|
|
7544
|
-
return {
|
|
7545
|
-
title: requiredString(item.title, `${key}[].title`),
|
|
7546
|
-
prompt: requiredString(item.prompt, `${key}[].prompt`)
|
|
7547
|
-
};
|
|
7548
|
-
});
|
|
7549
7478
|
}
|
|
7550
|
-
|
|
7551
|
-
|
|
7552
|
-
function findProjectRoot(ctx) {
|
|
7479
|
+
function createFileExclusive(ctx, path, content, existsMessage) {
|
|
7480
|
+
const temp = tempPathFor(path);
|
|
7553
7481
|
try {
|
|
7554
|
-
|
|
7482
|
+
ctx.fs.writeFile(temp, content);
|
|
7483
|
+
ctx.fs.link(temp, path);
|
|
7555
7484
|
} catch (error) {
|
|
7556
|
-
if (error instanceof
|
|
7485
|
+
if (error instanceof FsError && error.code === "EEXIST") throw new DldError(existsMessage);
|
|
7557
7486
|
throw error;
|
|
7487
|
+
} finally {
|
|
7488
|
+
removeQuietly(ctx, temp);
|
|
7558
7489
|
}
|
|
7559
7490
|
}
|
|
7560
|
-
function
|
|
7561
|
-
|
|
7562
|
-
|
|
7563
|
-
}
|
|
7564
|
-
|
|
7565
|
-
const root = findProjectRoot(ctx);
|
|
7566
|
-
const config = loadConfig(ctx, root);
|
|
7567
|
-
return { config, paths: resolvePaths(root, config) };
|
|
7491
|
+
function removeQuietly(ctx, path) {
|
|
7492
|
+
try {
|
|
7493
|
+
ctx.fs.remove(path);
|
|
7494
|
+
} catch {
|
|
7495
|
+
}
|
|
7568
7496
|
}
|
|
7569
7497
|
|
|
7570
|
-
// src/core/snapshot.ts
|
|
7571
|
-
import { basename as basename3, join as join6, relative as relative3 } from "node:path";
|
|
7572
|
-
|
|
7573
7498
|
// src/core/git.ts
|
|
7574
7499
|
import { relative, sep } from "node:path";
|
|
7575
7500
|
function gitAt(ctx, root) {
|
|
@@ -7599,8 +7524,8 @@ function resolveStateCommit(git, value) {
|
|
|
7599
7524
|
}
|
|
7600
7525
|
|
|
7601
7526
|
// src/core/records.ts
|
|
7602
|
-
var
|
|
7603
|
-
import { basename, join as
|
|
7527
|
+
var import_yaml = __toESM(require_dist(), 1);
|
|
7528
|
+
import { basename as basename2, join as join2, relative as relative2 } from "node:path";
|
|
7604
7529
|
var STATUSES = ["proposed", "accepted", "deprecated", "superseded"];
|
|
7605
7530
|
var RECORD_FILE = /^DL-(\d+)\.md$/;
|
|
7606
7531
|
var DECISION_ID = /^DL-\d+$/;
|
|
@@ -7613,7 +7538,7 @@ function listRecordFiles(ctx, recordsDir) {
|
|
|
7613
7538
|
const walk = (dir) => {
|
|
7614
7539
|
const entries = ctx.fs.readDir(dir).sort((a, b) => a.name < b.name ? -1 : 1);
|
|
7615
7540
|
for (const entry of entries) {
|
|
7616
|
-
const path =
|
|
7541
|
+
const path = join2(dir, entry.name);
|
|
7617
7542
|
if (entry.isDirectory) walk(path);
|
|
7618
7543
|
else if (entry.isFile && RECORD_FILE.test(entry.name)) found.push(path);
|
|
7619
7544
|
}
|
|
@@ -7622,12 +7547,25 @@ function listRecordFiles(ctx, recordsDir) {
|
|
|
7622
7547
|
return found;
|
|
7623
7548
|
}
|
|
7624
7549
|
function recordNumber(path) {
|
|
7625
|
-
const digits = RECORD_FILE.exec(
|
|
7550
|
+
const digits = RECORD_FILE.exec(basename2(path))?.[1];
|
|
7626
7551
|
if (digits === void 0) throw new Error(`not a decision record file: ${path}`);
|
|
7627
7552
|
return Number.parseInt(digits, 10);
|
|
7628
7553
|
}
|
|
7629
|
-
function findRecordFile(ctx, recordsDir, id) {
|
|
7630
|
-
|
|
7554
|
+
function findRecordFile(ctx, recordsDir, id, root) {
|
|
7555
|
+
const matches = listRecordFiles(ctx, recordsDir).filter((path) => basename2(path) === `${id}.md`);
|
|
7556
|
+
if (matches.length > 1) {
|
|
7557
|
+
const shown = matches.map((path) => root === void 0 ? path : relative2(root, path));
|
|
7558
|
+
throw new DldError(`decision ${id} is in more than one file: ${shown.join(", ")}`);
|
|
7559
|
+
}
|
|
7560
|
+
return matches[0];
|
|
7561
|
+
}
|
|
7562
|
+
function duplicateRecordFiles(ctx, recordsDir) {
|
|
7563
|
+
const byNumber = /* @__PURE__ */ new Map();
|
|
7564
|
+
for (const path of listRecordFiles(ctx, recordsDir)) {
|
|
7565
|
+
const number = recordNumber(path);
|
|
7566
|
+
byNumber.set(number, [...byNumber.get(number) ?? [], path]);
|
|
7567
|
+
}
|
|
7568
|
+
return [...byNumber.entries()].filter(([, paths]) => paths.length > 1).sort(([a], [b]) => a - b).map(([, paths]) => paths);
|
|
7631
7569
|
}
|
|
7632
7570
|
var isDelimiter = (line) => line === "---" || line === "---\r";
|
|
7633
7571
|
function frontmatterBlock(text) {
|
|
@@ -7642,15 +7580,21 @@ function recordBody(text) {
|
|
|
7642
7580
|
const block = frontmatterBlock(text);
|
|
7643
7581
|
return block === void 0 ? text : block.lines.slice(block.end + 1).join("\n");
|
|
7644
7582
|
}
|
|
7583
|
+
function recordHead(text) {
|
|
7584
|
+
const block = frontmatterBlock(text);
|
|
7585
|
+
if (block === void 0) return void 0;
|
|
7586
|
+
return `${block.lines.slice(0, block.end + 1).join("\n")}
|
|
7587
|
+
`;
|
|
7588
|
+
}
|
|
7645
7589
|
function parseRecord(text, source) {
|
|
7646
7590
|
const invalid2 = (message) => new DldError(`${source}: ${message}`);
|
|
7647
7591
|
const block = frontmatterBlock(text);
|
|
7648
7592
|
if (block === void 0) throw invalid2("no frontmatter between --- lines");
|
|
7649
7593
|
const frontmatter = block.lines.slice(block.start + 1, block.end).map((line) => line.replace(/\r$/, ""));
|
|
7650
|
-
const doc = (0,
|
|
7594
|
+
const doc = (0, import_yaml.parseDocument)(frontmatter.join("\n"), { logLevel: "silent" });
|
|
7651
7595
|
const problem = doc.errors[0] ?? doc.warnings[0];
|
|
7652
7596
|
const raw = problem === void 0 ? doc.toJS() : legacyFields(frontmatter);
|
|
7653
|
-
if (!
|
|
7597
|
+
if (!isRecord(raw)) throw invalid2("frontmatter must be a mapping");
|
|
7654
7598
|
const string = (key) => {
|
|
7655
7599
|
const value = raw[key];
|
|
7656
7600
|
if (value === void 0 || value === null) return void 0;
|
|
@@ -7707,13 +7651,13 @@ function references(value, invalid2) {
|
|
|
7707
7651
|
if (value === void 0 || value === null) return [];
|
|
7708
7652
|
if (!Array.isArray(value)) throw invalid2("'references' must be a list");
|
|
7709
7653
|
return value.map((item) => {
|
|
7710
|
-
if (!
|
|
7654
|
+
if (!isRecord(item) || typeof item.path !== "string") {
|
|
7711
7655
|
throw invalid2("'references' entries must have a 'path'");
|
|
7712
7656
|
}
|
|
7713
7657
|
return typeof item.symbol === "string" ? { path: item.path, symbol: item.symbol } : { path: item.path };
|
|
7714
7658
|
});
|
|
7715
7659
|
}
|
|
7716
|
-
function
|
|
7660
|
+
function isRecord(value) {
|
|
7717
7661
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7718
7662
|
}
|
|
7719
7663
|
function setStatus(text, status, source) {
|
|
@@ -7779,270 +7723,111 @@ function formatTimestamp(date) {
|
|
|
7779
7723
|
return `${date.toISOString().slice(0, 19)}Z`;
|
|
7780
7724
|
}
|
|
7781
7725
|
|
|
7726
|
+
// src/core/reindex.ts
|
|
7727
|
+
import { basename as basename3 } from "node:path";
|
|
7728
|
+
|
|
7782
7729
|
// src/core/ids.ts
|
|
7783
7730
|
function formatId(n) {
|
|
7784
7731
|
return `DL-${String(n).padStart(3, "0")}`;
|
|
7785
7732
|
}
|
|
7733
|
+
function isCanonicalId(id) {
|
|
7734
|
+
return /^DL-(\d{3}|[1-9]\d{3,})$/.test(id);
|
|
7735
|
+
}
|
|
7736
|
+
function canonicalSpelling(id) {
|
|
7737
|
+
const digits = /^dl-(\d+)$/i.exec(id)?.[1];
|
|
7738
|
+
if (digits === void 0) return void 0;
|
|
7739
|
+
const number = Number(digits);
|
|
7740
|
+
return Number.isSafeInteger(number) ? formatId(number) : void 0;
|
|
7741
|
+
}
|
|
7786
7742
|
function nextId(ctx, recordsDir) {
|
|
7787
7743
|
const highest = listRecordFiles(ctx, recordsDir).map(recordNumber).reduce((max, n) => Math.max(max, n), 0);
|
|
7788
7744
|
return formatId(highest + 1);
|
|
7789
7745
|
}
|
|
7790
7746
|
|
|
7791
|
-
// src/core/
|
|
7792
|
-
var
|
|
7793
|
-
|
|
7794
|
-
|
|
7795
|
-
|
|
7796
|
-
|
|
7797
|
-
var TEMP_PREFIX = ".dld-tmp-";
|
|
7798
|
-
function tempPathFor(path) {
|
|
7799
|
-
const suffix = Math.random().toString(36).slice(2, 10);
|
|
7800
|
-
return join4(dirname(path), `${TEMP_PREFIX}${basename2(path)}.${suffix}`);
|
|
7801
|
-
}
|
|
7802
|
-
function writeFileAtomic(ctx, path, content, mode) {
|
|
7803
|
-
const temp = tempPathFor(path);
|
|
7747
|
+
// src/core/open-prs.ts
|
|
7748
|
+
var GITHUB_REMOTE = /github\.com[:/]/;
|
|
7749
|
+
var PR_LIMIT = "100";
|
|
7750
|
+
var COMMIT_ID = /^[0-9a-f]{40}([0-9a-f]{24})?$/;
|
|
7751
|
+
function openPrIds(ctx, paths, base) {
|
|
7752
|
+
const skipped = (reason) => ({ claims: [], skipped: reason });
|
|
7804
7753
|
try {
|
|
7805
|
-
ctx.
|
|
7806
|
-
if (mode !== void 0) ctx.fs.chmod(temp, mode);
|
|
7807
|
-
ctx.fs.rename(temp, path);
|
|
7754
|
+
ctx.gh(["--version"]);
|
|
7808
7755
|
} catch (error) {
|
|
7809
|
-
|
|
7756
|
+
if (error instanceof ToolNotFoundError) return skipped("gh CLI not installed");
|
|
7757
|
+
if (!(error instanceof GhCommandError)) throw error;
|
|
7758
|
+
}
|
|
7759
|
+
const git = gitAt(ctx, paths.root);
|
|
7760
|
+
if (!GITHUB_REMOTE.test(gitOrEmpty(git, "remote", "get-url", "origin"))) {
|
|
7761
|
+
return skipped("origin is not a GitHub remote");
|
|
7762
|
+
}
|
|
7763
|
+
try {
|
|
7764
|
+
ctx.gh(["auth", "status"]);
|
|
7765
|
+
} catch (error) {
|
|
7766
|
+
if (error instanceof GhCommandError) return skipped("gh not authenticated");
|
|
7810
7767
|
throw error;
|
|
7811
7768
|
}
|
|
7812
|
-
|
|
7813
|
-
|
|
7814
|
-
const temp = tempPathFor(path);
|
|
7769
|
+
const prBase = base.startsWith("origin/") ? base.slice("origin/".length) : base;
|
|
7770
|
+
let output;
|
|
7815
7771
|
try {
|
|
7816
|
-
ctx.
|
|
7817
|
-
|
|
7772
|
+
output = ctx.gh([
|
|
7773
|
+
"pr",
|
|
7774
|
+
"list",
|
|
7775
|
+
"--state",
|
|
7776
|
+
"open",
|
|
7777
|
+
`--base=${prBase}`,
|
|
7778
|
+
"--json",
|
|
7779
|
+
"files,headRefName,headRefOid,isCrossRepository",
|
|
7780
|
+
"--limit",
|
|
7781
|
+
PR_LIMIT
|
|
7782
|
+
]);
|
|
7818
7783
|
} catch (error) {
|
|
7819
|
-
if (error instanceof
|
|
7784
|
+
if (error instanceof GhCommandError) {
|
|
7785
|
+
return skipped(`gh pr list failed: ${firstLine(error.stderr) || "no error output"}`);
|
|
7786
|
+
}
|
|
7820
7787
|
throw error;
|
|
7821
|
-
} finally {
|
|
7822
|
-
removeQuietly(ctx, temp);
|
|
7823
7788
|
}
|
|
7789
|
+
const prs = parsePrList(output);
|
|
7790
|
+
if (prs === void 0) return skipped("gh pr list returned unexpected output");
|
|
7791
|
+
const current = gitOrEmpty(git, "rev-parse", "--abbrev-ref", "HEAD").trim();
|
|
7792
|
+
const prefix = `${recordsPathspec(paths)}/`;
|
|
7793
|
+
const claims = [];
|
|
7794
|
+
for (const pr of prs) {
|
|
7795
|
+
if (!pr.isCrossRepository && pr.headRefName === current) continue;
|
|
7796
|
+
for (const path of pr.files) {
|
|
7797
|
+
if (!path.startsWith(prefix)) continue;
|
|
7798
|
+
for (const id of path.match(DECISION_MENTION) ?? []) claims.push({ id, head: pr.headRefOid });
|
|
7799
|
+
}
|
|
7800
|
+
}
|
|
7801
|
+
return { claims };
|
|
7824
7802
|
}
|
|
7825
|
-
function
|
|
7803
|
+
function parsePrList(output) {
|
|
7804
|
+
let value;
|
|
7826
7805
|
try {
|
|
7827
|
-
|
|
7806
|
+
value = JSON.parse(output);
|
|
7828
7807
|
} catch {
|
|
7808
|
+
return void 0;
|
|
7829
7809
|
}
|
|
7830
|
-
|
|
7831
|
-
|
|
7832
|
-
|
|
7833
|
-
|
|
7834
|
-
|
|
7835
|
-
|
|
7836
|
-
|
|
7837
|
-
|
|
7838
|
-
|
|
7839
|
-
|
|
7840
|
-
|
|
7841
|
-
|
|
7842
|
-
|
|
7843
|
-
if (problem !== void 0) throw new DldError(`${source}: not valid YAML: ${problem.message}`);
|
|
7844
|
-
if (doc.contents !== null && !(0, import_yaml3.isMap)(doc.contents)) {
|
|
7845
|
-
throw new DldError(`${source}: expected a mapping of sections`);
|
|
7810
|
+
if (!Array.isArray(value)) return void 0;
|
|
7811
|
+
const prs = [];
|
|
7812
|
+
for (const item of value) {
|
|
7813
|
+
if (!isObject(item)) return void 0;
|
|
7814
|
+
const files = Array.isArray(item.files) ? item.files : [];
|
|
7815
|
+
prs.push({
|
|
7816
|
+
headRefName: typeof item.headRefName === "string" ? item.headRefName : void 0,
|
|
7817
|
+
headRefOid: typeof item.headRefOid === "string" && COMMIT_ID.test(item.headRefOid) ? item.headRefOid : void 0,
|
|
7818
|
+
isCrossRepository: item.isCrossRepository === true,
|
|
7819
|
+
files: files.flatMap(
|
|
7820
|
+
(file) => isObject(file) && typeof file.path === "string" ? [file.path] : []
|
|
7821
|
+
)
|
|
7822
|
+
});
|
|
7846
7823
|
}
|
|
7847
|
-
return
|
|
7848
|
-
}
|
|
7849
|
-
function readStateSection(ctx, paths, section) {
|
|
7850
|
-
const value = loadStateDocument(ctx, paths).toJS()?.[section];
|
|
7851
|
-
return typeof value === "object" && value !== null && !Array.isArray(value) ? { ...value } : void 0;
|
|
7824
|
+
return prs;
|
|
7852
7825
|
}
|
|
7853
|
-
function
|
|
7854
|
-
|
|
7855
|
-
doc.set(section, value);
|
|
7856
|
-
writeFileAtomic(ctx, statePath(paths), String(doc));
|
|
7826
|
+
function isObject(value) {
|
|
7827
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7857
7828
|
}
|
|
7858
|
-
function
|
|
7859
|
-
|
|
7860
|
-
return typeof value === "string" && value !== "" ? value : void 0;
|
|
7861
|
-
}
|
|
7862
|
-
function shortHead(ctx, root) {
|
|
7863
|
-
try {
|
|
7864
|
-
return ctx.git(["-C", root, "rev-parse", "--short", "HEAD"]).trim();
|
|
7865
|
-
} catch (error) {
|
|
7866
|
-
if (error instanceof GitCommandError) return "unknown";
|
|
7867
|
-
throw error;
|
|
7868
|
-
}
|
|
7869
|
-
}
|
|
7870
|
-
|
|
7871
|
-
// src/core/snapshot.ts
|
|
7872
|
-
var DECISION_BOUNDARY = "===DLD_DECISION_BOUNDARY===";
|
|
7873
|
-
var BUILT_IN_ARTIFACTS = ["SNAPSHOT.md", "OVERVIEW.md"];
|
|
7874
|
-
function readRecords(ctx, { paths }) {
|
|
7875
|
-
return listRecordFiles(ctx, paths.recordsDir).map((path) => {
|
|
7876
|
-
const text = ctx.fs.readFile(path);
|
|
7877
|
-
const record = parseRecord(text, relative3(paths.root, path));
|
|
7878
|
-
return { path, number: recordNumber(path), text, accepted: record.status === "accepted" };
|
|
7879
|
-
}).sort((a, b) => a.number - b.number);
|
|
7880
|
-
}
|
|
7881
|
-
function collectActiveDecisions(ctx, project) {
|
|
7882
|
-
if (!ctx.fs.isDirectory(project.paths.recordsDir)) {
|
|
7883
|
-
throw new DldError(`records directory not found at ${project.paths.recordsDir}`);
|
|
7884
|
-
}
|
|
7885
|
-
return readRecords(ctx, project).filter((record) => record.accepted).map((record) => record.text).join(`${DECISION_BOUNDARY}
|
|
7886
|
-
`);
|
|
7887
|
-
}
|
|
7888
|
-
function detectSnapshotChanges(ctx, project) {
|
|
7889
|
-
const { paths } = project;
|
|
7890
|
-
const state = readStateSection(ctx, paths, "snapshot");
|
|
7891
|
-
if (state === void 0) return { mode: "full" };
|
|
7892
|
-
if (BUILT_IN_ARTIFACTS.some((name) => !ctx.fs.exists(join6(paths.decisionsDir, name)))) {
|
|
7893
|
-
return { mode: "full" };
|
|
7894
|
-
}
|
|
7895
|
-
const included = stateString(state, "decisions_included");
|
|
7896
|
-
if (included === void 0 || !/^\d+$/.test(included)) return { mode: "full" };
|
|
7897
|
-
const includedNumber = Number.parseInt(included, 10);
|
|
7898
|
-
const newDecisions = readRecords(ctx, project).filter((record) => record.number > includedNumber && record.accepted).map((record) => formatId(record.number));
|
|
7899
|
-
const git = gitAt(ctx, paths.root);
|
|
7900
|
-
let stored = stateString(state, "commit_hash");
|
|
7901
|
-
const lastRun = stateString(state, "last_run");
|
|
7902
|
-
if ((stored === void 0 || stored === "unknown") && lastRun !== void 0) {
|
|
7903
|
-
stored = gitOrEmpty(git, "log", `--until=${lastRun}`, "--format=%h", "-1").trim() || void 0;
|
|
7904
|
-
}
|
|
7905
|
-
const commit2 = resolveStateCommit(git, stored);
|
|
7906
|
-
let modifiedDecisions = [];
|
|
7907
|
-
let commitRange = "";
|
|
7908
|
-
if (commit2 !== void 0 && commit2 !== shortHead(ctx, paths.root)) {
|
|
7909
|
-
commitRange = `${commit2}..HEAD`;
|
|
7910
|
-
const numbers = nulSeparated(
|
|
7911
|
-
gitOrEmpty(git, "diff", "-z", "--name-only", commitRange, "--", recordsPathspec(paths))
|
|
7912
|
-
).map((path) => basename3(path)).filter((name) => RECORD_FILE.test(name)).map((name) => recordNumber(name)).filter((number) => number <= includedNumber);
|
|
7913
|
-
modifiedDecisions = [...new Set(numbers)].sort((a, b) => a - b).map(formatId);
|
|
7914
|
-
}
|
|
7915
|
-
return { mode: "incremental", newDecisions, modifiedDecisions, commitRange };
|
|
7916
|
-
}
|
|
7917
|
-
function formatSnapshotChanges(changes) {
|
|
7918
|
-
if (changes.mode === "full") return "mode: full\n";
|
|
7919
|
-
return [
|
|
7920
|
-
"mode: incremental",
|
|
7921
|
-
`new_decisions: ${changes.newDecisions.join(", ")}`,
|
|
7922
|
-
`modified_decisions: ${changes.modifiedDecisions.join(", ")}`,
|
|
7923
|
-
`commit_range: ${changes.commitRange}`,
|
|
7924
|
-
""
|
|
7925
|
-
].join("\n");
|
|
7926
|
-
}
|
|
7927
|
-
function updateSnapshotState(ctx, project, customArtifacts) {
|
|
7928
|
-
const { paths } = project;
|
|
7929
|
-
const timestamp = formatTimestamp(ctx.now());
|
|
7930
|
-
const commit2 = shortHead(ctx, paths.root);
|
|
7931
|
-
const highest = readRecords(ctx, project).filter((record) => record.accepted).reduce((max, record) => Math.max(max, record.number), 0);
|
|
7932
|
-
const artifacts = /* @__PURE__ */ Object.create(null);
|
|
7933
|
-
for (const name of [...BUILT_IN_ARTIFACTS, ...customArtifacts]) artifacts[name] = timestamp;
|
|
7934
|
-
writeStateSection(ctx, paths, "snapshot", {
|
|
7935
|
-
last_run: timestamp,
|
|
7936
|
-
commit_hash: commit2,
|
|
7937
|
-
decisions_included: String(highest),
|
|
7938
|
-
artifacts
|
|
7939
|
-
});
|
|
7940
|
-
return { timestamp, commit: commit2, highest };
|
|
7941
|
-
}
|
|
7942
|
-
|
|
7943
|
-
// src/cli/commands/collect-active-decisions.ts
|
|
7944
|
-
var collectActiveDecisionsCommand = {
|
|
7945
|
-
name: "collect-active-decisions",
|
|
7946
|
-
summary: "Print every accepted decision record",
|
|
7947
|
-
internal: true,
|
|
7948
|
-
usage: "Usage: dld collect-active-decisions\n\nPrint each accepted record in ID order, separated by ===DLD_DECISION_BOUNDARY=== lines.\n",
|
|
7949
|
-
run(args, io, ctx) {
|
|
7950
|
-
parseCommandArgs({ args: [...args], options: {} });
|
|
7951
|
-
io.stdout(collectActiveDecisions(ctx, loadProject(ctx)));
|
|
7952
|
-
return EXIT_OK;
|
|
7953
|
-
}
|
|
7954
|
-
};
|
|
7955
|
-
|
|
7956
|
-
// src/core/commit-reindex.ts
|
|
7957
|
-
import { join as join7, posix as posix2 } from "node:path";
|
|
7958
|
-
|
|
7959
|
-
// src/core/reindex.ts
|
|
7960
|
-
import { basename as basename4 } from "node:path";
|
|
7961
|
-
|
|
7962
|
-
// src/core/open-prs.ts
|
|
7963
|
-
var GITHUB_REMOTE = /github\.com[:/]/;
|
|
7964
|
-
var PR_LIMIT = "100";
|
|
7965
|
-
var COMMIT_ID = /^[0-9a-f]{40}([0-9a-f]{24})?$/;
|
|
7966
|
-
function openPrIds(ctx, paths, base) {
|
|
7967
|
-
const skipped = (reason) => ({ claims: [], skipped: reason });
|
|
7968
|
-
try {
|
|
7969
|
-
ctx.gh(["--version"]);
|
|
7970
|
-
} catch (error) {
|
|
7971
|
-
if (error instanceof ToolNotFoundError) return skipped("gh CLI not installed");
|
|
7972
|
-
if (!(error instanceof GhCommandError)) throw error;
|
|
7973
|
-
}
|
|
7974
|
-
const git = gitAt(ctx, paths.root);
|
|
7975
|
-
if (!GITHUB_REMOTE.test(gitOrEmpty(git, "remote", "get-url", "origin"))) {
|
|
7976
|
-
return skipped("origin is not a GitHub remote");
|
|
7977
|
-
}
|
|
7978
|
-
try {
|
|
7979
|
-
ctx.gh(["auth", "status"]);
|
|
7980
|
-
} catch (error) {
|
|
7981
|
-
if (error instanceof GhCommandError) return skipped("gh not authenticated");
|
|
7982
|
-
throw error;
|
|
7983
|
-
}
|
|
7984
|
-
const prBase = base.startsWith("origin/") ? base.slice("origin/".length) : base;
|
|
7985
|
-
let output;
|
|
7986
|
-
try {
|
|
7987
|
-
output = ctx.gh([
|
|
7988
|
-
"pr",
|
|
7989
|
-
"list",
|
|
7990
|
-
"--state",
|
|
7991
|
-
"open",
|
|
7992
|
-
`--base=${prBase}`,
|
|
7993
|
-
"--json",
|
|
7994
|
-
"files,headRefName,headRefOid,isCrossRepository",
|
|
7995
|
-
"--limit",
|
|
7996
|
-
PR_LIMIT
|
|
7997
|
-
]);
|
|
7998
|
-
} catch (error) {
|
|
7999
|
-
if (error instanceof GhCommandError) {
|
|
8000
|
-
return skipped(`gh pr list failed: ${firstLine(error.stderr) || "no error output"}`);
|
|
8001
|
-
}
|
|
8002
|
-
throw error;
|
|
8003
|
-
}
|
|
8004
|
-
const prs = parsePrList(output);
|
|
8005
|
-
if (prs === void 0) return skipped("gh pr list returned unexpected output");
|
|
8006
|
-
const current = gitOrEmpty(git, "rev-parse", "--abbrev-ref", "HEAD").trim();
|
|
8007
|
-
const prefix = `${recordsPathspec(paths)}/`;
|
|
8008
|
-
const claims = [];
|
|
8009
|
-
for (const pr of prs) {
|
|
8010
|
-
if (!pr.isCrossRepository && pr.headRefName === current) continue;
|
|
8011
|
-
for (const path of pr.files) {
|
|
8012
|
-
if (!path.startsWith(prefix)) continue;
|
|
8013
|
-
for (const id of path.match(DECISION_MENTION) ?? []) claims.push({ id, head: pr.headRefOid });
|
|
8014
|
-
}
|
|
8015
|
-
}
|
|
8016
|
-
return { claims };
|
|
8017
|
-
}
|
|
8018
|
-
function parsePrList(output) {
|
|
8019
|
-
let value;
|
|
8020
|
-
try {
|
|
8021
|
-
value = JSON.parse(output);
|
|
8022
|
-
} catch {
|
|
8023
|
-
return void 0;
|
|
8024
|
-
}
|
|
8025
|
-
if (!Array.isArray(value)) return void 0;
|
|
8026
|
-
const prs = [];
|
|
8027
|
-
for (const item of value) {
|
|
8028
|
-
if (!isObject(item)) return void 0;
|
|
8029
|
-
const files = Array.isArray(item.files) ? item.files : [];
|
|
8030
|
-
prs.push({
|
|
8031
|
-
headRefName: typeof item.headRefName === "string" ? item.headRefName : void 0,
|
|
8032
|
-
headRefOid: typeof item.headRefOid === "string" && COMMIT_ID.test(item.headRefOid) ? item.headRefOid : void 0,
|
|
8033
|
-
isCrossRepository: item.isCrossRepository === true,
|
|
8034
|
-
files: files.flatMap(
|
|
8035
|
-
(file) => isObject(file) && typeof file.path === "string" ? [file.path] : []
|
|
8036
|
-
)
|
|
8037
|
-
});
|
|
8038
|
-
}
|
|
8039
|
-
return prs;
|
|
8040
|
-
}
|
|
8041
|
-
function isObject(value) {
|
|
8042
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8043
|
-
}
|
|
8044
|
-
function firstLine(text) {
|
|
8045
|
-
return text.split("\n", 1)[0]?.trim() ?? "";
|
|
7829
|
+
function firstLine(text) {
|
|
7830
|
+
return text.split("\n", 1)[0]?.trim() ?? "";
|
|
8046
7831
|
}
|
|
8047
7832
|
|
|
8048
7833
|
// src/core/reindex.ts
|
|
@@ -8051,8 +7836,20 @@ var idNumber = (id) => Number.parseInt(id.slice(3), 10);
|
|
|
8051
7836
|
function compareIds(a, b) {
|
|
8052
7837
|
return idNumber(a) - idNumber(b) || (a < b ? -1 : a > b ? 1 : 0);
|
|
8053
7838
|
}
|
|
8054
|
-
function resolveBase(ctx) {
|
|
7839
|
+
function resolveBase(ctx, baseBranch) {
|
|
8055
7840
|
const git = (...args) => ctx.git(args);
|
|
7841
|
+
const tried = [];
|
|
7842
|
+
const firstExisting = (...refs) => refs.find((ref) => {
|
|
7843
|
+
tried.push(ref);
|
|
7844
|
+
return gitOrEmpty(git, "rev-parse", "--verify", "--quiet", `${ref}^{commit}`) !== "";
|
|
7845
|
+
});
|
|
7846
|
+
if (baseBranch !== void 0) {
|
|
7847
|
+
const found2 = firstExisting(`origin/${baseBranch}`, baseBranch);
|
|
7848
|
+
if (found2 !== void 0) return found2;
|
|
7849
|
+
throw new DldError(
|
|
7850
|
+
`base_branch '${baseBranch}' not found (tried ${tried.join(", ")}). Fetch first, or fix base_branch in dld.config.yaml.`
|
|
7851
|
+
);
|
|
7852
|
+
}
|
|
8056
7853
|
const current = gitOrEmpty(git, "rev-parse", "--abbrev-ref", "HEAD").trim();
|
|
8057
7854
|
const upstream = gitOrEmpty(
|
|
8058
7855
|
git,
|
|
@@ -8061,12 +7858,26 @@ function resolveBase(ctx) {
|
|
|
8061
7858
|
"--symbolic-full-name",
|
|
8062
7859
|
"@{upstream}"
|
|
8063
7860
|
).trim();
|
|
7861
|
+
const candidates = [];
|
|
8064
7862
|
if (upstream !== "") {
|
|
8065
7863
|
const slash = upstream.indexOf("/");
|
|
8066
7864
|
const branch = slash === -1 ? upstream : upstream.slice(slash + 1);
|
|
8067
|
-
if (branch !== "" && branch !== current)
|
|
7865
|
+
if (branch !== "" && branch !== current) candidates.push(upstream);
|
|
8068
7866
|
}
|
|
8069
|
-
|
|
7867
|
+
const remoteDefault = gitOrEmpty(
|
|
7868
|
+
git,
|
|
7869
|
+
"symbolic-ref",
|
|
7870
|
+
"--quiet",
|
|
7871
|
+
"--short",
|
|
7872
|
+
"refs/remotes/origin/HEAD"
|
|
7873
|
+
).trim();
|
|
7874
|
+
if (remoteDefault !== "") candidates.push(remoteDefault);
|
|
7875
|
+
candidates.push(DEFAULT_BASE, "main");
|
|
7876
|
+
const found = firstExisting(...new Set(candidates));
|
|
7877
|
+
if (found !== void 0) return found;
|
|
7878
|
+
throw new DldError(
|
|
7879
|
+
`no base branch found (tried ${tried.join(", ")}). Set base_branch in dld.config.yaml, or run git remote set-head origin --auto.`
|
|
7880
|
+
);
|
|
8070
7881
|
}
|
|
8071
7882
|
function verifyBase(ctx, paths, base, hint = "") {
|
|
8072
7883
|
if (base.startsWith("-")) throw new DldError(`--base must be a git ref, got '${base}'`);
|
|
@@ -8141,8 +7952,8 @@ function collisionsOf(ctx, paths, base) {
|
|
|
8141
7952
|
const git = gitAt(ctx, paths.root);
|
|
8142
7953
|
const added = localAdditions(git, paths, base);
|
|
8143
7954
|
const local = added.flatMap((path) => {
|
|
8144
|
-
const id =
|
|
8145
|
-
return RECORD_FILE.test(
|
|
7955
|
+
const id = basename3(path, ".md");
|
|
7956
|
+
return RECORD_FILE.test(basename3(path)) ? [{ path, id }] : [];
|
|
8146
7957
|
});
|
|
8147
7958
|
if (local.length === 0) return { collisions: [], localIds: [], taken: [] };
|
|
8148
7959
|
const taken = takenIds(ctx, paths, base);
|
|
@@ -8169,840 +7980,1134 @@ function formatRename({ path, oldId, newId }) {
|
|
|
8169
7980
|
return `${path} ${oldId} ${newId}`;
|
|
8170
7981
|
}
|
|
8171
7982
|
|
|
8172
|
-
// src/core/
|
|
8173
|
-
|
|
8174
|
-
|
|
8175
|
-
|
|
8176
|
-
|
|
8177
|
-
if (oldId === newId) return "old and new IDs are the same.";
|
|
8178
|
-
const segments = path.split("/");
|
|
8179
|
-
if (path.startsWith("/") || segments.some((s) => s === "" || s === "." || s === "..")) {
|
|
8180
|
-
return `path '${path}' must be relative to the project root, without '.' or '..' segments.`;
|
|
8181
|
-
}
|
|
8182
|
-
const records = recordsPathspec(paths);
|
|
8183
|
-
if (!path.startsWith(`${records}/`)) {
|
|
8184
|
-
return `path '${path}' is not under ${records}/.`;
|
|
7983
|
+
// src/core/decision-edits.ts
|
|
7984
|
+
function editBase(ctx, project, base) {
|
|
7985
|
+
if (base !== void 0) {
|
|
7986
|
+
verifyBase(ctx, project.paths, base, " Fetch first or pass --base.");
|
|
7987
|
+
return base;
|
|
8185
7988
|
}
|
|
8186
|
-
|
|
8187
|
-
|
|
7989
|
+
return resolveBase(ctx, project.config.baseBranch);
|
|
7990
|
+
}
|
|
7991
|
+
function checkDecisionEdits(ctx, { paths }, base, ids = [], { uncommitted = false } = {}) {
|
|
7992
|
+
const git = gitAt(ctx, paths.root);
|
|
7993
|
+
const records = integratedRecords(ctx, paths, base);
|
|
7994
|
+
const allowed = uncommitted ? /* @__PURE__ */ new Set() : trailerIds(git, records.onto);
|
|
7995
|
+
const allow = (edit) => (edit.state === "edited" || edit.state === "deleted") && allowed.has(edit.id) ? { ...edit, state: "allowed" } : edit;
|
|
7996
|
+
const stateOf = (id) => allow(rawStateOf(id));
|
|
7997
|
+
const rawStateOf = (id) => {
|
|
7998
|
+
const located = locate(ctx, paths, records, id);
|
|
7999
|
+
if (located === void 0) {
|
|
8000
|
+
const local = findRecordFile(ctx, paths.recordsDir, id, paths.root);
|
|
8001
|
+
if (local === void 0) throw new DldError(`decision ${id} not found.`);
|
|
8002
|
+
return { path: toPosix(relative3(paths.root, local)), id, state: "draft" };
|
|
8003
|
+
}
|
|
8004
|
+
const path = located.current ?? located.basePath;
|
|
8005
|
+
if (isProposed(located.baseText, located.basePath)) return { path, id, state: "draft" };
|
|
8006
|
+
const reference = uncommitted ? atHead(git, located) : { text: located.baseText, path };
|
|
8007
|
+
if (reference === void 0) return { path, id, state: "integrated" };
|
|
8008
|
+
if (located.current === void 0) return { path, id, state: "deleted" };
|
|
8009
|
+
const now = ctx.fs.readFile(join3(paths.root, located.current));
|
|
8010
|
+
const same = lockedContent(now, path) === lockedContent(reference.text, reference.path);
|
|
8011
|
+
return { path, id, state: same ? "integrated" : "edited" };
|
|
8012
|
+
};
|
|
8013
|
+
if (ids.length > 0) return ids.map(stateOf);
|
|
8014
|
+
const changed = nulSeparated(
|
|
8015
|
+
gitOrEmpty(
|
|
8016
|
+
git,
|
|
8017
|
+
"diff",
|
|
8018
|
+
"-z",
|
|
8019
|
+
"--no-renames",
|
|
8020
|
+
"--name-only",
|
|
8021
|
+
uncommitted ? "HEAD" : records.onto,
|
|
8022
|
+
"--",
|
|
8023
|
+
recordsPathspec(paths)
|
|
8024
|
+
)
|
|
8025
|
+
);
|
|
8026
|
+
const candidates = new Set(
|
|
8027
|
+
changed.map((path) => basename4(path)).filter((name) => RECORD_FILE.test(name)).map((name) => basename4(name, ".md")).filter((id) => records.byId.has(id))
|
|
8028
|
+
);
|
|
8029
|
+
return [...candidates].map(stateOf).filter(({ state }) => state === "edited" || state === "deleted" || state === "allowed").sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
8030
|
+
}
|
|
8031
|
+
var EDIT_TRAILER = "Decision-Edit";
|
|
8032
|
+
function trailerIds(git, onto) {
|
|
8033
|
+
const values = gitOrEmpty(
|
|
8034
|
+
git,
|
|
8035
|
+
"log",
|
|
8036
|
+
`--format=%(trailers:key=${EDIT_TRAILER},valueonly)`,
|
|
8037
|
+
`${onto}..HEAD`
|
|
8038
|
+
);
|
|
8039
|
+
return new Set(values.split(/[\s,]+/).filter((value) => DECISION_ID.test(value)));
|
|
8040
|
+
}
|
|
8041
|
+
function integratedRecords(ctx, paths, base) {
|
|
8042
|
+
const onto = mergeBase(ctx, paths, base);
|
|
8043
|
+
const byId = /* @__PURE__ */ new Map();
|
|
8044
|
+
const listing = gitOrEmpty(
|
|
8045
|
+
gitAt(ctx, paths.root),
|
|
8046
|
+
"ls-tree",
|
|
8047
|
+
"-r",
|
|
8048
|
+
"-z",
|
|
8049
|
+
"--name-only",
|
|
8050
|
+
onto,
|
|
8051
|
+
"--",
|
|
8052
|
+
recordsPathspec(paths)
|
|
8053
|
+
);
|
|
8054
|
+
for (const path of nulSeparated(listing)) {
|
|
8055
|
+
const name = basename4(path);
|
|
8056
|
+
if (RECORD_FILE.test(name)) byId.set(basename4(name, ".md"), path);
|
|
8057
|
+
}
|
|
8058
|
+
return { onto, byId };
|
|
8059
|
+
}
|
|
8060
|
+
function locate(ctx, paths, { onto, byId }, id) {
|
|
8061
|
+
const basePath = byId.get(id);
|
|
8062
|
+
if (basePath === void 0) return void 0;
|
|
8063
|
+
const baseText = gitAt(ctx, paths.root)("show", `${onto}:${basePath}`);
|
|
8064
|
+
const moved = findRecordFile(ctx, paths.recordsDir, id, paths.root);
|
|
8065
|
+
if (ctx.fs.isRegularFile(join3(paths.root, basePath))) {
|
|
8066
|
+
return { id, basePath, baseText, current: basePath };
|
|
8067
|
+
}
|
|
8068
|
+
const current = moved === void 0 ? void 0 : toPosix(relative3(paths.root, moved));
|
|
8069
|
+
return { id, basePath, baseText, current };
|
|
8070
|
+
}
|
|
8071
|
+
function atHead(git, { basePath, current }) {
|
|
8072
|
+
for (const path of /* @__PURE__ */ new Set([current, basePath])) {
|
|
8073
|
+
if (path === void 0) continue;
|
|
8074
|
+
const listed = gitOrEmpty(
|
|
8075
|
+
git,
|
|
8076
|
+
"--literal-pathspecs",
|
|
8077
|
+
"ls-tree",
|
|
8078
|
+
"--name-only",
|
|
8079
|
+
"HEAD",
|
|
8080
|
+
"--",
|
|
8081
|
+
path
|
|
8082
|
+
);
|
|
8083
|
+
if (listed !== "") return { text: git("show", `HEAD:${path}`), path };
|
|
8188
8084
|
}
|
|
8189
8085
|
return void 0;
|
|
8190
8086
|
}
|
|
8191
|
-
function
|
|
8192
|
-
|
|
8193
|
-
|
|
8194
|
-
|
|
8195
|
-
|
|
8196
|
-
|
|
8197
|
-
|
|
8198
|
-
|
|
8199
|
-
|
|
8200
|
-
|
|
8201
|
-
|
|
8202
|
-
|
|
8203
|
-
|
|
8204
|
-
|
|
8205
|
-
const
|
|
8206
|
-
if (
|
|
8207
|
-
|
|
8208
|
-
|
|
8209
|
-
|
|
8210
|
-
|
|
8211
|
-
|
|
8212
|
-
seen.new.add(newId);
|
|
8213
|
-
renames.push(rename);
|
|
8214
|
-
lineNumbers.push(index + 1);
|
|
8087
|
+
function toPosix(path) {
|
|
8088
|
+
return path.split(sep2).join("/");
|
|
8089
|
+
}
|
|
8090
|
+
function isProposed(text, source) {
|
|
8091
|
+
try {
|
|
8092
|
+
return parseRecord(text, source).status === "proposed";
|
|
8093
|
+
} catch {
|
|
8094
|
+
return false;
|
|
8095
|
+
}
|
|
8096
|
+
}
|
|
8097
|
+
function restoreDecisionProse(ctx, { paths }, base, ids, { uncommitted = false } = {}) {
|
|
8098
|
+
const git = gitAt(ctx, paths.root);
|
|
8099
|
+
const records = integratedRecords(ctx, paths, base);
|
|
8100
|
+
const targets = ids.map((id) => {
|
|
8101
|
+
const located = locate(ctx, paths, records, id);
|
|
8102
|
+
if (located === void 0 || isProposed(located.baseText, located.basePath)) {
|
|
8103
|
+
throw new DldError(`${id} is a draft (not on the base branch, or still proposed there).`);
|
|
8104
|
+
}
|
|
8105
|
+
const reference = uncommitted ? atHead(git, located) : { text: located.baseText, path: located.basePath };
|
|
8106
|
+
if (reference === void 0) throw new DldError(`${id} is not in HEAD; nothing to restore.`);
|
|
8107
|
+
return { located, reference };
|
|
8215
8108
|
});
|
|
8216
|
-
|
|
8217
|
-
|
|
8218
|
-
|
|
8219
|
-
|
|
8220
|
-
|
|
8109
|
+
return targets.map(({ located, reference }) => {
|
|
8110
|
+
const path = located.current ?? reference.path;
|
|
8111
|
+
const full = join3(paths.root, path);
|
|
8112
|
+
const current = located.current === void 0 ? void 0 : recordHead(ctx.fs.readFile(full));
|
|
8113
|
+
const referenceHead = recordHead(reference.text);
|
|
8114
|
+
if (current === void 0 || referenceHead === void 0) {
|
|
8115
|
+
writeFileAtomic(ctx, full, reference.text);
|
|
8116
|
+
if (located.current === void 0) git("--literal-pathspecs", "add", "--", path);
|
|
8117
|
+
} else {
|
|
8118
|
+
const eol = current.includes("\r\n") ? "\r\n" : "\n";
|
|
8119
|
+
const body = recordBody(reference.text).replace(/\r?\n/g, eol);
|
|
8120
|
+
writeFileAtomic(ctx, full, withLockedLines(current, referenceHead, eol) + body);
|
|
8221
8121
|
}
|
|
8122
|
+
return path;
|
|
8222
8123
|
});
|
|
8223
|
-
|
|
8124
|
+
}
|
|
8125
|
+
var LOCKED_KEYS = ["id", "timestamp"];
|
|
8126
|
+
function lockedContent(text, source) {
|
|
8127
|
+
const normalized = text.replace(/\r\n/g, "\n");
|
|
8128
|
+
let fields;
|
|
8129
|
+
try {
|
|
8130
|
+
const record = parseRecord(normalized, source);
|
|
8131
|
+
fields = JSON.stringify([record.id, record.timestamp ?? null]);
|
|
8132
|
+
} catch {
|
|
8133
|
+
fields = recordHead(normalized) ?? "";
|
|
8134
|
+
}
|
|
8135
|
+
return `${fields}
|
|
8136
|
+
${recordBody(normalized).trimEnd()}`;
|
|
8137
|
+
}
|
|
8138
|
+
function withLockedLines(head, baseHead, eol) {
|
|
8139
|
+
let result = head;
|
|
8140
|
+
for (const key of LOCKED_KEYS) {
|
|
8141
|
+
const pattern = new RegExp(`^${key}:[^\\r\\n]*`, "m");
|
|
8142
|
+
const line = baseHead.match(pattern)?.[0];
|
|
8143
|
+
if (line === void 0) continue;
|
|
8144
|
+
result = pattern.test(result) ? result.replace(pattern, () => line) : result.replace(/^---\r?\n/, (open) => `${open}${line}${eol}`);
|
|
8145
|
+
}
|
|
8146
|
+
return result;
|
|
8147
|
+
}
|
|
8148
|
+
function formatRecordEdit({ path, id, state }) {
|
|
8149
|
+
return `${path} ${id} ${state}`;
|
|
8224
8150
|
}
|
|
8225
8151
|
|
|
8226
|
-
// src/core/
|
|
8227
|
-
|
|
8228
|
-
function reindexMessage(renames, originalSubjects = "") {
|
|
8229
|
-
const pairs = renames.map(({ oldId, newId }) => `${oldId} -> ${newId}`);
|
|
8230
|
-
const subject = renames.length > SUBJECT_LIST_LIMIT ? `reindex ${renames.length} local decisions to avoid base-branch collisions` : `reindex local decisions: ${pairs.join(", ")}`;
|
|
8231
|
-
const message = `${subject}
|
|
8232
|
-
|
|
8233
|
-
Renames:
|
|
8234
|
-
${pairs.map((pair) => `- ${pair}`).join("\n")}`;
|
|
8235
|
-
return originalSubjects === "" ? message : `${message}
|
|
8152
|
+
// src/core/project.ts
|
|
8153
|
+
import { join as join5 } from "node:path";
|
|
8236
8154
|
|
|
8237
|
-
|
|
8238
|
-
|
|
8239
|
-
}
|
|
8240
|
-
|
|
8241
|
-
|
|
8242
|
-
|
|
8243
|
-
|
|
8244
|
-
|
|
8245
|
-
|
|
8246
|
-
|
|
8247
|
-
|
|
8248
|
-
|
|
8155
|
+
// src/core/config.ts
|
|
8156
|
+
var import_yaml2 = __toESM(require_dist(), 1);
|
|
8157
|
+
import { join as join4 } from "node:path";
|
|
8158
|
+
var CONFIG_FILE = "dld.config.yaml";
|
|
8159
|
+
var DECISION_EDITS = ["block", "ask", "allow"];
|
|
8160
|
+
var CONFIG_KEYS = [
|
|
8161
|
+
"decisions_dir",
|
|
8162
|
+
"mode",
|
|
8163
|
+
"namespaces",
|
|
8164
|
+
"annotation_prefix",
|
|
8165
|
+
"annotation_exclude",
|
|
8166
|
+
"implement_review",
|
|
8167
|
+
"decision_edits",
|
|
8168
|
+
"snapshot_artifacts",
|
|
8169
|
+
"base_branch"
|
|
8170
|
+
];
|
|
8171
|
+
function loadConfig(ctx, root) {
|
|
8172
|
+
const path = join4(root, CONFIG_FILE);
|
|
8173
|
+
if (!ctx.fs.exists(path)) {
|
|
8174
|
+
throw new DldError(`${CONFIG_FILE} not found. Run /dld-init first.`);
|
|
8249
8175
|
}
|
|
8250
|
-
|
|
8251
|
-
return squash ? squashReindex(ctx, git, paths, renames, base) : commitOnTop(ctx, git, paths, renames);
|
|
8176
|
+
return parseConfig(ctx.fs.readFile(path), (message) => ctx.warn(message));
|
|
8252
8177
|
}
|
|
8253
|
-
function
|
|
8254
|
-
|
|
8255
|
-
|
|
8256
|
-
|
|
8257
|
-
|
|
8178
|
+
function parseConfig(text, warn = () => {
|
|
8179
|
+
}) {
|
|
8180
|
+
const doc = (0, import_yaml2.parseDocument)(text, { logLevel: "silent" });
|
|
8181
|
+
const problem = doc.errors[0] ?? doc.warnings[0];
|
|
8182
|
+
if (problem !== void 0) throw invalid(`not valid YAML: ${problem.message}`);
|
|
8183
|
+
const raw = doc.toJS();
|
|
8184
|
+
if (!isRecord2(raw)) throw invalid("expected a mapping of keys to values");
|
|
8185
|
+
for (const key of Object.keys(raw)) {
|
|
8186
|
+
if (!CONFIG_KEYS.some((known) => known === key)) warn(unknownKeyMessage(key));
|
|
8258
8187
|
}
|
|
8259
|
-
|
|
8260
|
-
|
|
8188
|
+
const mode = raw.mode;
|
|
8189
|
+
if (mode !== "flat" && mode !== "namespaced") {
|
|
8190
|
+
throw invalid("'mode' must be 'flat' or 'namespaced'");
|
|
8261
8191
|
}
|
|
8262
|
-
const
|
|
8263
|
-
const
|
|
8264
|
-
|
|
8265
|
-
|
|
8266
|
-
|
|
8267
|
-
|
|
8268
|
-
|
|
8269
|
-
}
|
|
8270
|
-
stepName = "checking the staged changes";
|
|
8271
|
-
if (!hasStagedChanges(git)) {
|
|
8272
|
-
throw new DldError(
|
|
8273
|
-
"nothing to commit. The reindex may have already been committed, or the plan didn't match the working tree."
|
|
8274
|
-
);
|
|
8275
|
-
}
|
|
8276
|
-
stepName = "committing";
|
|
8277
|
-
commit(git, reindexMessage(renames));
|
|
8278
|
-
} catch (error) {
|
|
8279
|
-
try {
|
|
8280
|
-
git("read-tree", tree);
|
|
8281
|
-
} catch (restoreError) {
|
|
8282
|
-
throw new DldError(
|
|
8283
|
-
`commit-reindex failed while ${stepName}: ${describe(error)}
|
|
8284
|
-
Restoring the index also failed: ${describe(restoreError)}
|
|
8285
|
-
To restore by hand: git read-tree ${tree}`
|
|
8286
|
-
);
|
|
8287
|
-
}
|
|
8288
|
-
if (!(error instanceof DldError)) throw error;
|
|
8289
|
-
throw new DldError(
|
|
8290
|
-
`commit-reindex failed while ${stepName}: ${error.message}
|
|
8291
|
-
The index was restored.`,
|
|
8292
|
-
error.exitCode
|
|
8293
|
-
);
|
|
8192
|
+
const namespaces = optionalStringList(raw.namespaces, "namespaces");
|
|
8193
|
+
const unsafe = namespaces.find((name) => !/^[A-Za-z0-9._-]+$/.test(name) || /^\.\.?$/.test(name));
|
|
8194
|
+
if (unsafe !== void 0) {
|
|
8195
|
+
throw invalid(`'namespaces' entries must be plain directory names, got '${unsafe}'`);
|
|
8196
|
+
}
|
|
8197
|
+
if (mode === "namespaced" && namespaces.length === 0) {
|
|
8198
|
+
throw invalid("'namespaces' must list at least one namespace when 'mode' is 'namespaced'");
|
|
8294
8199
|
}
|
|
8295
8200
|
return {
|
|
8296
|
-
|
|
8297
|
-
|
|
8201
|
+
decisionsDir: requiredString(raw.decisions_dir, "decisions_dir"),
|
|
8202
|
+
mode,
|
|
8203
|
+
namespaces,
|
|
8204
|
+
annotationPrefix: optionalString(raw.annotation_prefix, "annotation_prefix") ?? "@decision",
|
|
8205
|
+
// @decision(DL-023)
|
|
8206
|
+
annotationExclude: repoRelativePatterns(raw.annotation_exclude, "annotation_exclude"),
|
|
8207
|
+
implementReview: optionalBoolean(raw.implement_review, "implement_review") ?? true,
|
|
8208
|
+
decisionEdits: decisionEdits(raw.decision_edits),
|
|
8209
|
+
snapshotArtifacts: snapshotArtifacts(raw.snapshot_artifacts),
|
|
8210
|
+
...optionalBaseBranch(raw.base_branch)
|
|
8298
8211
|
};
|
|
8299
8212
|
}
|
|
8300
|
-
function
|
|
8301
|
-
|
|
8302
|
-
|
|
8303
|
-
|
|
8304
|
-
|
|
8305
|
-
|
|
8213
|
+
function unknownKeyMessage(key) {
|
|
8214
|
+
let closest;
|
|
8215
|
+
let best = 3;
|
|
8216
|
+
for (const known of CONFIG_KEYS) {
|
|
8217
|
+
const distance = editDistance(key, known);
|
|
8218
|
+
if (distance < best) [closest, best] = [known, distance];
|
|
8219
|
+
}
|
|
8220
|
+
const hint = closest === void 0 ? "" : `; did you mean '${closest}'?`;
|
|
8221
|
+
return `${CONFIG_FILE}: unknown key '${key}'${hint}`;
|
|
8222
|
+
}
|
|
8223
|
+
function editDistance(a, b) {
|
|
8224
|
+
let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
8225
|
+
for (let i = 1; i <= a.length; i++) {
|
|
8226
|
+
const current = [i];
|
|
8227
|
+
for (let j = 1; j <= b.length; j++) {
|
|
8228
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
8229
|
+
current[j] = Math.min(
|
|
8230
|
+
(previous[j] ?? 0) + 1,
|
|
8231
|
+
(current[j - 1] ?? 0) + 1,
|
|
8232
|
+
(previous[j - 1] ?? 0) + cost
|
|
8233
|
+
);
|
|
8234
|
+
}
|
|
8235
|
+
previous = current;
|
|
8306
8236
|
}
|
|
8237
|
+
return previous[b.length] ?? 0;
|
|
8307
8238
|
}
|
|
8308
|
-
function
|
|
8309
|
-
const
|
|
8310
|
-
|
|
8311
|
-
if (
|
|
8312
|
-
|
|
8313
|
-
|
|
8314
|
-
|
|
8315
|
-
);
|
|
8316
|
-
const stage = /* @__PURE__ */ new Set();
|
|
8317
|
-
for (const { path, newId } of renames) {
|
|
8318
|
-
stage.add(path);
|
|
8319
|
-
stage.add(posix2.join(posix2.dirname(path), `${newId}.md`));
|
|
8239
|
+
function optionalBaseBranch(value) {
|
|
8240
|
+
const branch = optionalString(value, "base_branch");
|
|
8241
|
+
if (branch === void 0) return {};
|
|
8242
|
+
if (branch.startsWith("-") || branch === "@" || /[\s~^:?*[\\]|\.\.|@\{|^\/|\/$|\.lock$|\.$|^origin\//.test(branch)) {
|
|
8243
|
+
throw invalid(
|
|
8244
|
+
`'base_branch' must be a branch name such as main or develop, without origin/, got '${branch}'`
|
|
8245
|
+
);
|
|
8320
8246
|
}
|
|
8321
|
-
|
|
8322
|
-
|
|
8323
|
-
|
|
8324
|
-
|
|
8325
|
-
|
|
8326
|
-
|
|
8247
|
+
return { baseBranch: branch };
|
|
8248
|
+
}
|
|
8249
|
+
function invalid(message) {
|
|
8250
|
+
return new DldError(`${CONFIG_FILE}: ${message}`);
|
|
8251
|
+
}
|
|
8252
|
+
function isRecord2(value) {
|
|
8253
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8254
|
+
}
|
|
8255
|
+
function requiredString(value, key) {
|
|
8256
|
+
const result = optionalString(value, key);
|
|
8257
|
+
if (result === void 0) throw invalid(`'${key}' is required`);
|
|
8258
|
+
return result;
|
|
8259
|
+
}
|
|
8260
|
+
function optionalString(value, key) {
|
|
8261
|
+
if (value === void 0 || value === null) return void 0;
|
|
8262
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
8263
|
+
throw invalid(`'${key}' must be a non-empty string`);
|
|
8327
8264
|
}
|
|
8328
|
-
|
|
8329
|
-
|
|
8330
|
-
|
|
8331
|
-
|
|
8332
|
-
};
|
|
8333
|
-
|
|
8334
|
-
|
|
8335
|
-
|
|
8336
|
-
|
|
8337
|
-
};
|
|
8338
|
-
|
|
8339
|
-
|
|
8340
|
-
|
|
8341
|
-
if (existsAtHead(git, indexRel)) {
|
|
8342
|
-
git("--literal-pathspecs", "checkout", "HEAD", "--", indexRel);
|
|
8343
|
-
} else {
|
|
8344
|
-
ctx.fs.remove(indexFull);
|
|
8345
|
-
}
|
|
8346
|
-
});
|
|
8347
|
-
for (const path of [...stage].sort()) {
|
|
8348
|
-
step(`staging ${path}`, () => stagePath(ctx, git, join7(paths.root, path), path));
|
|
8265
|
+
return value;
|
|
8266
|
+
}
|
|
8267
|
+
function optionalBoolean(value, key) {
|
|
8268
|
+
if (value === void 0 || value === null) return void 0;
|
|
8269
|
+
if (typeof value !== "boolean") throw invalid(`'${key}' must be true or false`);
|
|
8270
|
+
return value;
|
|
8271
|
+
}
|
|
8272
|
+
function optionalStringList(value, key) {
|
|
8273
|
+
if (value === void 0 || value === null) return [];
|
|
8274
|
+
if (!Array.isArray(value)) throw invalid(`'${key}' must be a list of strings`);
|
|
8275
|
+
return value.map((item) => {
|
|
8276
|
+
if (typeof item !== "string" || item.trim() === "") {
|
|
8277
|
+
throw invalid(`'${key}' must be a list of non-empty strings`);
|
|
8349
8278
|
}
|
|
8350
|
-
|
|
8351
|
-
|
|
8352
|
-
|
|
8353
|
-
|
|
8354
|
-
|
|
8355
|
-
|
|
8356
|
-
|
|
8357
|
-
|
|
8358
|
-
} catch (error) {
|
|
8359
|
-
throw rollback(ctx, git, indexFull, saved, stepName, error);
|
|
8279
|
+
return item;
|
|
8280
|
+
});
|
|
8281
|
+
}
|
|
8282
|
+
function repoRelativePatterns(value, key) {
|
|
8283
|
+
const patterns = optionalStringList(value, key);
|
|
8284
|
+
const outside = patterns.find((pattern) => /^([/\\]|[A-Za-z]:|\.\.([/\\]|$))/.test(pattern));
|
|
8285
|
+
if (outside !== void 0) {
|
|
8286
|
+
throw invalid(`'${key}' patterns must be relative to the repository root, got '${outside}'`);
|
|
8360
8287
|
}
|
|
8361
|
-
return
|
|
8362
|
-
commit: git("rev-parse", "--short", "HEAD").trim(),
|
|
8363
|
-
onto: git("rev-parse", "--short", onto).trim()
|
|
8364
|
-
};
|
|
8288
|
+
return patterns;
|
|
8365
8289
|
}
|
|
8366
|
-
function
|
|
8367
|
-
|
|
8290
|
+
function decisionEdits(value) {
|
|
8291
|
+
if (value === void 0 || value === null) return "block";
|
|
8292
|
+
const allowed = DECISION_EDITS;
|
|
8293
|
+
if (!allowed.includes(value)) {
|
|
8294
|
+
throw invalid(`'decision_edits' must be one of ${DECISION_EDITS.join(", ")}`);
|
|
8295
|
+
}
|
|
8296
|
+
return value;
|
|
8368
8297
|
}
|
|
8369
|
-
function
|
|
8370
|
-
const
|
|
8371
|
-
if (
|
|
8298
|
+
function snapshotArtifacts(value) {
|
|
8299
|
+
const key = "snapshot_artifacts";
|
|
8300
|
+
if (value === void 0 || value === null) return [];
|
|
8301
|
+
if (!Array.isArray(value)) throw invalid(`'${key}' must be a list of {title, prompt} entries`);
|
|
8302
|
+
return value.map((item) => {
|
|
8303
|
+
if (!isRecord2(item)) throw invalid(`'${key}' entries must have 'title' and 'prompt'`);
|
|
8304
|
+
return {
|
|
8305
|
+
title: requiredString(item.title, `${key}[].title`),
|
|
8306
|
+
prompt: requiredString(item.prompt, `${key}[].prompt`)
|
|
8307
|
+
};
|
|
8308
|
+
});
|
|
8372
8309
|
}
|
|
8373
|
-
|
|
8310
|
+
|
|
8311
|
+
// src/core/project.ts
|
|
8312
|
+
function findProjectRoot(ctx) {
|
|
8374
8313
|
try {
|
|
8375
|
-
git("
|
|
8376
|
-
return false;
|
|
8314
|
+
return ctx.git(["rev-parse", "--show-toplevel"]).trim();
|
|
8377
8315
|
} catch (error) {
|
|
8378
|
-
if (error instanceof GitCommandError
|
|
8316
|
+
if (error instanceof GitCommandError) throw new DldError("not a git repository");
|
|
8379
8317
|
throw error;
|
|
8380
8318
|
}
|
|
8381
8319
|
}
|
|
8382
|
-
function
|
|
8383
|
-
|
|
8384
|
-
|
|
8385
|
-
git("read-tree", saved.tree);
|
|
8386
|
-
if (saved.index === void 0) ctx.fs.remove(indexFull);
|
|
8387
|
-
else writeFileAtomic(ctx, indexFull, saved.index.bytes, saved.index.mode);
|
|
8388
|
-
} catch (rollbackError) {
|
|
8389
|
-
return new DldError(
|
|
8390
|
-
`commit-reindex failed while ${stepName}: ${describe(cause)}
|
|
8391
|
-
Rolling back also failed: ${describe(rollbackError)}
|
|
8392
|
-
To restore by hand: git reset --soft ${saved.head} && git read-tree ${saved.tree}`
|
|
8393
|
-
);
|
|
8394
|
-
}
|
|
8395
|
-
if (!(cause instanceof DldError)) return cause;
|
|
8396
|
-
return new DldError(
|
|
8397
|
-
`commit-reindex failed while ${stepName}: ${cause.message}
|
|
8398
|
-
The branch was restored to ${saved.head}.`,
|
|
8399
|
-
cause.exitCode
|
|
8400
|
-
);
|
|
8320
|
+
function resolvePaths(root, config) {
|
|
8321
|
+
const decisionsDir = join5(root, config.decisionsDir);
|
|
8322
|
+
return { root, decisionsDir, recordsDir: join5(decisionsDir, "records") };
|
|
8401
8323
|
}
|
|
8402
|
-
function
|
|
8403
|
-
|
|
8324
|
+
function loadProject(ctx) {
|
|
8325
|
+
const root = findProjectRoot(ctx);
|
|
8326
|
+
const config = loadConfig(ctx, root);
|
|
8327
|
+
return { config, paths: resolvePaths(root, config) };
|
|
8404
8328
|
}
|
|
8405
8329
|
|
|
8406
8330
|
// src/cli/commands/base-option.ts
|
|
8407
|
-
function baseOption(value,
|
|
8408
|
-
|
|
8409
|
-
|
|
8410
|
-
|
|
8331
|
+
function baseOption(value, defaults) {
|
|
8332
|
+
if (value === void 0 && defaults !== void 0) {
|
|
8333
|
+
return resolveBase(defaults.ctx, defaults.project.config.baseBranch);
|
|
8334
|
+
}
|
|
8335
|
+
if (value === void 0 || value === "") throw new UsageError("--base is required.");
|
|
8336
|
+
if (value.startsWith("-")) throw new UsageError(`--base must be a git ref, got '${value}'`);
|
|
8337
|
+
return value;
|
|
8411
8338
|
}
|
|
8339
|
+
var DEFAULT_BASE_HELP = "default: the base resolve-base picks";
|
|
8412
8340
|
function skippedNotice(reason) {
|
|
8413
8341
|
return `[dld-reindex] open PRs not scanned: ${reason}
|
|
8414
8342
|
`;
|
|
8415
8343
|
}
|
|
8416
8344
|
|
|
8417
|
-
// src/cli/commands/
|
|
8418
|
-
var
|
|
8419
|
-
|
|
8420
|
-
|
|
8421
|
-
|
|
8422
|
-
usage: `Usage: dld
|
|
8345
|
+
// src/cli/commands/check-decision-edits.ts
|
|
8346
|
+
var EXIT_BLOCKED = 3;
|
|
8347
|
+
var checkDecisionEditsCommand = {
|
|
8348
|
+
name: "check-decision-edits",
|
|
8349
|
+
summary: "List edits to the prose of decisions already on the base branch (CI gate)",
|
|
8350
|
+
usage: `Usage: dld check-decision-edits [--base <ref>] [--uncommitted] [DL-NNN ...]
|
|
8423
8351
|
|
|
8424
|
-
|
|
8425
|
-
|
|
8426
|
-
|
|
8427
|
-
|
|
8352
|
+
A decision is integrated when it exists at the merge-base of the base branch and HEAD;
|
|
8353
|
+
otherwise it is a draft (so is one still proposed there). Without IDs, print
|
|
8354
|
+
<path>\\t<DL-NNN>\\t<state> for each integrated decision whose body (the text after the
|
|
8355
|
+
frontmatter), id or timestamp changed (edited), or that is gone (deleted). With IDs, print
|
|
8356
|
+
one line per ID with its state: draft, integrated, edited, deleted or allowed.
|
|
8428
8357
|
|
|
8429
|
-
|
|
8430
|
-
|
|
8431
|
-
|
|
8358
|
+
A commit on the branch with a "Decision-Edit: DL-NNN[, DL-NNN ...]" trailer allows edits to
|
|
8359
|
+
the decisions it names: they are printed as allowed. The trailer must be in the message's last
|
|
8360
|
+
paragraph, which holds only trailers. Trailers do not apply with --uncommitted.
|
|
8361
|
+
|
|
8362
|
+
Without IDs, exits 3 when decision_edits is block (the default) and an edited or deleted
|
|
8363
|
+
decision was printed. With IDs, and otherwise, exits 0. Errors exit 1, usage errors 2.
|
|
8432
8364
|
|
|
8433
8365
|
Options:
|
|
8434
|
-
--base <ref>
|
|
8435
|
-
--
|
|
8366
|
+
--base <ref> Base ref (${DEFAULT_BASE_HELP})
|
|
8367
|
+
--uncommitted Compare with HEAD instead of the merge-base: only uncommitted changes
|
|
8436
8368
|
`,
|
|
8437
8369
|
run(args, io, ctx) {
|
|
8438
|
-
const { values } = parseCommandArgs({
|
|
8370
|
+
const { values, positionals: ids } = parseCommandArgs({
|
|
8439
8371
|
args: [...args],
|
|
8440
|
-
options: { base: { type: "string" },
|
|
8372
|
+
options: { base: { type: "string" }, uncommitted: { type: "boolean" } },
|
|
8373
|
+
allowPositionals: true
|
|
8441
8374
|
});
|
|
8442
|
-
|
|
8443
|
-
|
|
8375
|
+
const bad = ids.find((id) => !DECISION_ID.test(id));
|
|
8376
|
+
if (bad !== void 0) throw new UsageError(`expected decision IDs like DL-001, got '${bad}'`);
|
|
8444
8377
|
const project = loadProject(ctx);
|
|
8445
|
-
const
|
|
8446
|
-
|
|
8378
|
+
const base = editBase(
|
|
8379
|
+
ctx,
|
|
8380
|
+
project,
|
|
8381
|
+
values.base === void 0 ? void 0 : baseOption(values.base)
|
|
8382
|
+
);
|
|
8383
|
+
const edits = checkDecisionEdits(ctx, project, base, ids, {
|
|
8384
|
+
uncommitted: values.uncommitted === true
|
|
8447
8385
|
});
|
|
8448
|
-
|
|
8386
|
+
for (const edit of edits) io.stdout(`${formatRecordEdit(edit)}
|
|
8449
8387
|
`);
|
|
8450
|
-
|
|
8388
|
+
const changed = edits.some(({ state }) => state === "edited" || state === "deleted");
|
|
8389
|
+
const blocked = ids.length === 0 && changed && project.config.decisionEdits === "block";
|
|
8390
|
+
return blocked ? EXIT_BLOCKED : EXIT_OK;
|
|
8451
8391
|
}
|
|
8452
8392
|
};
|
|
8453
8393
|
|
|
8454
|
-
// src/core/
|
|
8455
|
-
import { join as
|
|
8456
|
-
|
|
8457
|
-
|
|
8458
|
-
|
|
8394
|
+
// src/core/snapshot.ts
|
|
8395
|
+
import { basename as basename5, join as join7, relative as relative5 } from "node:path";
|
|
8396
|
+
|
|
8397
|
+
// src/core/state.ts
|
|
8398
|
+
var import_yaml3 = __toESM(require_dist(), 1);
|
|
8399
|
+
import { join as join6, relative as relative4 } from "node:path";
|
|
8400
|
+
var STATE_FILE = ".dld-state.yaml";
|
|
8401
|
+
function statePath(paths) {
|
|
8402
|
+
return join6(paths.decisionsDir, STATE_FILE);
|
|
8403
|
+
}
|
|
8404
|
+
function loadStateDocument(ctx, paths) {
|
|
8405
|
+
const path = statePath(paths);
|
|
8406
|
+
if (!ctx.fs.exists(path)) return new import_yaml3.Document(void 0, { schema: "failsafe" });
|
|
8407
|
+
const source = relative4(paths.root, path);
|
|
8408
|
+
const doc = (0, import_yaml3.parseDocument)(ctx.fs.readFile(path), { schema: "failsafe", logLevel: "error" });
|
|
8409
|
+
const problem = doc.errors[0] ?? doc.warnings[0];
|
|
8410
|
+
if (problem !== void 0) throw new DldError(`${source}: not valid YAML: ${problem.message}`);
|
|
8411
|
+
if (doc.contents !== null && !(0, import_yaml3.isMap)(doc.contents)) {
|
|
8412
|
+
throw new DldError(`${source}: expected a mapping of sections`);
|
|
8459
8413
|
}
|
|
8460
|
-
return
|
|
8414
|
+
return doc;
|
|
8461
8415
|
}
|
|
8462
|
-
function
|
|
8463
|
-
const
|
|
8464
|
-
|
|
8465
|
-
lines.push("annotation_prefix: '@decision'");
|
|
8466
|
-
return `${lines.join("\n")}
|
|
8467
|
-
`;
|
|
8416
|
+
function readStateSection(ctx, paths, section) {
|
|
8417
|
+
const value = loadStateDocument(ctx, paths).toJS()?.[section];
|
|
8418
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? { ...value } : void 0;
|
|
8468
8419
|
}
|
|
8469
|
-
function
|
|
8470
|
-
|
|
8471
|
-
|
|
8472
|
-
|
|
8473
|
-
const path = join8(root, CONFIG_FILE);
|
|
8474
|
-
createFileExclusive(ctx, path, renderConfig(mode, namespaces), `${CONFIG_FILE} already exists.`);
|
|
8475
|
-
return path;
|
|
8420
|
+
function writeStateSection(ctx, paths, section, value) {
|
|
8421
|
+
const doc = loadStateDocument(ctx, paths);
|
|
8422
|
+
doc.set(section, value);
|
|
8423
|
+
writeFileAtomic(ctx, statePath(paths), String(doc));
|
|
8476
8424
|
}
|
|
8477
|
-
function
|
|
8478
|
-
|
|
8479
|
-
|
|
8480
|
-
|
|
8481
|
-
|
|
8482
|
-
|
|
8483
|
-
ctx.
|
|
8484
|
-
|
|
8485
|
-
if (
|
|
8425
|
+
function stateString(section, key) {
|
|
8426
|
+
const value = section?.[key];
|
|
8427
|
+
return typeof value === "string" && value !== "" ? value : void 0;
|
|
8428
|
+
}
|
|
8429
|
+
function shortHead(ctx, root) {
|
|
8430
|
+
try {
|
|
8431
|
+
return ctx.git(["-C", root, "rev-parse", "--short", "HEAD"]).trim();
|
|
8432
|
+
} catch (error) {
|
|
8433
|
+
if (error instanceof GitCommandError) return "unknown";
|
|
8434
|
+
throw error;
|
|
8486
8435
|
}
|
|
8487
8436
|
}
|
|
8488
8437
|
|
|
8489
|
-
// src/
|
|
8490
|
-
var
|
|
8491
|
-
|
|
8492
|
-
|
|
8493
|
-
|
|
8494
|
-
|
|
8495
|
-
|
|
8496
|
-
const
|
|
8497
|
-
|
|
8498
|
-
|
|
8499
|
-
|
|
8500
|
-
|
|
8501
|
-
|
|
8502
|
-
|
|
8503
|
-
const path = createConfig(ctx, findProjectRoot(ctx), parseMode(mode), namespaces);
|
|
8504
|
-
io.stdout(`Created ${path}
|
|
8505
|
-
`);
|
|
8506
|
-
return EXIT_OK;
|
|
8438
|
+
// src/core/snapshot.ts
|
|
8439
|
+
var DECISION_BOUNDARY = "===DLD_DECISION_BOUNDARY===";
|
|
8440
|
+
var BUILT_IN_ARTIFACTS = ["SNAPSHOT.md", "OVERVIEW.md"];
|
|
8441
|
+
var isSnapshotted = (status) => status === "accepted" || status === "proposed";
|
|
8442
|
+
function readRecords(ctx, { paths }) {
|
|
8443
|
+
return listRecordFiles(ctx, paths.recordsDir).map((path) => {
|
|
8444
|
+
const text = ctx.fs.readFile(path);
|
|
8445
|
+
const record = parseRecord(text, relative5(paths.root, path));
|
|
8446
|
+
return { path, number: recordNumber(path), text, status: record.status };
|
|
8447
|
+
}).sort((a, b) => a.number - b.number);
|
|
8448
|
+
}
|
|
8449
|
+
function collectActiveDecisions(ctx, project, { includeProposed = false } = {}) {
|
|
8450
|
+
if (!ctx.fs.isDirectory(project.paths.recordsDir)) {
|
|
8451
|
+
throw new DldError(`records directory not found at ${project.paths.recordsDir}`);
|
|
8507
8452
|
}
|
|
8508
|
-
|
|
8509
|
-
|
|
8510
|
-
|
|
8511
|
-
import { join as join9, relative as relative4 } from "node:path";
|
|
8512
|
-
function updateStatus(ctx, project, id, status) {
|
|
8513
|
-
const path = findRecordFile(ctx, project.paths.recordsDir, id);
|
|
8514
|
-
if (path === void 0) throw new DldError(`decision ${id} not found.`);
|
|
8515
|
-
const text = ctx.fs.readFile(path);
|
|
8516
|
-
writeFileAtomic(ctx, path, setStatus(text, status, relative4(project.paths.root, path)));
|
|
8517
|
-
return path;
|
|
8453
|
+
const wanted = includeProposed ? isSnapshotted : (status) => status === "accepted";
|
|
8454
|
+
return readRecords(ctx, project).filter((record) => wanted(record.status)).map((record) => record.text).join(`${DECISION_BOUNDARY}
|
|
8455
|
+
`);
|
|
8518
8456
|
}
|
|
8519
|
-
function
|
|
8520
|
-
|
|
8521
|
-
|
|
8457
|
+
function detectSnapshotChanges(ctx, project) {
|
|
8458
|
+
const { paths } = project;
|
|
8459
|
+
const state = readStateSection(ctx, paths, "snapshot");
|
|
8460
|
+
if (state === void 0) return { mode: "full" };
|
|
8461
|
+
if (BUILT_IN_ARTIFACTS.some((name) => !ctx.fs.exists(join7(paths.decisionsDir, name)))) {
|
|
8462
|
+
return { mode: "full" };
|
|
8522
8463
|
}
|
|
8523
|
-
const
|
|
8524
|
-
if (
|
|
8525
|
-
|
|
8464
|
+
const included = stateString(state, "decisions_included");
|
|
8465
|
+
if (included === void 0 || !/^\d+$/.test(included)) return { mode: "full" };
|
|
8466
|
+
const includedNumber = Number.parseInt(included, 10);
|
|
8467
|
+
const proposedIncluded = stateString(state, "includes_proposed") === "true";
|
|
8468
|
+
const newDecisions = readRecords(ctx, project).filter(
|
|
8469
|
+
(record) => record.number > includedNumber && isSnapshotted(record.status) || !proposedIncluded && record.status === "proposed"
|
|
8470
|
+
).map((record) => formatId(record.number));
|
|
8471
|
+
const git = gitAt(ctx, paths.root);
|
|
8472
|
+
let stored = stateString(state, "commit_hash");
|
|
8473
|
+
const lastRun = stateString(state, "last_run");
|
|
8474
|
+
if ((stored === void 0 || stored === "unknown") && lastRun !== void 0) {
|
|
8475
|
+
stored = gitOrEmpty(git, "log", `--until=${lastRun}`, "--format=%h", "-1").trim() || void 0;
|
|
8526
8476
|
}
|
|
8527
|
-
const
|
|
8528
|
-
|
|
8529
|
-
|
|
8530
|
-
|
|
8531
|
-
|
|
8532
|
-
|
|
8533
|
-
|
|
8534
|
-
|
|
8535
|
-
|
|
8536
|
-
|
|
8537
|
-
|
|
8538
|
-
|
|
8539
|
-
|
|
8540
|
-
|
|
8541
|
-
return
|
|
8477
|
+
const commit2 = resolveStateCommit(git, stored);
|
|
8478
|
+
let modifiedDecisions = [];
|
|
8479
|
+
let commitRange = "";
|
|
8480
|
+
if (commit2 !== void 0 && commit2 !== shortHead(ctx, paths.root)) {
|
|
8481
|
+
commitRange = `${commit2}..HEAD`;
|
|
8482
|
+
const numbers = nulSeparated(
|
|
8483
|
+
gitOrEmpty(git, "diff", "-z", "--name-only", commitRange, "--", recordsPathspec(paths))
|
|
8484
|
+
).map((path) => basename5(path)).filter((name) => RECORD_FILE.test(name)).map((name) => recordNumber(name)).filter((number) => number <= includedNumber);
|
|
8485
|
+
modifiedDecisions = [...new Set(numbers)].sort((a, b) => a - b).map(formatId);
|
|
8486
|
+
}
|
|
8487
|
+
return { mode: "incremental", newDecisions, modifiedDecisions, commitRange };
|
|
8488
|
+
}
|
|
8489
|
+
function formatSnapshotChanges(changes) {
|
|
8490
|
+
if (changes.mode === "full") return "mode: full\n";
|
|
8491
|
+
return [
|
|
8492
|
+
"mode: incremental",
|
|
8493
|
+
`new_decisions: ${changes.newDecisions.join(", ")}`,
|
|
8494
|
+
`modified_decisions: ${changes.modifiedDecisions.join(", ")}`,
|
|
8495
|
+
`commit_range: ${changes.commitRange}`,
|
|
8496
|
+
""
|
|
8497
|
+
].join("\n");
|
|
8542
8498
|
}
|
|
8543
|
-
function
|
|
8544
|
-
|
|
8499
|
+
function updateSnapshotState(ctx, project, customArtifacts) {
|
|
8500
|
+
const { paths } = project;
|
|
8501
|
+
const timestamp = formatTimestamp(ctx.now());
|
|
8502
|
+
const commit2 = shortHead(ctx, paths.root);
|
|
8503
|
+
const highest = readRecords(ctx, project).filter((record) => isSnapshotted(record.status)).reduce((max, record) => Math.max(max, record.number), 0);
|
|
8504
|
+
const artifacts = /* @__PURE__ */ Object.create(null);
|
|
8505
|
+
for (const name of [...BUILT_IN_ARTIFACTS, ...customArtifacts]) artifacts[name] = timestamp;
|
|
8506
|
+
writeStateSection(ctx, paths, "snapshot", {
|
|
8507
|
+
last_run: timestamp,
|
|
8508
|
+
commit_hash: commit2,
|
|
8509
|
+
decisions_included: String(highest),
|
|
8510
|
+
includes_proposed: "true",
|
|
8511
|
+
artifacts
|
|
8512
|
+
});
|
|
8513
|
+
return { timestamp, commit: commit2, highest };
|
|
8545
8514
|
}
|
|
8546
8515
|
|
|
8547
|
-
// src/cli/commands/
|
|
8548
|
-
var
|
|
8549
|
-
name: "
|
|
8550
|
-
summary: "
|
|
8516
|
+
// src/cli/commands/collect-active-decisions.ts
|
|
8517
|
+
var collectActiveDecisionsCommand = {
|
|
8518
|
+
name: "collect-active-decisions",
|
|
8519
|
+
summary: "Print every accepted decision record",
|
|
8551
8520
|
internal: true,
|
|
8552
|
-
usage: `Usage: dld
|
|
8521
|
+
usage: `Usage: dld collect-active-decisions [--include-proposed]
|
|
8553
8522
|
|
|
8554
|
-
|
|
8523
|
+
Print each accepted record in ID order, separated by ===DLD_DECISION_BOUNDARY=== lines.
|
|
8555
8524
|
|
|
8556
8525
|
Options:
|
|
8557
|
-
--
|
|
8558
|
-
--title <title> Title (required)
|
|
8559
|
-
--namespace <name> Namespace directory (namespaced projects only)
|
|
8560
|
-
--tags <a,b> Comma-separated tags
|
|
8561
|
-
--supersedes <DL-X,...> Decisions this one supersedes
|
|
8562
|
-
--amends <DL-X,...> Decisions this one amends
|
|
8563
|
-
--body-stdin Read the markdown body from standard input
|
|
8526
|
+
--include-proposed Print proposed records too, in the same ID order
|
|
8564
8527
|
`,
|
|
8565
8528
|
run(args, io, ctx) {
|
|
8566
8529
|
const { values } = parseCommandArgs({
|
|
8567
8530
|
args: [...args],
|
|
8568
|
-
options: {
|
|
8569
|
-
id: { type: "string" },
|
|
8570
|
-
title: { type: "string" },
|
|
8571
|
-
namespace: { type: "string" },
|
|
8572
|
-
tags: { type: "string" },
|
|
8573
|
-
supersedes: { type: "string" },
|
|
8574
|
-
amends: { type: "string" },
|
|
8575
|
-
"body-stdin": { type: "boolean" }
|
|
8576
|
-
}
|
|
8577
|
-
});
|
|
8578
|
-
const body = values["body-stdin"] === true ? ctx.readStdin() : "";
|
|
8579
|
-
const { id, title } = values;
|
|
8580
|
-
if (id === void 0 || id === "" || title === void 0 || title === "") {
|
|
8581
|
-
throw new DldError("--id and --title are required.");
|
|
8582
|
-
}
|
|
8583
|
-
const namespace = values.namespace === "" ? void 0 : values.namespace;
|
|
8584
|
-
const path = createDecision(ctx, loadProject(ctx), {
|
|
8585
|
-
id,
|
|
8586
|
-
title,
|
|
8587
|
-
...namespace === void 0 ? {} : { namespace },
|
|
8588
|
-
tags: values.tags ?? "",
|
|
8589
|
-
supersedes: values.supersedes ?? "",
|
|
8590
|
-
amends: values.amends ?? "",
|
|
8591
|
-
body
|
|
8531
|
+
options: { "include-proposed": { type: "boolean" } }
|
|
8592
8532
|
});
|
|
8593
|
-
io.stdout(
|
|
8594
|
-
|
|
8533
|
+
io.stdout(
|
|
8534
|
+
collectActiveDecisions(ctx, loadProject(ctx), {
|
|
8535
|
+
includeProposed: values["include-proposed"] === true
|
|
8536
|
+
})
|
|
8537
|
+
);
|
|
8595
8538
|
return EXIT_OK;
|
|
8596
8539
|
}
|
|
8597
8540
|
};
|
|
8598
8541
|
|
|
8599
|
-
// src/
|
|
8600
|
-
|
|
8601
|
-
name: "create-directories",
|
|
8602
|
-
summary: "Create the decisions directory structure from the config",
|
|
8603
|
-
internal: true,
|
|
8604
|
-
usage: "Usage: dld create-directories\n\nCreate the decisions and records directories, and one per namespace.\n",
|
|
8605
|
-
run(args, io, ctx) {
|
|
8606
|
-
parseCommandArgs({ args: [...args], options: {} });
|
|
8607
|
-
createDirectories(ctx, loadProject(ctx));
|
|
8608
|
-
io.stdout("Created decisions directory structure.\n");
|
|
8609
|
-
return EXIT_OK;
|
|
8610
|
-
}
|
|
8611
|
-
};
|
|
8542
|
+
// src/core/commit-reindex.ts
|
|
8543
|
+
import { join as join8, posix as posix2 } from "node:path";
|
|
8612
8544
|
|
|
8613
|
-
// src/core/
|
|
8614
|
-
import {
|
|
8615
|
-
|
|
8616
|
-
|
|
8617
|
-
|
|
8618
|
-
|
|
8619
|
-
|
|
8620
|
-
""
|
|
8621
|
-
namespaced ? "| ID | Title | Status | Namespace | Tags |" : "| ID | Title | Status | Tags |",
|
|
8622
|
-
namespaced ? "|----|-------|--------|-----------|------|" : "|----|-------|--------|------|"
|
|
8623
|
-
];
|
|
8624
|
-
const sorted = [...rows].sort((a, b) => b.number - a.number);
|
|
8625
|
-
for (const { record } of sorted) {
|
|
8626
|
-
const cells = [record.id, record.title, record.status];
|
|
8627
|
-
if (namespaced) cells.push(record.namespace ?? "");
|
|
8628
|
-
cells.push(record.tags.join(", "));
|
|
8629
|
-
lines.push(`| ${cells.join(" | ")} |`);
|
|
8545
|
+
// src/core/rename-plan.ts
|
|
8546
|
+
import { posix } from "node:path";
|
|
8547
|
+
function renameProblem(paths, rename) {
|
|
8548
|
+
const { path, oldId, newId } = rename;
|
|
8549
|
+
if (!DECISION_ID.test(oldId) || !DECISION_ID.test(newId)) return "IDs must match DL-[0-9]+.";
|
|
8550
|
+
if (!isCanonicalId(newId)) {
|
|
8551
|
+
const canonical = canonicalSpelling(newId);
|
|
8552
|
+
return `new ID '${newId}' is not canonical${canonical === void 0 ? "" : `; use ${canonical}`}.`;
|
|
8630
8553
|
}
|
|
8631
|
-
return
|
|
8632
|
-
|
|
8633
|
-
|
|
8634
|
-
|
|
8635
|
-
return join10(paths.decisionsDir, INDEX_FILE);
|
|
8636
|
-
}
|
|
8637
|
-
function collectIndexRows(ctx, paths, includeBase) {
|
|
8638
|
-
const localFiles = listRecordFiles(ctx, paths.recordsDir);
|
|
8639
|
-
const rows = localFiles.map((path) => ({
|
|
8640
|
-
number: recordNumber(path),
|
|
8641
|
-
record: parseRecord(ctx.fs.readFile(path), relative5(paths.root, path))
|
|
8642
|
-
}));
|
|
8643
|
-
if (includeBase === void 0) return rows;
|
|
8644
|
-
const git = (...args) => ctx.git(["-C", paths.root, ...args]);
|
|
8645
|
-
try {
|
|
8646
|
-
git("rev-parse", "--verify", "--quiet", `${includeBase}^{commit}`);
|
|
8647
|
-
} catch (error) {
|
|
8648
|
-
if (error instanceof GitCommandError) {
|
|
8649
|
-
throw new DldError(`--include-base ref '${includeBase}' not found.`);
|
|
8650
|
-
}
|
|
8651
|
-
throw error;
|
|
8554
|
+
if (oldId === newId) return "old and new IDs are the same.";
|
|
8555
|
+
const segments = path.split("/");
|
|
8556
|
+
if (path.startsWith("/") || segments.some((s) => s === "" || s === "." || s === "..")) {
|
|
8557
|
+
return `path '${path}' must be relative to the project root, without '.' or '..' segments.`;
|
|
8652
8558
|
}
|
|
8653
|
-
const
|
|
8654
|
-
|
|
8655
|
-
|
|
8656
|
-
for (const path of basePaths) {
|
|
8657
|
-
rows.push({
|
|
8658
|
-
number: recordNumber(path),
|
|
8659
|
-
record: parseRecord(git("show", `${includeBase}:${path}`), `${includeBase}:${path}`)
|
|
8660
|
-
});
|
|
8559
|
+
const records = recordsPathspec(paths);
|
|
8560
|
+
if (!path.startsWith(`${records}/`)) {
|
|
8561
|
+
return `path '${path}' is not under ${records}/.`;
|
|
8661
8562
|
}
|
|
8662
|
-
|
|
8563
|
+
if (posix.basename(path) !== `${oldId}.md`) {
|
|
8564
|
+
return `path '${path}' is not named ${oldId}.md.`;
|
|
8565
|
+
}
|
|
8566
|
+
return void 0;
|
|
8663
8567
|
}
|
|
8664
|
-
function
|
|
8665
|
-
|
|
8568
|
+
function parseRenamePlan(paths, text) {
|
|
8569
|
+
const renames = [];
|
|
8570
|
+
const lineNumbers = [];
|
|
8571
|
+
const seen = { path: /* @__PURE__ */ new Set(), old: /* @__PURE__ */ new Set(), new: /* @__PURE__ */ new Set() };
|
|
8572
|
+
text.split("\n").forEach((raw, index) => {
|
|
8573
|
+
const line = raw.endsWith("\r") ? raw.slice(0, -1) : raw;
|
|
8574
|
+
if (line.trim() === "") return;
|
|
8575
|
+
const fail = (message) => new DldError(`rename plan line ${index + 1}: ${message}`, EXIT_USAGE);
|
|
8576
|
+
const fields = line.split(" ");
|
|
8577
|
+
const [path, oldId, newId] = fields;
|
|
8578
|
+
if (fields.length !== 3 || path === void 0 || oldId === void 0 || newId === void 0) {
|
|
8579
|
+
throw fail("expected <path>\\t<DL-OLD>\\t<DL-NEW>.");
|
|
8580
|
+
}
|
|
8581
|
+
const rename = { path, oldId, newId };
|
|
8582
|
+
const problem = renameProblem(paths, rename);
|
|
8583
|
+
if (problem !== void 0) throw fail(problem);
|
|
8584
|
+
if (seen.path.has(path)) throw fail(`path '${path}' appears more than once.`);
|
|
8585
|
+
if (seen.old.has(oldId)) throw fail(`${oldId} is renamed more than once.`);
|
|
8586
|
+
if (seen.new.has(newId)) throw fail(`${newId} is the target of more than one rename.`);
|
|
8587
|
+
seen.path.add(path);
|
|
8588
|
+
seen.old.add(oldId);
|
|
8589
|
+
seen.new.add(newId);
|
|
8590
|
+
renames.push(rename);
|
|
8591
|
+
lineNumbers.push(index + 1);
|
|
8592
|
+
});
|
|
8593
|
+
renames.forEach(({ newId }, index) => {
|
|
8594
|
+
if (seen.old.has(newId)) {
|
|
8595
|
+
throw new DldError(
|
|
8596
|
+
`rename plan line ${lineNumbers[index]}: ${newId} is both renamed and a rename target.`
|
|
8597
|
+
);
|
|
8598
|
+
}
|
|
8599
|
+
});
|
|
8600
|
+
return renames;
|
|
8666
8601
|
}
|
|
8667
8602
|
|
|
8668
|
-
// src/
|
|
8669
|
-
var
|
|
8670
|
-
|
|
8671
|
-
|
|
8672
|
-
|
|
8673
|
-
|
|
8674
|
-
run(args, io, ctx) {
|
|
8675
|
-
parseCommandArgs({ args: [...args], options: {} });
|
|
8676
|
-
const { config, paths } = loadProject(ctx);
|
|
8677
|
-
writeIndex(ctx, paths, renderIndex([], config.mode));
|
|
8678
|
-
io.stdout(`Created ${indexPath(paths)}
|
|
8679
|
-
`);
|
|
8680
|
-
return EXIT_OK;
|
|
8681
|
-
}
|
|
8682
|
-
};
|
|
8603
|
+
// src/core/commit-reindex.ts
|
|
8604
|
+
var SUBJECT_LIST_LIMIT = 3;
|
|
8605
|
+
function reindexMessage(renames, originalSubjects = "") {
|
|
8606
|
+
const pairs = renames.map(({ oldId, newId }) => `${oldId} -> ${newId}`);
|
|
8607
|
+
const subject = renames.length > SUBJECT_LIST_LIMIT ? `reindex ${renames.length} local decisions to avoid base-branch collisions` : `reindex local decisions: ${pairs.join(", ")}`;
|
|
8608
|
+
const message = `${subject}
|
|
8683
8609
|
|
|
8684
|
-
|
|
8685
|
-
|
|
8686
|
-
|
|
8687
|
-
summary: "Report what changed since the last snapshot",
|
|
8688
|
-
internal: true,
|
|
8689
|
-
usage: "Usage: dld detect-snapshot-changes\n\nPrint mode (full or incremental), new_decisions, modified_decisions and commit_range.\n",
|
|
8690
|
-
run(args, io, ctx) {
|
|
8691
|
-
parseCommandArgs({ args: [...args], options: {} });
|
|
8692
|
-
io.stdout(formatSnapshotChanges(detectSnapshotChanges(ctx, loadProject(ctx))));
|
|
8693
|
-
return EXIT_OK;
|
|
8694
|
-
}
|
|
8695
|
-
};
|
|
8610
|
+
Renames:
|
|
8611
|
+
${pairs.map((pair) => `- ${pair}`).join("\n")}`;
|
|
8612
|
+
return originalSubjects === "" ? message : `${message}
|
|
8696
8613
|
|
|
8697
|
-
|
|
8698
|
-
|
|
8699
|
-
var EXCLUDED_DIRS = /* @__PURE__ */ new Set([
|
|
8700
|
-
".git",
|
|
8701
|
-
// Agent configuration directories, which hold installed skills.
|
|
8702
|
-
".claude",
|
|
8703
|
-
".agents",
|
|
8704
|
-
".agent",
|
|
8705
|
-
".codex",
|
|
8706
|
-
".cursor",
|
|
8707
|
-
".opencode",
|
|
8708
|
-
".pi",
|
|
8709
|
-
".tessl",
|
|
8710
|
-
"node_modules",
|
|
8711
|
-
"vendor",
|
|
8712
|
-
".venv",
|
|
8713
|
-
"__pycache__",
|
|
8714
|
-
"target",
|
|
8715
|
-
"dist",
|
|
8716
|
-
"build",
|
|
8717
|
-
"out",
|
|
8718
|
-
".next",
|
|
8719
|
-
".gradle",
|
|
8720
|
-
"coverage"
|
|
8721
|
-
]);
|
|
8722
|
-
var EXCLUDED_FILE = /(\.lock|\.min\.js|\.min\.css|\.map)$/;
|
|
8723
|
-
var BINARY_SNIFF_BYTES = 8192;
|
|
8724
|
-
function scanOptionsFor({ config, paths }) {
|
|
8725
|
-
return {
|
|
8726
|
-
root: paths.root,
|
|
8727
|
-
decisionsDir: paths.decisionsDir,
|
|
8728
|
-
prefix: config.annotationPrefix,
|
|
8729
|
-
exclude: config.annotationExclude
|
|
8730
|
-
};
|
|
8614
|
+
Squashed from original branch commits:
|
|
8615
|
+
${originalSubjects}`;
|
|
8731
8616
|
}
|
|
8732
|
-
function
|
|
8733
|
-
|
|
8734
|
-
|
|
8735
|
-
|
|
8736
|
-
|
|
8737
|
-
|
|
8738
|
-
|
|
8739
|
-
|
|
8740
|
-
|
|
8741
|
-
// @decision(DL-023)
|
|
8742
|
-
"--",
|
|
8743
|
-
...(options.exclude ?? []).map((pattern) => `:(exclude,glob)${pattern}`)
|
|
8744
|
-
]);
|
|
8745
|
-
const decisionsRel = relative6(options.root, options.decisionsDir).split(sep3).join("/");
|
|
8746
|
-
const files = /* @__PURE__ */ new Set();
|
|
8747
|
-
for (const file of output.split("\0")) {
|
|
8748
|
-
if (file === "" || EXCLUDED_FILE.test(file)) continue;
|
|
8749
|
-
if (file === decisionsRel || file.startsWith(`${decisionsRel}/`)) continue;
|
|
8750
|
-
const dirs = file.split("/").slice(0, -1);
|
|
8751
|
-
if (dirs.some((dir) => EXCLUDED_DIRS.has(dir))) continue;
|
|
8752
|
-
files.add(file);
|
|
8617
|
+
function commitReindex(ctx, { paths }, planText, base, { squash = false } = {}) {
|
|
8618
|
+
verifyBase(ctx, paths, base);
|
|
8619
|
+
const renames = parseRenamePlan(paths, planText);
|
|
8620
|
+
if (renames.length === 0) throw new DldError("no rename plan on stdin.");
|
|
8621
|
+
for (const { oldId, newId, path } of renames) {
|
|
8622
|
+
const renamed = posix2.join(posix2.dirname(path), `${newId}.md`);
|
|
8623
|
+
if (!ctx.fs.isRegularFile(join8(paths.root, renamed))) {
|
|
8624
|
+
throw new DldError(`${renamed} not found. Run rename-decision for ${oldId} first.`);
|
|
8625
|
+
}
|
|
8753
8626
|
}
|
|
8754
|
-
|
|
8755
|
-
|
|
8756
|
-
function escapeRegExp(text) {
|
|
8757
|
-
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
8627
|
+
const git = gitAt(ctx, paths.root);
|
|
8628
|
+
return squash ? squashReindex(ctx, git, paths, renames, base) : commitOnTop(ctx, git, paths, renames);
|
|
8758
8629
|
}
|
|
8759
|
-
function
|
|
8760
|
-
const
|
|
8761
|
-
const
|
|
8762
|
-
|
|
8763
|
-
|
|
8764
|
-
if (!ctx.fs.isRegularFile(path)) continue;
|
|
8765
|
-
const text = ctx.fs.readFile(path);
|
|
8766
|
-
if (text.slice(0, BINARY_SNIFF_BYTES).includes("\0")) continue;
|
|
8767
|
-
text.split("\n").forEach((content, index) => {
|
|
8768
|
-
for (const match of content.matchAll(pattern)) {
|
|
8769
|
-
if (match[1] !== void 0) found.push({ file, line: index + 1, id: match[1] });
|
|
8770
|
-
}
|
|
8771
|
-
});
|
|
8630
|
+
function commitOnTop(ctx, git, paths, renames) {
|
|
8631
|
+
const stage = /* @__PURE__ */ new Set();
|
|
8632
|
+
for (const { path, newId } of renames) {
|
|
8633
|
+
stage.add(path);
|
|
8634
|
+
stage.add(posix2.join(posix2.dirname(path), `${newId}.md`));
|
|
8772
8635
|
}
|
|
8773
|
-
|
|
8774
|
-
|
|
8775
|
-
|
|
8776
|
-
const
|
|
8777
|
-
|
|
8636
|
+
for (const file of nulSeparated(git("diff", "-z", "--no-renames", "--name-only", "HEAD"))) {
|
|
8637
|
+
stage.add(file);
|
|
8638
|
+
}
|
|
8639
|
+
const head = git("rev-parse", "HEAD").trim();
|
|
8640
|
+
const tree = git("write-tree").trim();
|
|
8641
|
+
let stepName = "staging";
|
|
8642
|
+
try {
|
|
8643
|
+
for (const path of [...stage].sort()) {
|
|
8644
|
+
stepName = `staging ${path}`;
|
|
8645
|
+
stagePath(ctx, git, join8(paths.root, path), path);
|
|
8646
|
+
}
|
|
8647
|
+
stepName = "checking the staged changes";
|
|
8648
|
+
if (!hasStagedChanges(git)) {
|
|
8649
|
+
throw new DldError(
|
|
8650
|
+
"nothing to commit. The reindex may have already been committed, or the plan didn't match the working tree."
|
|
8651
|
+
);
|
|
8652
|
+
}
|
|
8653
|
+
stepName = "committing";
|
|
8654
|
+
commit(git, reindexMessage(renames));
|
|
8655
|
+
} catch (error) {
|
|
8656
|
+
try {
|
|
8657
|
+
git("read-tree", tree);
|
|
8658
|
+
} catch (restoreError) {
|
|
8659
|
+
throw new DldError(
|
|
8660
|
+
`commit-reindex failed while ${stepName}: ${describe(error)}
|
|
8661
|
+
Restoring the index also failed: ${describe(restoreError)}
|
|
8662
|
+
To restore by hand: git read-tree ${tree}`
|
|
8663
|
+
);
|
|
8664
|
+
}
|
|
8665
|
+
if (!(error instanceof DldError)) throw error;
|
|
8666
|
+
throw new DldError(
|
|
8667
|
+
`commit-reindex failed while ${stepName}: ${error.message}
|
|
8668
|
+
The index was restored.`,
|
|
8669
|
+
error.exitCode
|
|
8670
|
+
);
|
|
8671
|
+
}
|
|
8672
|
+
return {
|
|
8673
|
+
commit: git("rev-parse", "--short", "HEAD").trim(),
|
|
8674
|
+
onto: git("rev-parse", "--short", head).trim()
|
|
8675
|
+
};
|
|
8778
8676
|
}
|
|
8779
|
-
function
|
|
8780
|
-
|
|
8677
|
+
function commit(git, message) {
|
|
8678
|
+
try {
|
|
8679
|
+
git("commit", "--quiet", "-m", message);
|
|
8680
|
+
} catch (error) {
|
|
8681
|
+
if (error instanceof GitCommandError) throw new DldError(`git commit failed: ${error.stderr}`);
|
|
8682
|
+
throw error;
|
|
8683
|
+
}
|
|
8781
8684
|
}
|
|
8782
|
-
|
|
8783
|
-
|
|
8784
|
-
|
|
8785
|
-
|
|
8786
|
-
|
|
8787
|
-
|
|
8788
|
-
|
|
8789
|
-
|
|
8790
|
-
|
|
8791
|
-
|
|
8792
|
-
|
|
8793
|
-
`);
|
|
8794
|
-
}
|
|
8795
|
-
return EXIT_OK;
|
|
8685
|
+
function squashReindex(ctx, git, paths, renames, base) {
|
|
8686
|
+
const onto = mergeBase(ctx, paths, base);
|
|
8687
|
+
const head = git("rev-parse", "HEAD").trim();
|
|
8688
|
+
if (onto === head) throw new DldError("HEAD is already at the merge-base \u2014 nothing to squash.");
|
|
8689
|
+
const indexRel = posix2.join(decisionsPathspec(paths), "INDEX.md");
|
|
8690
|
+
const branchFiles = nulSeparated(
|
|
8691
|
+
git("diff", "-z", "--no-renames", "--name-only", "--diff-filter=AMRD", `${onto}..HEAD`)
|
|
8692
|
+
);
|
|
8693
|
+
const stage = /* @__PURE__ */ new Set();
|
|
8694
|
+
for (const { path, newId } of renames) {
|
|
8695
|
+
stage.add(path);
|
|
8696
|
+
stage.add(posix2.join(posix2.dirname(path), `${newId}.md`));
|
|
8796
8697
|
}
|
|
8797
|
-
|
|
8798
|
-
|
|
8799
|
-
|
|
8800
|
-
|
|
8801
|
-
|
|
8802
|
-
|
|
8698
|
+
for (const file of branchFiles) if (file !== indexRel) stage.add(file);
|
|
8699
|
+
const subjects = git("log", "--reverse", "--format=- %s", `${onto}..HEAD`).replace(/\n+$/, "");
|
|
8700
|
+
const message = reindexMessage(renames, subjects);
|
|
8701
|
+
const indexFull = join8(paths.root, indexRel);
|
|
8702
|
+
if (ctx.fs.lexists(indexFull) && !ctx.fs.isRegularFile(indexFull)) {
|
|
8703
|
+
throw new DldError(`${indexRel} is not a regular file.`);
|
|
8704
|
+
}
|
|
8705
|
+
const saved = {
|
|
8706
|
+
head,
|
|
8707
|
+
tree: git("write-tree").trim(),
|
|
8708
|
+
index: ctx.fs.isRegularFile(indexFull) ? { bytes: ctx.fs.readBytes(indexFull), mode: ctx.fs.fileMode(indexFull) } : void 0
|
|
8709
|
+
};
|
|
8710
|
+
let stepName = "resetting to the merge-base";
|
|
8711
|
+
const step = (name, run2) => {
|
|
8712
|
+
stepName = name;
|
|
8713
|
+
return run2();
|
|
8714
|
+
};
|
|
8715
|
+
try {
|
|
8716
|
+
step("resetting to the merge-base", () => git("reset", "--quiet", onto));
|
|
8717
|
+
step(`restoring ${indexRel}`, () => {
|
|
8718
|
+
if (existsAtHead(git, indexRel)) {
|
|
8719
|
+
git("--literal-pathspecs", "checkout", "HEAD", "--", indexRel);
|
|
8720
|
+
} else {
|
|
8721
|
+
ctx.fs.remove(indexFull);
|
|
8722
|
+
}
|
|
8723
|
+
});
|
|
8724
|
+
for (const path of [...stage].sort()) {
|
|
8725
|
+
step(`staging ${path}`, () => stagePath(ctx, git, join8(paths.root, path), path));
|
|
8726
|
+
}
|
|
8727
|
+
step("checking the staged changes", () => {
|
|
8728
|
+
if (!hasStagedChanges(git)) {
|
|
8729
|
+
throw new DldError(
|
|
8730
|
+
"nothing to commit after squash. The reindex may have already been applied, or the plan didn't match the branch state."
|
|
8731
|
+
);
|
|
8732
|
+
}
|
|
8733
|
+
});
|
|
8734
|
+
step("committing", () => commit(git, message));
|
|
8735
|
+
} catch (error) {
|
|
8736
|
+
throw rollback(ctx, git, indexFull, saved, stepName, error);
|
|
8737
|
+
}
|
|
8738
|
+
return {
|
|
8739
|
+
commit: git("rev-parse", "--short", "HEAD").trim(),
|
|
8740
|
+
onto: git("rev-parse", "--short", onto).trim()
|
|
8741
|
+
};
|
|
8742
|
+
}
|
|
8743
|
+
function existsAtHead(git, path) {
|
|
8744
|
+
return git("--literal-pathspecs", "ls-tree", "-z", "HEAD", "--", path) !== "";
|
|
8745
|
+
}
|
|
8746
|
+
function stagePath(ctx, git, full, path) {
|
|
8747
|
+
const known = ctx.fs.lexists(full) || git("--literal-pathspecs", "ls-files", "-z", "--", path) !== "";
|
|
8748
|
+
if (known) git("--literal-pathspecs", "add", "-A", "--", path);
|
|
8749
|
+
}
|
|
8750
|
+
function hasStagedChanges(git) {
|
|
8751
|
+
try {
|
|
8752
|
+
git("diff", "--cached", "--quiet");
|
|
8753
|
+
return false;
|
|
8754
|
+
} catch (error) {
|
|
8755
|
+
if (error instanceof GitCommandError && error.status === 1) return true;
|
|
8756
|
+
throw error;
|
|
8757
|
+
}
|
|
8758
|
+
}
|
|
8759
|
+
function rollback(ctx, git, indexFull, saved, stepName, cause) {
|
|
8760
|
+
try {
|
|
8761
|
+
git("reset", "--quiet", "--soft", saved.head);
|
|
8762
|
+
git("read-tree", saved.tree);
|
|
8763
|
+
if (saved.index === void 0) ctx.fs.remove(indexFull);
|
|
8764
|
+
else writeFileAtomic(ctx, indexFull, saved.index.bytes, saved.index.mode);
|
|
8765
|
+
} catch (rollbackError) {
|
|
8766
|
+
return new DldError(
|
|
8767
|
+
`commit-reindex failed while ${stepName}: ${describe(cause)}
|
|
8768
|
+
Rolling back also failed: ${describe(rollbackError)}
|
|
8769
|
+
To restore by hand: git reset --soft ${saved.head} && git read-tree ${saved.tree}`
|
|
8770
|
+
);
|
|
8771
|
+
}
|
|
8772
|
+
if (!(cause instanceof DldError)) return cause;
|
|
8773
|
+
return new DldError(
|
|
8774
|
+
`commit-reindex failed while ${stepName}: ${cause.message}
|
|
8775
|
+
The branch was restored to ${saved.head}.`,
|
|
8776
|
+
cause.exitCode
|
|
8777
|
+
);
|
|
8778
|
+
}
|
|
8779
|
+
function describe(error) {
|
|
8780
|
+
return error instanceof Error ? error.message : String(error);
|
|
8781
|
+
}
|
|
8782
|
+
|
|
8783
|
+
// src/cli/commands/commit-reindex.ts
|
|
8784
|
+
var commitReindexCommand = {
|
|
8785
|
+
name: "commit-reindex",
|
|
8786
|
+
summary: "Commit the renamed decisions",
|
|
8803
8787
|
internal: true,
|
|
8804
|
-
usage: `Usage: dld
|
|
8788
|
+
usage: `Usage: dld commit-reindex --base <ref> [--squash] < plan
|
|
8805
8789
|
|
|
8806
|
-
|
|
8807
|
-
|
|
8790
|
+
Read a rename plan on standard input and commit the renames, which rename-decision has
|
|
8791
|
+
already applied. By default this adds one commit on top of HEAD with the plan's paths and
|
|
8792
|
+
every tracked file changed since HEAD; untracked files are left out. On failure the index is
|
|
8793
|
+
restored.
|
|
8794
|
+
|
|
8795
|
+
With --squash, the branch's commits since the merge-base with <ref> are squashed into one
|
|
8796
|
+
reindex commit instead, and INDEX.md is left at its merge-base state. On failure the branch,
|
|
8797
|
+
index and INDEX.md are restored.
|
|
8808
8798
|
|
|
8809
8799
|
Options:
|
|
8810
|
-
--base <ref> Base ref (
|
|
8800
|
+
--base <ref> Base ref (required)
|
|
8801
|
+
--squash Squash the branch instead of adding a commit
|
|
8811
8802
|
`,
|
|
8812
8803
|
run(args, io, ctx) {
|
|
8813
|
-
const { values } = parseCommandArgs({
|
|
8804
|
+
const { values } = parseCommandArgs({
|
|
8805
|
+
args: [...args],
|
|
8806
|
+
options: { base: { type: "string" }, squash: { type: "boolean" } }
|
|
8807
|
+
});
|
|
8814
8808
|
const base = baseOption(values.base);
|
|
8815
|
-
const
|
|
8816
|
-
|
|
8809
|
+
const project = loadProject(ctx);
|
|
8810
|
+
const { commit: commit2, onto } = commitReindex(ctx, project, ctx.readStdin(), base, {
|
|
8811
|
+
squash: values.squash === true
|
|
8812
|
+
});
|
|
8813
|
+
io.stdout(`Created reindex commit ${commit2} on top of ${onto}
|
|
8817
8814
|
`);
|
|
8818
|
-
if (skipped !== void 0) io.stderr(skippedNotice(skipped));
|
|
8819
8815
|
return EXIT_OK;
|
|
8820
8816
|
}
|
|
8821
8817
|
};
|
|
8822
8818
|
|
|
8823
|
-
// src/core/
|
|
8824
|
-
import {
|
|
8825
|
-
|
|
8826
|
-
|
|
8827
|
-
|
|
8828
|
-
|
|
8829
|
-
|
|
8830
|
-
const stored = stateString(readStateSection(ctx, paths, "audit"), "commit_hash");
|
|
8831
|
-
const commit2 = resolveStateCommit(git, stored);
|
|
8832
|
-
if (commit2 === void 0) return void 0;
|
|
8833
|
-
const records = recordsPathspec(paths);
|
|
8834
|
-
return /* @__PURE__ */ new Set([
|
|
8835
|
-
...nulSeparated(git("diff", "-z", "--name-only", commit2, "--", records)),
|
|
8836
|
-
...nulSeparated(git("ls-files", "-z", "--others", "--exclude-standard", "--", records))
|
|
8837
|
-
]);
|
|
8819
|
+
// src/core/init.ts
|
|
8820
|
+
import { join as join9 } from "node:path";
|
|
8821
|
+
function parseMode(value) {
|
|
8822
|
+
if (value !== "flat" && value !== "namespaced") {
|
|
8823
|
+
throw new DldError(`mode must be 'flat' or 'namespaced', got '${value}'.`);
|
|
8824
|
+
}
|
|
8825
|
+
return value;
|
|
8838
8826
|
}
|
|
8839
|
-
function
|
|
8840
|
-
const
|
|
8841
|
-
|
|
8842
|
-
|
|
8843
|
-
|
|
8844
|
-
|
|
8845
|
-
|
|
8846
|
-
|
|
8847
|
-
|
|
8848
|
-
|
|
8849
|
-
const text = ctx.fs.readFile(file);
|
|
8850
|
-
const record = parseRecord(text, rel);
|
|
8851
|
-
const source = basename6(file, ".md");
|
|
8852
|
-
const declared = /* @__PURE__ */ new Set([...record.supersedes, ...record.amends]);
|
|
8853
|
-
const mentioned = new Set(recordBody(text).match(MENTION) ?? []);
|
|
8854
|
-
const refs = [...mentioned].filter((id) => id !== source && !declared.has(id)).sort((a, b) => idNumber2(a) - idNumber2(b));
|
|
8855
|
-
for (const referenced of refs) found.push({ source, referenced });
|
|
8827
|
+
function renderConfig(mode, namespaces) {
|
|
8828
|
+
const lines = ["decisions_dir: decisions", `mode: ${mode}`];
|
|
8829
|
+
if (mode === "namespaced") lines.push("namespaces:", ...namespaces.map((ns) => ` - ${ns}`));
|
|
8830
|
+
lines.push("annotation_prefix: '@decision'");
|
|
8831
|
+
return `${lines.join("\n")}
|
|
8832
|
+
`;
|
|
8833
|
+
}
|
|
8834
|
+
function createConfig(ctx, root, mode, namespaces) {
|
|
8835
|
+
if (mode === "namespaced" && namespaces.length === 0) {
|
|
8836
|
+
throw new DldError("namespaced mode requires at least one namespace.");
|
|
8856
8837
|
}
|
|
8857
|
-
|
|
8838
|
+
const path = join9(root, CONFIG_FILE);
|
|
8839
|
+
createFileExclusive(ctx, path, renderConfig(mode, namespaces), `${CONFIG_FILE} already exists.`);
|
|
8840
|
+
return path;
|
|
8858
8841
|
}
|
|
8859
|
-
function
|
|
8860
|
-
|
|
8861
|
-
|
|
8862
|
-
|
|
8863
|
-
|
|
8842
|
+
function createDirectories(ctx, { config, paths }) {
|
|
8843
|
+
ctx.fs.mkdir(paths.decisionsDir);
|
|
8844
|
+
ctx.fs.mkdir(paths.recordsDir);
|
|
8845
|
+
if (config.mode !== "namespaced") return;
|
|
8846
|
+
for (const namespace of config.namespaces) {
|
|
8847
|
+
const dir = join9(paths.recordsDir, namespace);
|
|
8848
|
+
ctx.fs.mkdir(dir);
|
|
8849
|
+
const keep = join9(dir, ".gitkeep");
|
|
8850
|
+
if (!ctx.fs.exists(keep)) createFileExclusive(ctx, keep, "", `${keep} already exists.`);
|
|
8851
|
+
}
|
|
8864
8852
|
}
|
|
8865
8853
|
|
|
8866
|
-
// src/cli/commands/
|
|
8867
|
-
var
|
|
8868
|
-
name: "
|
|
8869
|
-
summary: "
|
|
8854
|
+
// src/cli/commands/create-config.ts
|
|
8855
|
+
var createConfigCommand = {
|
|
8856
|
+
name: "create-config",
|
|
8857
|
+
summary: "Create dld.config.yaml at the project root",
|
|
8870
8858
|
internal: true,
|
|
8871
|
-
usage:
|
|
8872
|
-
|
|
8873
|
-
Print <source-id>:<referenced-id> for each decision ID a record's body mentions without
|
|
8874
|
-
listing it in supersedes or amends. Only records changed since the last audit are checked.
|
|
8875
|
-
|
|
8876
|
-
Options:
|
|
8877
|
-
--all Check every record
|
|
8878
|
-
`,
|
|
8859
|
+
usage: "Usage: dld create-config <flat|namespaced> [namespace ...]\n\nCreate dld.config.yaml. Namespaced mode needs at least one namespace.\n",
|
|
8879
8860
|
run(args, io, ctx) {
|
|
8880
|
-
const {
|
|
8881
|
-
|
|
8882
|
-
|
|
8861
|
+
const { positionals } = parseCommandArgs({
|
|
8862
|
+
args: [...args],
|
|
8863
|
+
options: {},
|
|
8864
|
+
allowPositionals: true
|
|
8865
|
+
});
|
|
8866
|
+
const [mode, ...namespaces] = positionals;
|
|
8867
|
+
if (mode === void 0) throw new UsageError("missing <flat|namespaced>");
|
|
8868
|
+
const path = createConfig(ctx, findProjectRoot(ctx), parseMode(mode), namespaces);
|
|
8869
|
+
io.stdout(`Created ${path}
|
|
8883
8870
|
`);
|
|
8884
8871
|
return EXIT_OK;
|
|
8885
8872
|
}
|
|
8886
8873
|
};
|
|
8887
8874
|
|
|
8888
|
-
// src/core/
|
|
8889
|
-
import {
|
|
8890
|
-
|
|
8891
|
-
|
|
8892
|
-
|
|
8893
|
-
|
|
8894
|
-
|
|
8895
|
-
|
|
8896
|
-
|
|
8897
|
-
|
|
8898
|
-
|
|
8899
|
-
|
|
8900
|
-
|
|
8901
|
-
|
|
8875
|
+
// src/core/decisions.ts
|
|
8876
|
+
import { join as join10, relative as relative6 } from "node:path";
|
|
8877
|
+
function updateStatus(ctx, project, id, status) {
|
|
8878
|
+
const path = findRecordFile(ctx, project.paths.recordsDir, id, project.paths.root);
|
|
8879
|
+
if (path === void 0) throw new DldError(`decision ${id} not found.`);
|
|
8880
|
+
const text = ctx.fs.readFile(path);
|
|
8881
|
+
writeFileAtomic(ctx, path, setStatus(text, status, relative6(project.paths.root, path)));
|
|
8882
|
+
return path;
|
|
8883
|
+
}
|
|
8884
|
+
function createDecision(ctx, project, input) {
|
|
8885
|
+
if (!isCanonicalId(input.id)) {
|
|
8886
|
+
const canonical = canonicalSpelling(input.id);
|
|
8887
|
+
throw new DldError(
|
|
8888
|
+
canonical === void 0 ? `invalid decision ID '${input.id}'; expected DL-<digits>, e.g. DL-001.` : `decision ID '${input.id}' is not canonical; use ${canonical}.`,
|
|
8889
|
+
EXIT_USAGE
|
|
8890
|
+
);
|
|
8891
|
+
}
|
|
8892
|
+
const number = Number.parseInt(input.id.slice(3), 10);
|
|
8893
|
+
const { config } = project;
|
|
8894
|
+
if (input.namespace !== void 0) {
|
|
8895
|
+
if (config.mode !== "namespaced") {
|
|
8896
|
+
throw new DldError("--namespace is only valid in a namespaced project.", EXIT_USAGE);
|
|
8897
|
+
}
|
|
8898
|
+
if (!config.namespaces.includes(input.namespace)) {
|
|
8899
|
+
throw new DldError(
|
|
8900
|
+
`unknown namespace '${input.namespace}'; configured namespaces: ${config.namespaces.join(", ")}.`,
|
|
8901
|
+
EXIT_USAGE
|
|
8902
|
+
);
|
|
8903
|
+
}
|
|
8904
|
+
}
|
|
8905
|
+
const existing = listRecordFiles(ctx, project.paths.recordsDir).find(
|
|
8906
|
+
(path2) => recordNumber(path2) === number
|
|
8902
8907
|
);
|
|
8903
|
-
|
|
8904
|
-
|
|
8905
|
-
|
|
8906
|
-
|
|
8907
|
-
|
|
8908
|
-
const
|
|
8909
|
-
const
|
|
8910
|
-
|
|
8911
|
-
|
|
8912
|
-
|
|
8913
|
-
|
|
8914
|
-
|
|
8915
|
-
|
|
8916
|
-
|
|
8917
|
-
|
|
8918
|
-
|
|
8919
|
-
|
|
8920
|
-
|
|
8921
|
-
|
|
8922
|
-
|
|
8923
|
-
|
|
8924
|
-
if (ctx.fs.lexists(newFull)) throw new DldError(`${newPath} already exists.`);
|
|
8925
|
-
verifyBase(ctx, paths, base);
|
|
8926
|
-
const since = mergeBase(ctx, paths, base);
|
|
8927
|
-
const scannable = new Set(listScannableFiles(ctx, scanOptionsFor(project)));
|
|
8928
|
-
gitAt(ctx, paths.root)("mv", "--", path, newPath);
|
|
8929
|
-
const substitute = (text) => text.replace(idPattern(oldId, "g"), newId);
|
|
8930
|
-
rewriteFile(ctx, newFull, (text) => substitute(setId(text, oldId, newId)));
|
|
8931
|
-
const decisions = `${decisionsPathspec(paths)}/`;
|
|
8932
|
-
const prefix = Buffer2.from(config.annotationPrefix, "utf8").toString("latin1");
|
|
8933
|
-
const oldAnnotation = `${prefix}(${oldId})`;
|
|
8934
|
-
const newAnnotation = `${prefix}(${newId})`;
|
|
8935
|
-
for (const file of changedFiles(ctx, paths, since)) {
|
|
8936
|
-
if (file === newPath) continue;
|
|
8937
|
-
if (file.startsWith(decisions)) {
|
|
8938
|
-
rewriteFile(ctx, join12(paths.root, file), substitute);
|
|
8939
|
-
} else if (scannable.has(file)) {
|
|
8940
|
-
rewriteFile(
|
|
8941
|
-
ctx,
|
|
8942
|
-
join12(paths.root, file),
|
|
8943
|
-
(text) => text.split(oldAnnotation).join(newAnnotation)
|
|
8944
|
-
);
|
|
8945
|
-
}
|
|
8946
|
-
}
|
|
8947
|
-
return newPath;
|
|
8948
|
-
}
|
|
8949
|
-
function findStaleMentions(ctx, { paths }, planText, base) {
|
|
8950
|
-
const renames = parseRenamePlan(paths, planText);
|
|
8951
|
-
if (renames.length === 0) throw new DldError("no rename plan on stdin.");
|
|
8952
|
-
verifyBase(ctx, paths, base);
|
|
8953
|
-
const decisions = `${decisionsPathspec(paths)}/`;
|
|
8954
|
-
const files = changedFiles(ctx, paths, mergeBase(ctx, paths, base)).flatMap((file) => {
|
|
8955
|
-
if (file.startsWith(decisions)) return [];
|
|
8956
|
-
const full = join12(paths.root, file);
|
|
8957
|
-
if (!ctx.fs.isRegularFile(full)) return [];
|
|
8958
|
-
const text = ctx.fs.readFile(full);
|
|
8959
|
-
if (text.slice(0, BINARY_SNIFF_BYTES).includes("\0")) return [];
|
|
8960
|
-
return [{ file, lines: text.split("\n").map((line) => line.replace(/\r$/, "")) }];
|
|
8961
|
-
});
|
|
8962
|
-
const found = [];
|
|
8963
|
-
for (const { oldId, newId } of renames) {
|
|
8964
|
-
const pattern = idPattern(oldId);
|
|
8965
|
-
for (const { file, lines } of files) {
|
|
8966
|
-
lines.forEach((text, index) => {
|
|
8967
|
-
if (pattern.test(text)) found.push({ file, line: index + 1, oldId, newId, text });
|
|
8968
|
-
});
|
|
8969
|
-
}
|
|
8970
|
-
}
|
|
8971
|
-
return found;
|
|
8972
|
-
}
|
|
8973
|
-
function formatStaleMention({ file, line, oldId, newId, text }) {
|
|
8974
|
-
return `${file} ${line} ${oldId} ${newId} ${text}`;
|
|
8908
|
+
if (existing !== void 0) {
|
|
8909
|
+
throw new DldError(
|
|
8910
|
+
`decision ${input.id} already exists: ${relative6(project.paths.root, existing)}`
|
|
8911
|
+
);
|
|
8912
|
+
}
|
|
8913
|
+
const namespaced = input.namespace !== void 0;
|
|
8914
|
+
const dir = namespaced ? join10(project.paths.recordsDir, input.namespace ?? "") : project.paths.recordsDir;
|
|
8915
|
+
ctx.fs.mkdir(dir);
|
|
8916
|
+
const path = join10(dir, `${input.id}.md`);
|
|
8917
|
+
const content = renderNewRecord({
|
|
8918
|
+
id: input.id,
|
|
8919
|
+
title: input.title,
|
|
8920
|
+
timestamp: formatTimestamp(ctx.now()),
|
|
8921
|
+
...namespaced && input.namespace !== void 0 ? { namespace: input.namespace } : {},
|
|
8922
|
+
tags: input.tags,
|
|
8923
|
+
supersedes: input.supersedes,
|
|
8924
|
+
amends: input.amends,
|
|
8925
|
+
body: input.body
|
|
8926
|
+
});
|
|
8927
|
+
createFileExclusive(ctx, path, content, `${path} already exists.`);
|
|
8928
|
+
return path;
|
|
8975
8929
|
}
|
|
8976
8930
|
|
|
8977
|
-
// src/cli/commands/
|
|
8978
|
-
var
|
|
8979
|
-
name: "
|
|
8980
|
-
summary: "
|
|
8931
|
+
// src/cli/commands/create-decision.ts
|
|
8932
|
+
var createDecisionCommand = {
|
|
8933
|
+
name: "create-decision",
|
|
8934
|
+
summary: "Create a proposed decision record",
|
|
8981
8935
|
internal: true,
|
|
8982
|
-
usage: `Usage: dld
|
|
8936
|
+
usage: `Usage: dld create-decision --id <DL-NNN> --title <title> [options]
|
|
8983
8937
|
|
|
8984
|
-
|
|
8985
|
-
<path>\\t<line>\\t<DL-OLD>\\t<DL-NEW>\\t<text> for each remaining DL-OLD mention in changed
|
|
8986
|
-
files outside the decisions directory.
|
|
8938
|
+
Create a decision record with status proposed and print its path.
|
|
8987
8939
|
|
|
8988
8940
|
Options:
|
|
8989
|
-
--
|
|
8941
|
+
--id <DL-NNN> Decision ID (required)
|
|
8942
|
+
--title <title> Title (required)
|
|
8943
|
+
--namespace <name> Namespace directory (namespaced projects only)
|
|
8944
|
+
--tags <a,b> Comma-separated tags
|
|
8945
|
+
--supersedes <DL-X,...> Decisions this one supersedes
|
|
8946
|
+
--amends <DL-X,...> Decisions this one amends
|
|
8947
|
+
--body-stdin Read the markdown body from standard input
|
|
8990
8948
|
`,
|
|
8991
8949
|
run(args, io, ctx) {
|
|
8992
|
-
const { values } = parseCommandArgs({
|
|
8993
|
-
|
|
8994
|
-
|
|
8995
|
-
|
|
8996
|
-
|
|
8997
|
-
|
|
8998
|
-
|
|
8950
|
+
const { values } = parseCommandArgs({
|
|
8951
|
+
args: [...args],
|
|
8952
|
+
options: {
|
|
8953
|
+
id: { type: "string" },
|
|
8954
|
+
title: { type: "string" },
|
|
8955
|
+
namespace: { type: "string" },
|
|
8956
|
+
tags: { type: "string" },
|
|
8957
|
+
supersedes: { type: "string" },
|
|
8958
|
+
amends: { type: "string" },
|
|
8959
|
+
"body-stdin": { type: "boolean" }
|
|
8960
|
+
}
|
|
8961
|
+
});
|
|
8962
|
+
const body = values["body-stdin"] === true ? ctx.readStdin() : "";
|
|
8963
|
+
const { id, title } = values;
|
|
8964
|
+
if (id === void 0 || id === "" || title === void 0 || title === "") {
|
|
8965
|
+
throw new UsageError("--id and --title are required.");
|
|
8999
8966
|
}
|
|
8967
|
+
const namespace = values.namespace === "" ? void 0 : values.namespace;
|
|
8968
|
+
const path = createDecision(ctx, loadProject(ctx), {
|
|
8969
|
+
id,
|
|
8970
|
+
title,
|
|
8971
|
+
...namespace === void 0 ? {} : { namespace },
|
|
8972
|
+
tags: values.tags ?? "",
|
|
8973
|
+
supersedes: values.supersedes ?? "",
|
|
8974
|
+
amends: values.amends ?? "",
|
|
8975
|
+
body
|
|
8976
|
+
});
|
|
8977
|
+
io.stdout(`${path}
|
|
8978
|
+
`);
|
|
9000
8979
|
return EXIT_OK;
|
|
9001
8980
|
}
|
|
9002
8981
|
};
|
|
9003
8982
|
|
|
9004
|
-
// src/cli/commands/
|
|
9005
|
-
|
|
8983
|
+
// src/cli/commands/create-directories.ts
|
|
8984
|
+
var createDirectoriesCommand = {
|
|
8985
|
+
name: "create-directories",
|
|
8986
|
+
summary: "Create the decisions directory structure from the config",
|
|
8987
|
+
internal: true,
|
|
8988
|
+
usage: "Usage: dld create-directories\n\nCreate the decisions and records directories, and one per namespace.\n",
|
|
8989
|
+
run(args, io, ctx) {
|
|
8990
|
+
parseCommandArgs({ args: [...args], options: {} });
|
|
8991
|
+
createDirectories(ctx, loadProject(ctx));
|
|
8992
|
+
io.stdout("Created decisions directory structure.\n");
|
|
8993
|
+
return EXIT_OK;
|
|
8994
|
+
}
|
|
8995
|
+
};
|
|
8996
|
+
|
|
8997
|
+
// src/core/index-file.ts
|
|
8998
|
+
import { basename as basename6, join as join11, relative as relative7, sep as sep3 } from "node:path";
|
|
8999
|
+
var INDEX_FILE = "INDEX.md";
|
|
9000
|
+
function renderIndex(rows, mode) {
|
|
9001
|
+
const namespaced = mode === "namespaced";
|
|
9002
|
+
const lines = [
|
|
9003
|
+
"# Decision Log",
|
|
9004
|
+
"",
|
|
9005
|
+
namespaced ? "| ID | Title | Status | Namespace | Tags |" : "| ID | Title | Status | Tags |",
|
|
9006
|
+
namespaced ? "|----|-------|--------|-----------|------|" : "|----|-------|--------|------|"
|
|
9007
|
+
];
|
|
9008
|
+
const sorted = [...rows].sort((a, b) => b.number - a.number);
|
|
9009
|
+
for (const { record } of sorted) {
|
|
9010
|
+
const cells = [record.id, record.title, record.status];
|
|
9011
|
+
if (namespaced) cells.push(record.namespace ?? "");
|
|
9012
|
+
cells.push(record.tags.join(", "));
|
|
9013
|
+
lines.push(`| ${cells.join(" | ")} |`);
|
|
9014
|
+
}
|
|
9015
|
+
return `${lines.join("\n")}
|
|
9016
|
+
`;
|
|
9017
|
+
}
|
|
9018
|
+
function indexPath(paths) {
|
|
9019
|
+
return join11(paths.decisionsDir, INDEX_FILE);
|
|
9020
|
+
}
|
|
9021
|
+
function collectIndexRows(ctx, paths, includeBase) {
|
|
9022
|
+
const localFiles = listRecordFiles(ctx, paths.recordsDir);
|
|
9023
|
+
const rows = localFiles.map((path) => ({
|
|
9024
|
+
number: recordNumber(path),
|
|
9025
|
+
record: parseRecord(ctx.fs.readFile(path), relative7(paths.root, path))
|
|
9026
|
+
}));
|
|
9027
|
+
if (includeBase === void 0) return rows;
|
|
9028
|
+
const git = (...args) => ctx.git(["-C", paths.root, ...args]);
|
|
9029
|
+
try {
|
|
9030
|
+
git("rev-parse", "--verify", "--quiet", `${includeBase}^{commit}`);
|
|
9031
|
+
} catch (error) {
|
|
9032
|
+
if (error instanceof GitCommandError) {
|
|
9033
|
+
throw new DldError(`--include-base ref '${includeBase}' not found.`);
|
|
9034
|
+
}
|
|
9035
|
+
throw error;
|
|
9036
|
+
}
|
|
9037
|
+
const localNames = new Set(localFiles.map((path) => basename6(path)));
|
|
9038
|
+
const recordsRel = relative7(paths.root, paths.recordsDir).split(sep3).join("/");
|
|
9039
|
+
const basePaths = git("ls-tree", "-r", "--name-only", includeBase, "--", recordsRel).split("\n").filter((path) => RECORD_FILE.test(basename6(path)) && !localNames.has(basename6(path)));
|
|
9040
|
+
for (const path of basePaths) {
|
|
9041
|
+
rows.push({
|
|
9042
|
+
number: recordNumber(path),
|
|
9043
|
+
record: parseRecord(git("show", `${includeBase}:${path}`), `${includeBase}:${path}`)
|
|
9044
|
+
});
|
|
9045
|
+
}
|
|
9046
|
+
return rows;
|
|
9047
|
+
}
|
|
9048
|
+
function writeIndex(ctx, paths, content) {
|
|
9049
|
+
writeFileAtomic(ctx, indexPath(paths), content);
|
|
9050
|
+
}
|
|
9051
|
+
|
|
9052
|
+
// src/cli/install.ts
|
|
9053
|
+
import { relative as relative8, sep as sep4 } from "node:path";
|
|
9054
|
+
|
|
9055
|
+
// src/generate/gitattributes.ts
|
|
9056
|
+
import { join as join12 } from "node:path";
|
|
9057
|
+
var GITATTRIBUTES = ".gitattributes";
|
|
9058
|
+
var INDEX_MERGE_COMMENT = "# dld-kit: keep both sides' rows when merging the decision index";
|
|
9059
|
+
var MERGE_ATTRIBUTE = /^([-!]?merge(=|$)|binary$)/;
|
|
9060
|
+
var SPECIAL = /[\s"\\*?[\]]|^[!#]|(^|\/)\.\.(\/|$)/;
|
|
9061
|
+
function unquote(pattern) {
|
|
9062
|
+
return pattern.length > 1 && pattern.startsWith('"') && pattern.endsWith('"') ? pattern.slice(1, -1).replace(/\\(.)/g, "$1") : pattern;
|
|
9063
|
+
}
|
|
9064
|
+
function planIndexMergeAttribute(ctx, root, index) {
|
|
9065
|
+
const path = join12(root, GITATTRIBUTES);
|
|
9066
|
+
const wanted = `/${index} merge=union`;
|
|
9067
|
+
if (SPECIAL.test(index)) {
|
|
9068
|
+
return {
|
|
9069
|
+
warnings: [
|
|
9070
|
+
`the decision index ${index} cannot be named in ${GITATTRIBUTES} as a plain pattern, so dld-kit does not add its merge=union line. Add a line that matches it yourself, or two branches that add decisions will conflict on it.`
|
|
9071
|
+
]
|
|
9072
|
+
};
|
|
9073
|
+
}
|
|
9074
|
+
if (ctx.fs.lexists(path) && !ctx.fs.isRegularFile(path)) {
|
|
9075
|
+
return {
|
|
9076
|
+
warnings: [
|
|
9077
|
+
`${GITATTRIBUTES} is not a regular file, so dld-kit does not edit it. Add this line to it yourself: ${wanted}`
|
|
9078
|
+
]
|
|
9079
|
+
};
|
|
9080
|
+
}
|
|
9081
|
+
const text = ctx.fs.lexists(path) ? ctx.fs.readFile(path) : "";
|
|
9082
|
+
const lines = text.split(/\r?\n/);
|
|
9083
|
+
const warnings = [];
|
|
9084
|
+
let set = false;
|
|
9085
|
+
lines.forEach((line, index_) => {
|
|
9086
|
+
const [raw, ...attributes] = line.trim().split(/\s+/);
|
|
9087
|
+
if (raw === void 0 || raw === "" || raw.startsWith("#")) return;
|
|
9088
|
+
const pattern = unquote(raw);
|
|
9089
|
+
const forIndex = pattern === `/${index}` || pattern === index;
|
|
9090
|
+
if (forIndex && attributes.some((attribute) => MERGE_ATTRIBUTE.test(attribute))) set = true;
|
|
9091
|
+
if (!forIndex && lines[index_ - 1] === INDEX_MERGE_COMMENT) {
|
|
9092
|
+
warnings.push(
|
|
9093
|
+
`${GITATTRIBUTES} still sets a merge driver for ${pattern}, an earlier decisions directory. Remove that line if the index is no longer there.`
|
|
9094
|
+
);
|
|
9095
|
+
}
|
|
9096
|
+
});
|
|
9097
|
+
if (set) return { warnings };
|
|
9098
|
+
const eol = text.includes("\r\n") ? "\r\n" : "\n";
|
|
9099
|
+
const separator = text === "" ? "" : text.endsWith("\n") ? eol : eol + eol;
|
|
9100
|
+
return {
|
|
9101
|
+
content: `${text}${separator}${INDEX_MERGE_COMMENT}${eol}${wanted}${eol}`,
|
|
9102
|
+
creates: !ctx.fs.lexists(path),
|
|
9103
|
+
warnings
|
|
9104
|
+
};
|
|
9105
|
+
}
|
|
9106
|
+
function applyIndexMergeAttribute(ctx, root, plan) {
|
|
9107
|
+
if (plan.content === void 0) return false;
|
|
9108
|
+
writeFileAtomic(ctx, join12(root, GITATTRIBUTES), plan.content);
|
|
9109
|
+
return true;
|
|
9110
|
+
}
|
|
9006
9111
|
|
|
9007
9112
|
// src/generate/harnesses.ts
|
|
9008
9113
|
import { join as join13 } from "node:path";
|
|
@@ -9219,491 +9324,995 @@ var HARNESS_NAMES = HARNESSES.map((harness) => harness.name);
|
|
|
9219
9324
|
function findHarness(name) {
|
|
9220
9325
|
return HARNESSES.find((harness) => harness.name === name);
|
|
9221
9326
|
}
|
|
9222
|
-
function detectHarnesses(ctx, root) {
|
|
9223
|
-
const found = (marker) => ctx.fs.lexists(join13(root, marker));
|
|
9224
|
-
const detected = [];
|
|
9225
|
-
for (const harness of HARNESSES) {
|
|
9226
|
-
const marker = harness.markers.find(found);
|
|
9227
|
-
if (marker !== void 0) detected.push({ harness, marker });
|
|
9327
|
+
function detectHarnesses(ctx, root) {
|
|
9328
|
+
const found = (marker) => ctx.fs.lexists(join13(root, marker));
|
|
9329
|
+
const detected = [];
|
|
9330
|
+
for (const harness of HARNESSES) {
|
|
9331
|
+
const marker = harness.markers.find(found);
|
|
9332
|
+
if (marker !== void 0) detected.push({ harness, marker });
|
|
9333
|
+
}
|
|
9334
|
+
const generic = GENERIC_MARKERS.find(found);
|
|
9335
|
+
const agentsLayoutFound = detected.some(({ harness }) => harness.layout === AGENTS_LAYOUT);
|
|
9336
|
+
const codex = findHarness("codex");
|
|
9337
|
+
if (generic !== void 0 && !agentsLayoutFound && codex !== void 0) {
|
|
9338
|
+
detected.push({ harness: codex, marker: generic });
|
|
9339
|
+
detected.sort((a, b) => HARNESSES.indexOf(a.harness) - HARNESSES.indexOf(b.harness));
|
|
9340
|
+
}
|
|
9341
|
+
return detected;
|
|
9342
|
+
}
|
|
9343
|
+
function targetsFor(harnesses) {
|
|
9344
|
+
return {
|
|
9345
|
+
layouts: new Set(harnesses.map((harness) => harness.layout)),
|
|
9346
|
+
rules: new Set(harnesses.map((harness) => harness.rule))
|
|
9347
|
+
};
|
|
9348
|
+
}
|
|
9349
|
+
|
|
9350
|
+
// src/generate/rule.ts
|
|
9351
|
+
import { dirname as dirname2, join as join14, posix as posix3 } from "node:path";
|
|
9352
|
+
|
|
9353
|
+
// templates/rules/dld-workflow.md
|
|
9354
|
+
var dld_workflow_default = "# DLD (Decision-Linked Development)\n\nThis project uses Decision-Linked Development. Decision records (DL-*.md) live in the `records/` subdirectory of the decisions directory set in `dld.config.yaml` (`decisions/` by default). High-level docs (INDEX.md, OVERVIEW.md, SNAPSHOT.md) live in the decisions directory.\n\n## Rules\n\n- When you encounter `@decision(DL-XXX)` annotations in code, read the referenced decision with the dld-lookup skill BEFORE modifying the annotated code.\n- ALWAYS look up and verify related decisions before modifying annotated code. Do not skip this step.\n- NEVER modify code in a way that contradicts an existing decision without first confirming with the user. If the change requires breaking a previous decision, a new decision must be recorded (with the dld-decide skill) that explicitly supersedes the old one. If it only partially modifies a previous decision, record it as an amendment instead.\n- Decision records that are not on the base branch yet (e.g. `main`), or still `proposed` there, are drafts: edit them freely, whatever their status. On records already on the base branch, the frontmatter can always be updated (except `id` and `timestamp`), but change the prose below it only through the dld-adjust skill, which follows `decision_edits` in `dld.config.yaml`: `block` (the default) never edits it and records an amendment or superseding decision instead, `ask` edits after the user confirms, `allow` edits.\n\n## Skills\n\n- dld-decide: record a new decision\n- dld-plan: break down a feature into multiple grouped decisions\n- dld-implement: implement proposed decisions\n- dld-lookup: query decisions by ID, tag, or code path\n- dld-adjust: adjust or update existing decisions\n- dld-audit: scan for drift between decisions and code\n- dld-snapshot: regenerate SNAPSHOT.md and OVERVIEW.md from the decision log\n- dld-status: a quick overview of the decision log state\n- dld-retrofit: generate decisions from an existing codebase\n- dld-reindex: resolve decision-ID collisions with the base branch and open PRs\n";
|
|
9355
|
+
|
|
9356
|
+
// src/generate/rule.ts
|
|
9357
|
+
var RULE_TEXT = dld_workflow_default;
|
|
9358
|
+
var RULE_SOURCE = "templates/rules/dld-workflow.md";
|
|
9359
|
+
var DEFAULT_PREFIX = "@decision";
|
|
9360
|
+
function ruleTextFor(prefix) {
|
|
9361
|
+
return RULE_TEXT.replaceAll(`\`${DEFAULT_PREFIX}(`, () => `\`${prefix}(`);
|
|
9362
|
+
}
|
|
9363
|
+
function projectRuleText(ctx, root) {
|
|
9364
|
+
if (!ctx.fs.exists(join14(root, CONFIG_FILE))) return RULE_TEXT;
|
|
9365
|
+
try {
|
|
9366
|
+
return ruleTextFor(loadConfig({ ...ctx, warn: () => {
|
|
9367
|
+
} }, root).annotationPrefix);
|
|
9368
|
+
} catch {
|
|
9369
|
+
return RULE_TEXT;
|
|
9370
|
+
}
|
|
9371
|
+
}
|
|
9372
|
+
var CLAUDE_RULE_FILE = ".claude/rules/dld-workflow.md";
|
|
9373
|
+
var AGENTS_RULE_FILE = ".agents/rules/dld-workflow.md";
|
|
9374
|
+
var CLAUDE_IMPORT_FILE = ".claude/CLAUDE.md";
|
|
9375
|
+
var BLOCK_START = "<!-- dld-kit:start -->";
|
|
9376
|
+
var BLOCK_END = "<!-- dld-kit:end -->";
|
|
9377
|
+
var AGENTS_MD = "AGENTS.md";
|
|
9378
|
+
var CLAUDE_MD = "CLAUDE.md";
|
|
9379
|
+
var CLAUDE_FILES = [CLAUDE_MD, CLAUDE_IMPORT_FILE, "CLAUDE.local.md"];
|
|
9380
|
+
var SHARED_CLAUDE_FILES = [CLAUDE_MD, CLAUDE_IMPORT_FILE];
|
|
9381
|
+
var RULE_FILES = {
|
|
9382
|
+
"claude-file": CLAUDE_RULE_FILE,
|
|
9383
|
+
"agents-file": AGENTS_RULE_FILE
|
|
9384
|
+
};
|
|
9385
|
+
function notice(version2, what) {
|
|
9386
|
+
return `<!-- Generated by dld-kit ${version2} from ${RULE_SOURCE}. Do not edit: updating the rule overwrites this ${what}. -->`;
|
|
9387
|
+
}
|
|
9388
|
+
var STAMP = /<!-- Generated by dld-kit (\S+) from /;
|
|
9389
|
+
function ruleVersion(content) {
|
|
9390
|
+
return STAMP.exec(content)?.[1];
|
|
9391
|
+
}
|
|
9392
|
+
function renderRuleFile(channel, text, version2) {
|
|
9393
|
+
const frontmatter = channel === "agents-file" ? "---\ntrigger: always_on\n---\n" : "";
|
|
9394
|
+
return `${frontmatter}${notice(version2, "file")}
|
|
9395
|
+
|
|
9396
|
+
${text}`;
|
|
9397
|
+
}
|
|
9398
|
+
function renderImportFile(version2) {
|
|
9399
|
+
return `<!-- Generated by dld-kit ${version2} for Claude Code: it loads AGENTS.md, which holds the DLD rule, through the import below. Do not edit: updating the rule overwrites this file. -->
|
|
9400
|
+
|
|
9401
|
+
@../AGENTS.md
|
|
9402
|
+
`;
|
|
9403
|
+
}
|
|
9404
|
+
var IMPORT_STAMP = /^<!-- Generated by dld-kit (\S+) for Claude Code:/;
|
|
9405
|
+
function importFileVersion(content) {
|
|
9406
|
+
const version2 = IMPORT_STAMP.exec(content)?.[1];
|
|
9407
|
+
if (version2 === void 0) return void 0;
|
|
9408
|
+
const afterNotice = (text) => text.replace(/\r\n/g, "\n").replace(/^[^\n]*\n/, "");
|
|
9409
|
+
return afterNotice(content) === afterNotice(renderImportFile(version2)) ? version2 : void 0;
|
|
9410
|
+
}
|
|
9411
|
+
function editedImportWarnings(content) {
|
|
9412
|
+
if (!IMPORT_STAMP.test(content) || importFileVersion(content) !== void 0) return [];
|
|
9413
|
+
const warning = `${CLAUDE_IMPORT_FILE} holds content dld-kit did not write, so dld-kit leaves it as it is and no longer updates it. Remove its "Generated by dld-kit" line to keep it as your own file.`;
|
|
9414
|
+
if (importsAgentsMd(content, CLAUDE_IMPORT_FILE)) return [warning];
|
|
9415
|
+
return [
|
|
9416
|
+
`${warning} It no longer imports AGENTS.md, so Claude Code may not load the DLD rule from it: add the line @../AGENTS.md back.`
|
|
9417
|
+
];
|
|
9418
|
+
}
|
|
9419
|
+
function renderBlock(text, version2) {
|
|
9420
|
+
const body = text.endsWith("\n") ? text.slice(0, -1) : text;
|
|
9421
|
+
return [BLOCK_START, notice(version2, "block"), "", ...body.split("\n"), BLOCK_END];
|
|
9422
|
+
}
|
|
9423
|
+
function findBlock(content, file) {
|
|
9424
|
+
const starts = [];
|
|
9425
|
+
const ends = [];
|
|
9426
|
+
let offset = 0;
|
|
9427
|
+
for (const line of content.split("\n")) {
|
|
9428
|
+
const text = line.endsWith("\r") ? line.slice(0, -1) : line;
|
|
9429
|
+
if (text === BLOCK_START) starts.push(offset);
|
|
9430
|
+
if (text === BLOCK_END) ends.push(offset);
|
|
9431
|
+
offset += line.length + 1;
|
|
9432
|
+
}
|
|
9433
|
+
if (starts.length === 0 && ends.length === 0) return void 0;
|
|
9434
|
+
const [from] = starts;
|
|
9435
|
+
const [end] = ends;
|
|
9436
|
+
if (starts.length !== 1 || ends.length !== 1 || from === void 0 || end === void 0) {
|
|
9437
|
+
throw malformed(file);
|
|
9438
|
+
}
|
|
9439
|
+
if (end < from) throw malformed(file);
|
|
9440
|
+
return { from, to: end + BLOCK_END.length };
|
|
9441
|
+
}
|
|
9442
|
+
function malformed(file) {
|
|
9443
|
+
return new DldError(
|
|
9444
|
+
`${file}: expected one '${BLOCK_START}' line followed by one '${BLOCK_END}' line; fix the dld-kit markers by hand`
|
|
9445
|
+
);
|
|
9446
|
+
}
|
|
9447
|
+
function upsertBlock(content, block, file) {
|
|
9448
|
+
const eol = content.includes("\r\n") ? "\r\n" : "\n";
|
|
9449
|
+
const rendered = block.join(eol);
|
|
9450
|
+
const span = findBlock(content, file);
|
|
9451
|
+
if (span !== void 0) return content.slice(0, span.from) + rendered + content.slice(span.to);
|
|
9452
|
+
if (content === "") return rendered + eol;
|
|
9453
|
+
const separator = content.endsWith("\n") ? eol : eol + eol;
|
|
9454
|
+
return content + separator + rendered + eol;
|
|
9455
|
+
}
|
|
9456
|
+
function planRule(ctx, root, channels, version2, { harnesses = [] } = {}, text = RULE_TEXT) {
|
|
9457
|
+
const plan = { writes: [], removals: [], warnings: [] };
|
|
9458
|
+
const regular = (file) => ctx.fs.isRegularFile(join14(root, file));
|
|
9459
|
+
const read = (file) => regular(file) ? ctx.fs.readFile(join14(root, file)) : "";
|
|
9460
|
+
const resolved = (file) => {
|
|
9461
|
+
const full = join14(root, file);
|
|
9462
|
+
return ctx.fs.exists(full) ? ctx.fs.realPath(full) : full;
|
|
9463
|
+
};
|
|
9464
|
+
let blockFiles = [AGENTS_MD, CLAUDE_MD].filter(
|
|
9465
|
+
(file) => regular(file) && findBlock(read(file), file) !== void 0
|
|
9466
|
+
);
|
|
9467
|
+
if (blockFiles.length === 0 && channels.has("block")) {
|
|
9468
|
+
const targets = harnesses.filter((h) => h.rule === "block");
|
|
9469
|
+
const file = blockPlacement(ctx, root, targets);
|
|
9470
|
+
blockFiles = [file];
|
|
9471
|
+
plan.warnings.push(...switchWarnings(ctx, root, file, targets));
|
|
9472
|
+
}
|
|
9473
|
+
const pending = /* @__PURE__ */ new Map();
|
|
9474
|
+
for (const file of blockFiles) {
|
|
9475
|
+
const content = read(file);
|
|
9476
|
+
const updated = upsertBlock(content, renderBlock(text, version2), file);
|
|
9477
|
+
pending.set(file, updated);
|
|
9478
|
+
if (updated !== content) plan.writes.push({ path: file, content: updated });
|
|
9479
|
+
}
|
|
9480
|
+
const linkedDir = symlinkedPart(ctx, root, dirname2(CLAUDE_IMPORT_FILE));
|
|
9481
|
+
const ownsImport = linkedDir === void 0 && importFileVersion(read(CLAUDE_IMPORT_FILE)) !== void 0;
|
|
9482
|
+
const wantsImport = channels.has("claude-file") && blockFiles.includes(AGENTS_MD) && !SHARED_CLAUDE_FILES.some((file) => ctx.fs.lexists(join14(root, file)));
|
|
9483
|
+
if (ownsImport || wantsImport && linkedDir === void 0) {
|
|
9484
|
+
const content = renderImportFile(version2);
|
|
9485
|
+
pending.set(CLAUDE_IMPORT_FILE, content);
|
|
9486
|
+
if (read(CLAUDE_IMPORT_FILE) !== content) {
|
|
9487
|
+
plan.writes.push({ path: CLAUDE_IMPORT_FILE, content });
|
|
9488
|
+
}
|
|
9489
|
+
} else if (wantsImport) {
|
|
9490
|
+
plan.warnings.push(
|
|
9491
|
+
`${linkedDir} is a symlink, so dld-kit does not write ${CLAUDE_IMPORT_FILE}, which would import AGENTS.md for Claude Code. Claude Code reads the rule block in AGENTS.md by itself only while the project has no CLAUDE.md, ${CLAUDE_IMPORT_FILE} or CLAUDE.local.md.`
|
|
9492
|
+
);
|
|
9493
|
+
}
|
|
9494
|
+
const withBlock = new Set(blockFiles.map(resolved));
|
|
9495
|
+
const current = projectView(ctx, root);
|
|
9496
|
+
const view = {
|
|
9497
|
+
exists: (file) => pending.has(file) || current.exists(file),
|
|
9498
|
+
read: (file) => pending.get(file) ?? current.read(file)
|
|
9499
|
+
};
|
|
9500
|
+
const reads = (harness) => instructionFiles(harness, view)[0];
|
|
9501
|
+
const readsBlock = (harness) => instructionFiles(harness, view).some((file) => withBlock.has(resolved(file)));
|
|
9502
|
+
const ownerOf = (channel) => HARNESSES.find((h) => h.rule === channel);
|
|
9503
|
+
const loadsBlock = (channel) => {
|
|
9504
|
+
const owner = ownerOf(channel);
|
|
9505
|
+
return owner !== void 0 && readsBlock(owner);
|
|
9506
|
+
};
|
|
9507
|
+
if (blockFiles.length > 0) {
|
|
9508
|
+
plan.warnings.push(
|
|
9509
|
+
...blindWarnings(
|
|
9510
|
+
blockFiles,
|
|
9511
|
+
harnesses.filter((h) => h.rule === "block" && !readsBlock(h)),
|
|
9512
|
+
reads
|
|
9513
|
+
)
|
|
9514
|
+
);
|
|
9515
|
+
}
|
|
9516
|
+
for (const channel of ["claude-file", "agents-file"]) {
|
|
9517
|
+
const path = RULE_FILES[channel];
|
|
9518
|
+
if (loadsBlock(channel)) {
|
|
9519
|
+
if (ctx.fs.lexists(join14(root, path))) plan.removals.push(path);
|
|
9520
|
+
} else if (channels.has(channel)) {
|
|
9521
|
+
refuseSymlinkedDir(ctx, root, dirname2(path));
|
|
9522
|
+
const content = renderRuleFile(channel, text, version2);
|
|
9523
|
+
if (read(path) !== content) plan.writes.push({ path, content });
|
|
9524
|
+
}
|
|
9525
|
+
}
|
|
9526
|
+
const ruleInstalled = blockFiles.length > 0 || channels.size > 0;
|
|
9527
|
+
plan.warnings.push(...legacyBlockWarnings(read(CLAUDE_MD), ruleInstalled));
|
|
9528
|
+
if (linkedDir === void 0)
|
|
9529
|
+
plan.warnings.push(...editedImportWarnings(read(CLAUDE_IMPORT_FILE)));
|
|
9530
|
+
return plan;
|
|
9531
|
+
}
|
|
9532
|
+
function projectView(ctx, root) {
|
|
9533
|
+
const exists = (file) => ctx.fs.exists(join14(root, file));
|
|
9534
|
+
return {
|
|
9535
|
+
exists,
|
|
9536
|
+
read: (file) => exists(file) && !ctx.fs.isDirectory(join14(root, file)) ? ctx.fs.readFile(join14(root, file)) : ""
|
|
9537
|
+
};
|
|
9538
|
+
}
|
|
9539
|
+
function instructionFiles(harness, view) {
|
|
9540
|
+
if (harness.name !== "claude") {
|
|
9541
|
+
if (harness.readsAll) return harness.instructions.filter((f) => view.exists(f));
|
|
9542
|
+
const file = harness.instructions.find((f) => view.exists(f));
|
|
9543
|
+
return file === void 0 ? [] : [file];
|
|
9544
|
+
}
|
|
9545
|
+
const files = CLAUDE_FILES.filter((file) => view.exists(file));
|
|
9546
|
+
if (files.length === 0) return view.exists(AGENTS_MD) ? [AGENTS_MD] : [];
|
|
9547
|
+
const imported = files.some((file) => importsAgentsMd(view.read(file), file));
|
|
9548
|
+
return imported && view.exists(AGENTS_MD) ? [...files, AGENTS_MD] : files;
|
|
9549
|
+
}
|
|
9550
|
+
var FENCE = /^ {0,3}(`{3,}|~{3,})/;
|
|
9551
|
+
function importsAgentsMd(content, file) {
|
|
9552
|
+
const dir = posix3.dirname(file);
|
|
9553
|
+
let fence;
|
|
9554
|
+
for (const line of content.split(/\r?\n/)) {
|
|
9555
|
+
const marker = FENCE.exec(line)?.[1];
|
|
9556
|
+
if (marker !== void 0) {
|
|
9557
|
+
if (fence === void 0) fence = marker;
|
|
9558
|
+
else if (marker[0] === fence[0] && marker.length >= fence.length) fence = void 0;
|
|
9559
|
+
continue;
|
|
9560
|
+
}
|
|
9561
|
+
if (fence !== void 0) continue;
|
|
9562
|
+
const prose = line.replace(/`[^`]*`/g, "");
|
|
9563
|
+
for (const match of prose.matchAll(/(?:^|\s)@(\S+)/g)) {
|
|
9564
|
+
const target = match[1];
|
|
9565
|
+
if (target !== void 0 && posix3.normalize(posix3.join(dir, target)) === AGENTS_MD) {
|
|
9566
|
+
return true;
|
|
9567
|
+
}
|
|
9568
|
+
}
|
|
9569
|
+
}
|
|
9570
|
+
return false;
|
|
9571
|
+
}
|
|
9572
|
+
function blindWarnings(blockFiles, blind, reads) {
|
|
9573
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
9574
|
+
for (const harness of blind) {
|
|
9575
|
+
const file = reads(harness) ?? harness.instructions[0] ?? AGENTS_MD;
|
|
9576
|
+
byFile.set(file, [...byFile.get(file) ?? [], harness]);
|
|
9577
|
+
}
|
|
9578
|
+
return [...byFile].map(([file, harnesses]) => {
|
|
9579
|
+
const names = harnesses.map((h) => h.title).join(" and ");
|
|
9580
|
+
const one = harnesses.length === 1;
|
|
9581
|
+
return `The dld-kit rule block is in ${blockFiles.join(" and ")}, but ${names} ${one ? "reads" : "read"} ${file}. To cover ${one ? "it" : "them"}, move the block (the dld-kit:start line through the dld-kit:end line) into ${file}, then run dld install-rule.`;
|
|
9582
|
+
});
|
|
9583
|
+
}
|
|
9584
|
+
function blockPlacement(ctx, root, targets) {
|
|
9585
|
+
if (ctx.fs.isRegularFile(join14(root, AGENTS_MD))) return AGENTS_MD;
|
|
9586
|
+
const agentsLinked = ctx.fs.lexists(join14(root, AGENTS_MD));
|
|
9587
|
+
if (ctx.fs.isRegularFile(join14(root, CLAUDE_MD)) && (agentsLinked || targets.every((h) => h.instructions.includes(CLAUDE_MD)))) {
|
|
9588
|
+
return CLAUDE_MD;
|
|
9589
|
+
}
|
|
9590
|
+
if (agentsLinked) {
|
|
9591
|
+
throw new DldError(
|
|
9592
|
+
`${AGENTS_MD} is not a regular file (a symlink?); dld-kit does not edit through it. Point it at a regular file or add the dld-kit block by hand.`
|
|
9593
|
+
);
|
|
9594
|
+
}
|
|
9595
|
+
return AGENTS_MD;
|
|
9596
|
+
}
|
|
9597
|
+
function switchWarnings(ctx, root, file, targets) {
|
|
9598
|
+
if (file !== AGENTS_MD || ctx.fs.lexists(join14(root, AGENTS_MD))) return [];
|
|
9599
|
+
const claudeMd = join14(root, CLAUDE_MD);
|
|
9600
|
+
if (!ctx.fs.exists(claudeMd) || ctx.fs.isDirectory(claudeMd)) return [];
|
|
9601
|
+
const switching = targets.filter((h) => !h.readsAll && h.instructions.includes(CLAUDE_MD));
|
|
9602
|
+
if (switching.length === 0) return [];
|
|
9603
|
+
const names = switching.map((h) => h.title).join(" and ");
|
|
9604
|
+
return [
|
|
9605
|
+
`dld-kit puts the rule block in a new AGENTS.md. ${names} will read AGENTS.md instead of CLAUDE.md from now on, so move anything ${switching.length === 1 ? "it needs" : "they need"} from CLAUDE.md into AGENTS.md.`
|
|
9606
|
+
];
|
|
9607
|
+
}
|
|
9608
|
+
var LEGACY_HEADING = /^## DLD \(Decision-Linked Development\)\s*$/;
|
|
9609
|
+
function legacyBlockWarnings(claudeMd, ruleInstalled) {
|
|
9610
|
+
const span = findBlock(claudeMd, CLAUDE_MD);
|
|
9611
|
+
const outside = span === void 0 ? claudeMd : claudeMd.slice(0, span.from) + claudeMd.slice(span.to);
|
|
9612
|
+
if (!outside.split(/\r?\n/).some((line) => LEGACY_HEADING.test(line))) return [];
|
|
9613
|
+
const section = "CLAUDE.md has a '## DLD (Decision-Linked Development)' section from an older dld-init.";
|
|
9614
|
+
return [
|
|
9615
|
+
ruleInstalled ? `${section} dld-kit now installs the rule separately, so that section can be removed.` : `${section} Install the rule with dld install-rule --agent <name> before removing it.`
|
|
9616
|
+
];
|
|
9617
|
+
}
|
|
9618
|
+
function loadsRule(ctx, root, harness) {
|
|
9619
|
+
if (harness.rule !== "block" && ctx.fs.exists(join14(root, RULE_FILES[harness.rule]))) {
|
|
9620
|
+
return true;
|
|
9621
|
+
}
|
|
9622
|
+
const view = projectView(ctx, root);
|
|
9623
|
+
return instructionFiles(harness, view).some(
|
|
9624
|
+
(file) => view.read(file).split(/\r?\n/).some((line) => line === BLOCK_START || LEGACY_HEADING.test(line))
|
|
9625
|
+
);
|
|
9626
|
+
}
|
|
9627
|
+
function sessionContext(ctx, root, harness, text) {
|
|
9628
|
+
if (!ctx.fs.exists(join14(root, CONFIG_FILE))) return void 0;
|
|
9629
|
+
return loadsRule(ctx, root, harness) ? void 0 : text ?? projectRuleText(ctx, root);
|
|
9630
|
+
}
|
|
9631
|
+
function symlinkedPart(ctx, root, dir) {
|
|
9632
|
+
let current = "";
|
|
9633
|
+
for (const part of dir.split("/")) {
|
|
9634
|
+
current = current === "" ? part : `${current}/${part}`;
|
|
9635
|
+
const full = join14(root, current);
|
|
9636
|
+
if (!ctx.fs.lexists(full)) return void 0;
|
|
9637
|
+
if (!ctx.fs.exists(full) || ctx.fs.realPath(full) !== join14(ctx.fs.realPath(root), current)) {
|
|
9638
|
+
return current;
|
|
9639
|
+
}
|
|
9640
|
+
}
|
|
9641
|
+
return void 0;
|
|
9642
|
+
}
|
|
9643
|
+
function refuseSymlinkedDir(ctx, root, dir) {
|
|
9644
|
+
const linked = symlinkedPart(ctx, root, dir);
|
|
9645
|
+
if (linked !== void 0) {
|
|
9646
|
+
throw new DldError(
|
|
9647
|
+
`${linked} is a symlink; dld-kit does not install through symlinks. Replace it with a directory (dld-kit keeps each agent's copy separate).`
|
|
9648
|
+
);
|
|
9649
|
+
}
|
|
9650
|
+
}
|
|
9651
|
+
function installedRuleChannels(ctx, root) {
|
|
9652
|
+
const channels = /* @__PURE__ */ new Set();
|
|
9653
|
+
for (const channel of ["claude-file", "agents-file"]) {
|
|
9654
|
+
if (ctx.fs.lexists(join14(root, RULE_FILES[channel]))) channels.add(channel);
|
|
9655
|
+
}
|
|
9656
|
+
if (importFileVersion(projectView(ctx, root).read(CLAUDE_IMPORT_FILE)) !== void 0) {
|
|
9657
|
+
channels.add("claude-file");
|
|
9658
|
+
}
|
|
9659
|
+
for (const file of [AGENTS_MD, CLAUDE_MD]) {
|
|
9660
|
+
const path = join14(root, file);
|
|
9661
|
+
if (ctx.fs.isRegularFile(path) && findBlock(ctx.fs.readFile(path), file) !== void 0) {
|
|
9662
|
+
channels.add("block");
|
|
9663
|
+
}
|
|
9664
|
+
}
|
|
9665
|
+
return channels;
|
|
9666
|
+
}
|
|
9667
|
+
function installedRuleStamps(ctx, root) {
|
|
9668
|
+
const stamps = [];
|
|
9669
|
+
const regular = (file) => ctx.fs.isRegularFile(join14(root, file));
|
|
9670
|
+
for (const file of [CLAUDE_RULE_FILE, AGENTS_RULE_FILE].filter(regular)) {
|
|
9671
|
+
stamps.push({ path: file, version: ruleVersion(ctx.fs.readFile(join14(root, file))) });
|
|
9228
9672
|
}
|
|
9229
|
-
const
|
|
9230
|
-
|
|
9231
|
-
const
|
|
9232
|
-
|
|
9233
|
-
|
|
9234
|
-
|
|
9673
|
+
const imported = importFileVersion(projectView(ctx, root).read(CLAUDE_IMPORT_FILE));
|
|
9674
|
+
if (imported !== void 0) stamps.push({ path: CLAUDE_IMPORT_FILE, version: imported });
|
|
9675
|
+
for (const file of [AGENTS_MD, CLAUDE_MD].filter(regular)) {
|
|
9676
|
+
const content = ctx.fs.readFile(join14(root, file));
|
|
9677
|
+
const span = findBlock(content, file);
|
|
9678
|
+
if (span !== void 0) {
|
|
9679
|
+
stamps.push({ path: file, version: ruleVersion(content.slice(span.from, span.to)) });
|
|
9680
|
+
}
|
|
9235
9681
|
}
|
|
9236
|
-
return
|
|
9682
|
+
return stamps;
|
|
9237
9683
|
}
|
|
9238
|
-
function
|
|
9239
|
-
|
|
9240
|
-
|
|
9241
|
-
|
|
9242
|
-
|
|
9684
|
+
function applyRulePlan(ctx, root, plan) {
|
|
9685
|
+
for (const { path, content } of plan.writes) {
|
|
9686
|
+
const full = join14(root, path);
|
|
9687
|
+
ctx.fs.mkdir(dirname2(full));
|
|
9688
|
+
if (ctx.fs.lexists(full) && !ctx.fs.isRegularFile(full)) ctx.fs.remove(full);
|
|
9689
|
+
writeFileAtomic(ctx, full, content);
|
|
9690
|
+
}
|
|
9691
|
+
for (const path of plan.removals) {
|
|
9692
|
+
const full = join14(root, path);
|
|
9693
|
+
ctx.fs.remove(full);
|
|
9694
|
+
if (ctx.fs.isDirectory(dirname2(full)) && ctx.fs.readDir(dirname2(full)).length === 0) {
|
|
9695
|
+
ctx.fs.removeDir(dirname2(full));
|
|
9696
|
+
}
|
|
9697
|
+
}
|
|
9243
9698
|
}
|
|
9244
9699
|
|
|
9245
|
-
// src/
|
|
9246
|
-
|
|
9247
|
-
|
|
9248
|
-
|
|
9249
|
-
|
|
9250
|
-
|
|
9251
|
-
|
|
9252
|
-
|
|
9253
|
-
|
|
9254
|
-
|
|
9255
|
-
const found = [];
|
|
9256
|
-
for (const entry of ctx.fs.readDir(dir).sort((a, b) => a.name < b.name ? -1 : 1)) {
|
|
9257
|
-
if (entry.name.startsWith(".")) continue;
|
|
9258
|
-
const rel = `${prefix}${entry.name}`;
|
|
9259
|
-
if (entry.isDirectory) {
|
|
9260
|
-
found.push(...listFiles(ctx, join14(dir, entry.name), other, `${rel}/`));
|
|
9261
|
-
} else {
|
|
9262
|
-
if (!entry.isFile) other(rel);
|
|
9263
|
-
found.push(rel);
|
|
9700
|
+
// src/cli/install.ts
|
|
9701
|
+
var AGENT_HELP = `Agents: ${HARNESS_NAMES.join(", ")}. --agent takes a comma-separated list and can be repeated.`;
|
|
9702
|
+
function parseAgents(values) {
|
|
9703
|
+
const names = (values ?? []).flatMap((value) => value.split(",")).map((name) => name.trim());
|
|
9704
|
+
const harnesses = /* @__PURE__ */ new Set();
|
|
9705
|
+
for (const name of names) {
|
|
9706
|
+
if (name === "") continue;
|
|
9707
|
+
const harness = findHarness(name);
|
|
9708
|
+
if (harness === void 0) {
|
|
9709
|
+
throw new UsageError(`unknown agent '${name}'; expected one of: ${HARNESS_NAMES.join(", ")}`);
|
|
9264
9710
|
}
|
|
9711
|
+
harnesses.add(harness);
|
|
9265
9712
|
}
|
|
9266
|
-
return
|
|
9713
|
+
return HARNESSES.filter((harness) => harnesses.has(harness));
|
|
9267
9714
|
}
|
|
9268
|
-
|
|
9269
|
-
|
|
9270
|
-
|
|
9271
|
-
|
|
9272
|
-
|
|
9273
|
-
|
|
9274
|
-
|
|
9715
|
+
async function selectHarnesses(io, detected, requested, yes) {
|
|
9716
|
+
const selected = /* @__PURE__ */ new Set([...detected.map((d) => d.harness), ...requested]);
|
|
9717
|
+
const ordered = () => HARNESSES.filter((harness) => selected.has(harness));
|
|
9718
|
+
if (io.prompt === void 0 || yes) {
|
|
9719
|
+
if (selected.size === 0) {
|
|
9720
|
+
throw new UsageError(
|
|
9721
|
+
`no agent detected in this project; name them with --agent (${HARNESS_NAMES.join(", ")})`
|
|
9722
|
+
);
|
|
9723
|
+
}
|
|
9724
|
+
return ordered();
|
|
9725
|
+
}
|
|
9726
|
+
const found = new Map(detected.map((d) => [d.harness, d.marker]));
|
|
9727
|
+
const width = Math.max(...HARNESS_NAMES.map((name) => name.length));
|
|
9728
|
+
for (; ; ) {
|
|
9729
|
+
const lines = HARNESSES.map((harness, i) => {
|
|
9730
|
+
const box = selected.has(harness) ? "[x]" : "[ ]";
|
|
9731
|
+
const marker = found.get(harness);
|
|
9732
|
+
const note = marker === void 0 ? "" : ` (found ${marker})`;
|
|
9733
|
+
return ` ${i + 1}. ${box} ${harness.name.padEnd(width)} ${harness.title}${note}
|
|
9734
|
+
`;
|
|
9735
|
+
});
|
|
9736
|
+
io.stdout(`Install DLD for these agents:
|
|
9737
|
+
${lines.join("")}`);
|
|
9738
|
+
const answer = (await io.prompt("Numbers to toggle, or Enter to continue: ")).trim();
|
|
9739
|
+
if (answer === "") {
|
|
9740
|
+
if (selected.size > 0) return ordered();
|
|
9741
|
+
io.stdout("Select at least one agent.\n");
|
|
9742
|
+
continue;
|
|
9743
|
+
}
|
|
9744
|
+
const picks = answer.split(/[\s,]+/).map((pick) => HARNESSES[Number(pick) - 1]);
|
|
9745
|
+
if (picks.some((harness) => harness === void 0)) {
|
|
9746
|
+
io.stdout(`Enter numbers from 1 to ${HARNESSES.length}.
|
|
9747
|
+
`);
|
|
9748
|
+
continue;
|
|
9749
|
+
}
|
|
9750
|
+
for (const harness of picks) {
|
|
9751
|
+
if (harness === void 0) continue;
|
|
9752
|
+
if (selected.has(harness)) selected.delete(harness);
|
|
9753
|
+
else selected.add(harness);
|
|
9754
|
+
}
|
|
9275
9755
|
}
|
|
9276
9756
|
}
|
|
9277
|
-
function
|
|
9278
|
-
|
|
9279
|
-
return
|
|
9280
|
-
"---",
|
|
9281
|
-
...fields,
|
|
9282
|
-
...adapter.extraFrontmatter,
|
|
9283
|
-
...usesDld ? adapter.dldFrontmatter : [],
|
|
9284
|
-
"metadata:",
|
|
9285
|
-
` dld-kit-version: "${version2}"`,
|
|
9286
|
-
"---",
|
|
9287
|
-
`<!-- Generated by dld-kit from ${template.source}. Do not edit: updating the skills overwrites this file. -->`,
|
|
9288
|
-
body
|
|
9289
|
-
].join("\n");
|
|
9757
|
+
function cliPath(io) {
|
|
9758
|
+
if (io.cliPath === void 0) throw new DldError("cannot locate the running dld file");
|
|
9759
|
+
return io.cliPath;
|
|
9290
9760
|
}
|
|
9291
|
-
function
|
|
9292
|
-
const
|
|
9293
|
-
|
|
9294
|
-
|
|
9295
|
-
|
|
9296
|
-
|
|
9297
|
-
throw new DldError(`${sourceDir}/${skill}/${rel}: templates must be regular files`);
|
|
9298
|
-
}).filter((file) => file !== MANIFEST);
|
|
9299
|
-
support.set(skill, files);
|
|
9300
|
-
for (const file of files) provided.add(`${skill}/${file}`);
|
|
9761
|
+
function printReport(io, report2) {
|
|
9762
|
+
for (const { dir, written, removed, unchanged, removedPaths } of report2.skills) {
|
|
9763
|
+
io.stdout(`${dir}: ${written} written, ${removed} removed, ${unchanged} unchanged
|
|
9764
|
+
`);
|
|
9765
|
+
for (const path of removedPaths) io.stdout(`Removed ${path}
|
|
9766
|
+
`);
|
|
9301
9767
|
}
|
|
9302
|
-
for (const path of
|
|
9303
|
-
|
|
9304
|
-
|
|
9768
|
+
for (const path of report2.ruleWritten) {
|
|
9769
|
+
io.stdout(
|
|
9770
|
+
path === CLAUDE_IMPORT_FILE ? `Wrote ${path} (imports AGENTS.md for Claude Code)
|
|
9771
|
+
` : `Wrote the DLD rule to ${path}
|
|
9772
|
+
`
|
|
9773
|
+
);
|
|
9305
9774
|
}
|
|
9306
|
-
const
|
|
9307
|
-
|
|
9308
|
-
|
|
9309
|
-
|
|
9310
|
-
|
|
9311
|
-
|
|
9312
|
-
|
|
9313
|
-
|
|
9314
|
-
|
|
9315
|
-
|
|
9316
|
-
|
|
9317
|
-
|
|
9318
|
-
|
|
9319
|
-
|
|
9320
|
-
|
|
9321
|
-
|
|
9322
|
-
|
|
9323
|
-
|
|
9324
|
-
|
|
9325
|
-
|
|
9326
|
-
|
|
9327
|
-
|
|
9328
|
-
|
|
9329
|
-
|
|
9775
|
+
for (const path of report2.ruleRemoved) io.stdout(`Removed ${path} (the rule is in the block)
|
|
9776
|
+
`);
|
|
9777
|
+
for (const warning of report2.warnings) io.stderr(`Warning: ${warning}
|
|
9778
|
+
`);
|
|
9779
|
+
}
|
|
9780
|
+
function ensureIndexMergeAttribute(io, ctx, project) {
|
|
9781
|
+
const { root } = project.paths;
|
|
9782
|
+
const index = relative8(root, indexPath(project.paths)).split(sep4).join("/");
|
|
9783
|
+
const plan = planIndexMergeAttribute(ctx, root, index);
|
|
9784
|
+
if (applyIndexMergeAttribute(ctx, root, plan)) {
|
|
9785
|
+
const verb = plan.creates === true ? "Created" : "Updated";
|
|
9786
|
+
io.stdout(`${verb} ${GITATTRIBUTES}: ${index} merges keep the rows from both sides
|
|
9787
|
+
`);
|
|
9788
|
+
}
|
|
9789
|
+
for (const warning of plan.warnings) io.stderr(`Warning: ${warning}
|
|
9790
|
+
`);
|
|
9791
|
+
}
|
|
9792
|
+
|
|
9793
|
+
// src/cli/commands/create-empty-index.ts
|
|
9794
|
+
var createEmptyIndexCommand = {
|
|
9795
|
+
name: "create-empty-index",
|
|
9796
|
+
summary: "Write an INDEX.md with no decisions",
|
|
9797
|
+
internal: true,
|
|
9798
|
+
usage: "Usage: dld create-empty-index\n\nWrite INDEX.md in the decisions directory with only the table header, and add its\nunion-merge line to .gitattributes.\n",
|
|
9799
|
+
run(args, io, ctx) {
|
|
9800
|
+
parseCommandArgs({ args: [...args], options: {} });
|
|
9801
|
+
const project = loadProject(ctx);
|
|
9802
|
+
const { config, paths } = project;
|
|
9803
|
+
writeIndex(ctx, paths, renderIndex([], config.mode));
|
|
9804
|
+
io.stdout(`Created ${indexPath(paths)}
|
|
9805
|
+
`);
|
|
9806
|
+
ensureIndexMergeAttribute(io, ctx, project);
|
|
9807
|
+
return EXIT_OK;
|
|
9808
|
+
}
|
|
9809
|
+
};
|
|
9810
|
+
|
|
9811
|
+
// src/cli/commands/detect-snapshot-changes.ts
|
|
9812
|
+
var detectSnapshotChangesCommand = {
|
|
9813
|
+
name: "detect-snapshot-changes",
|
|
9814
|
+
summary: "Report what changed since the last snapshot",
|
|
9815
|
+
internal: true,
|
|
9816
|
+
usage: "Usage: dld detect-snapshot-changes\n\nPrint mode (full or incremental), new_decisions, modified_decisions and commit_range.\n",
|
|
9817
|
+
run(args, io, ctx) {
|
|
9818
|
+
parseCommandArgs({ args: [...args], options: {} });
|
|
9819
|
+
io.stdout(formatSnapshotChanges(detectSnapshotChanges(ctx, loadProject(ctx))));
|
|
9820
|
+
return EXIT_OK;
|
|
9821
|
+
}
|
|
9822
|
+
};
|
|
9823
|
+
|
|
9824
|
+
// src/core/annotations.ts
|
|
9825
|
+
import { join as join15, relative as relative9, sep as sep5 } from "node:path";
|
|
9826
|
+
var EXCLUDED_DIRS = /* @__PURE__ */ new Set([
|
|
9827
|
+
".git",
|
|
9828
|
+
// Agent configuration directories, which hold installed skills.
|
|
9829
|
+
".claude",
|
|
9830
|
+
".agents",
|
|
9831
|
+
".agent",
|
|
9832
|
+
".codex",
|
|
9833
|
+
".cursor",
|
|
9834
|
+
".opencode",
|
|
9835
|
+
".pi",
|
|
9836
|
+
".tessl",
|
|
9837
|
+
"node_modules",
|
|
9838
|
+
"vendor",
|
|
9839
|
+
".venv",
|
|
9840
|
+
"__pycache__",
|
|
9841
|
+
"target",
|
|
9842
|
+
"dist",
|
|
9843
|
+
"build",
|
|
9844
|
+
"out",
|
|
9845
|
+
".next",
|
|
9846
|
+
".gradle",
|
|
9847
|
+
"coverage"
|
|
9848
|
+
]);
|
|
9849
|
+
var EXCLUDED_FILE = /(\.lock|\.min\.js|\.min\.css|\.map)$/;
|
|
9850
|
+
var BINARY_SNIFF_BYTES = 8192;
|
|
9851
|
+
var MAX_SCAN_BYTES = 16 * 1024 * 1024;
|
|
9852
|
+
function scanOptionsFor({ config, paths }) {
|
|
9853
|
+
return {
|
|
9854
|
+
root: paths.root,
|
|
9855
|
+
decisionsDir: paths.decisionsDir,
|
|
9856
|
+
prefix: config.annotationPrefix,
|
|
9857
|
+
exclude: config.annotationExclude
|
|
9858
|
+
};
|
|
9859
|
+
}
|
|
9860
|
+
function listScannableFiles(ctx, options) {
|
|
9861
|
+
const output = ctx.git([
|
|
9862
|
+
"-C",
|
|
9863
|
+
options.root,
|
|
9864
|
+
"ls-files",
|
|
9865
|
+
"-z",
|
|
9866
|
+
"--cached",
|
|
9867
|
+
"--others",
|
|
9868
|
+
"--exclude-standard",
|
|
9869
|
+
// @decision(DL-023)
|
|
9870
|
+
"--",
|
|
9871
|
+
...(options.exclude ?? []).map((pattern) => `:(exclude,glob)${pattern}`)
|
|
9872
|
+
]);
|
|
9873
|
+
const decisionsRel = relative9(options.root, options.decisionsDir).split(sep5).join("/");
|
|
9874
|
+
const files = /* @__PURE__ */ new Set();
|
|
9875
|
+
for (const file of output.split("\0")) {
|
|
9876
|
+
if (file === "" || EXCLUDED_FILE.test(file)) continue;
|
|
9877
|
+
if (file === decisionsRel || file.startsWith(`${decisionsRel}/`)) continue;
|
|
9878
|
+
const dirs = file.split("/").slice(0, -1);
|
|
9879
|
+
if (dirs.some((dir) => EXCLUDED_DIRS.has(dir))) continue;
|
|
9880
|
+
files.add(file);
|
|
9330
9881
|
}
|
|
9331
|
-
|
|
9332
|
-
return new Map([...output].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0));
|
|
9882
|
+
return [...files].sort();
|
|
9333
9883
|
}
|
|
9334
|
-
function
|
|
9335
|
-
|
|
9336
|
-
return generated.has(skill) || skill.startsWith("dld-") || files.has(path);
|
|
9884
|
+
function escapeRegExp(text) {
|
|
9885
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
9337
9886
|
}
|
|
9338
|
-
function
|
|
9339
|
-
const
|
|
9340
|
-
|
|
9341
|
-
|
|
9342
|
-
|
|
9343
|
-
|
|
9344
|
-
|
|
9345
|
-
|
|
9346
|
-
if (!existing.has(path) || !ctx.fs.isRegularFile(full)) diff.missing.push(path);
|
|
9347
|
-
else if (ctx.fs.readFile(full) !== file.content || normalMode(ctx.fs.fileMode(full)) !== file.mode) {
|
|
9348
|
-
diff.changed.push(path);
|
|
9887
|
+
function readScannable(ctx, root, file, onSkip) {
|
|
9888
|
+
const path = join15(root, file);
|
|
9889
|
+
let text;
|
|
9890
|
+
try {
|
|
9891
|
+
if (!ctx.fs.isRegularFile(path)) return void 0;
|
|
9892
|
+
if (ctx.fs.fileSize(path) > MAX_SCAN_BYTES) {
|
|
9893
|
+
onSkip({ file, reason: `larger than ${MAX_SCAN_BYTES / 1024 / 1024} MB` });
|
|
9894
|
+
return void 0;
|
|
9349
9895
|
}
|
|
9896
|
+
text = ctx.fs.readFile(path);
|
|
9897
|
+
} catch (error) {
|
|
9898
|
+
if (!(error instanceof FsError)) throw error;
|
|
9899
|
+
onSkip({ file, reason: error.message });
|
|
9900
|
+
return void 0;
|
|
9350
9901
|
}
|
|
9351
|
-
|
|
9352
|
-
return diff;
|
|
9902
|
+
return text.slice(0, BINARY_SNIFF_BYTES).includes("\0") ? void 0 : text;
|
|
9353
9903
|
}
|
|
9354
|
-
function
|
|
9355
|
-
|
|
9356
|
-
|
|
9357
|
-
|
|
9358
|
-
|
|
9359
|
-
|
|
9360
|
-
|
|
9361
|
-
|
|
9362
|
-
|
|
9363
|
-
|
|
9904
|
+
function scanAnnotations(ctx, options, onSkip = () => {
|
|
9905
|
+
}, onNearMiss = () => {
|
|
9906
|
+
}) {
|
|
9907
|
+
const prefix = escapeRegExp(options.prefix);
|
|
9908
|
+
const pattern = new RegExp(`${prefix}\\((DL-\\d+)\\)`, "g");
|
|
9909
|
+
const exact = new RegExp(`^${prefix}\\(DL-\\d+\\)$`);
|
|
9910
|
+
const loose = new RegExp(`${prefix}\\([^()\\n]*\\)`, "gi");
|
|
9911
|
+
const found = [];
|
|
9912
|
+
for (const file of listScannableFiles(ctx, options)) {
|
|
9913
|
+
const text = readScannable(ctx, options.root, file, onSkip);
|
|
9914
|
+
if (text === void 0) continue;
|
|
9915
|
+
text.split("\n").forEach((content, index) => {
|
|
9916
|
+
const line = index + 1;
|
|
9917
|
+
for (const match of content.matchAll(pattern)) {
|
|
9918
|
+
if (match[1] !== void 0) found.push({ file, line, id: match[1] });
|
|
9919
|
+
}
|
|
9920
|
+
for (const [candidate] of content.matchAll(loose)) {
|
|
9921
|
+
if (!exact.test(candidate) && /dl-\d/i.test(candidate)) {
|
|
9922
|
+
onNearMiss({ file, line, text: candidate });
|
|
9923
|
+
}
|
|
9924
|
+
}
|
|
9925
|
+
});
|
|
9364
9926
|
}
|
|
9365
|
-
|
|
9366
|
-
return diff;
|
|
9927
|
+
return found;
|
|
9367
9928
|
}
|
|
9368
|
-
function
|
|
9369
|
-
|
|
9370
|
-
for (const path of paths) {
|
|
9371
|
-
for (let parent = dirname2(path); parent !== "."; parent = dirname2(parent)) parents.add(parent);
|
|
9372
|
-
}
|
|
9373
|
-
const deepestFirst = [...parents].sort((a, b) => b.split("/").length - a.split("/").length);
|
|
9374
|
-
for (const parent of deepestFirst) {
|
|
9375
|
-
const full = join14(dir, parent);
|
|
9376
|
-
if (ctx.fs.isDirectory(full) && ctx.fs.readDir(full).length === 0) ctx.fs.removeDir(full);
|
|
9377
|
-
}
|
|
9929
|
+
function formatNearMiss({ file, line, text }) {
|
|
9930
|
+
return `${file}:${line}: not an annotation: ${text}`;
|
|
9378
9931
|
}
|
|
9379
|
-
|
|
9380
|
-
|
|
9381
|
-
|
|
9382
|
-
|
|
9383
|
-
|
|
9384
|
-
var dld_workflow_default = "# DLD (Decision-Linked Development)\n\nThis project uses Decision-Linked Development. Decision records (DL-*.md) live in the `records/` subdirectory of the decisions directory set in `dld.config.yaml` (`decisions/` by default). High-level docs (INDEX.md, OVERVIEW.md, SNAPSHOT.md) live in the decisions directory.\n\n## Rules\n\n- When you encounter `@decision(DL-XXX)` annotations in code, read the referenced decision with the dld-lookup skill BEFORE modifying the annotated code.\n- ALWAYS look up and verify related decisions before modifying annotated code. Do not skip this step.\n- NEVER modify code in a way that contradicts an existing decision without first confirming with the user. If the change requires breaking a previous decision, a new decision must be recorded (with the dld-decide skill) that explicitly supersedes the old one. If it only partially modifies a previous decision, record it as an amendment instead.\n\n## Skills\n\n- dld-decide: record a new decision\n- dld-plan: break down a feature into multiple grouped decisions\n- dld-implement: implement proposed decisions\n- dld-lookup: query decisions by ID, tag, or code path\n- dld-adjust: adjust or update existing decisions\n- dld-audit: scan for drift between decisions and code\n- dld-snapshot: regenerate SNAPSHOT.md and OVERVIEW.md from the decision log\n- dld-status: a quick overview of the decision log state\n- dld-retrofit: generate decisions from an existing codebase\n- dld-reindex: resolve decision-ID collisions with the base branch and open PRs\n";
|
|
9385
|
-
|
|
9386
|
-
// src/generate/rule.ts
|
|
9387
|
-
var RULE_TEXT = dld_workflow_default;
|
|
9388
|
-
var RULE_SOURCE = "templates/rules/dld-workflow.md";
|
|
9389
|
-
var CLAUDE_RULE_FILE = ".claude/rules/dld-workflow.md";
|
|
9390
|
-
var AGENTS_RULE_FILE = ".agents/rules/dld-workflow.md";
|
|
9391
|
-
var CLAUDE_IMPORT_FILE = ".claude/CLAUDE.md";
|
|
9392
|
-
var BLOCK_START = "<!-- dld-kit:start -->";
|
|
9393
|
-
var BLOCK_END = "<!-- dld-kit:end -->";
|
|
9394
|
-
var AGENTS_MD = "AGENTS.md";
|
|
9395
|
-
var CLAUDE_MD = "CLAUDE.md";
|
|
9396
|
-
var CLAUDE_FILES = [CLAUDE_MD, CLAUDE_IMPORT_FILE, "CLAUDE.local.md"];
|
|
9397
|
-
var SHARED_CLAUDE_FILES = [CLAUDE_MD, CLAUDE_IMPORT_FILE];
|
|
9398
|
-
var RULE_FILES = {
|
|
9399
|
-
"claude-file": CLAUDE_RULE_FILE,
|
|
9400
|
-
"agents-file": AGENTS_RULE_FILE
|
|
9401
|
-
};
|
|
9402
|
-
function notice(version2, what) {
|
|
9403
|
-
return `<!-- Generated by dld-kit ${version2} from ${RULE_SOURCE}. Do not edit: updating the rule overwrites this ${what}. -->`;
|
|
9932
|
+
function missingAnnotations(ctx, options, ids, onSkip, onNearMiss) {
|
|
9933
|
+
const annotated = new Set(
|
|
9934
|
+
scanAnnotations(ctx, options, onSkip, onNearMiss).map((annotation) => annotation.id)
|
|
9935
|
+
);
|
|
9936
|
+
return ids.filter((id) => !annotated.has(id));
|
|
9404
9937
|
}
|
|
9405
|
-
|
|
9406
|
-
|
|
9407
|
-
return STAMP.exec(content)?.[1];
|
|
9938
|
+
function formatSkipped({ file, reason }) {
|
|
9939
|
+
return `Warning: skipped ${file} (${reason}); annotations in it are not counted`;
|
|
9408
9940
|
}
|
|
9409
|
-
function
|
|
9410
|
-
|
|
9411
|
-
return `${frontmatter}${notice(version2, "file")}
|
|
9412
|
-
|
|
9413
|
-
${text}`;
|
|
9941
|
+
function formatAnnotation({ file, line, id }) {
|
|
9942
|
+
return `${file}:${line}:${id}`;
|
|
9414
9943
|
}
|
|
9415
|
-
function renderImportFile(version2) {
|
|
9416
|
-
return `<!-- Generated by dld-kit ${version2} for Claude Code: it loads AGENTS.md, which holds the DLD rule, through the import below. Do not edit: updating the rule overwrites this file. -->
|
|
9417
9944
|
|
|
9418
|
-
|
|
9419
|
-
|
|
9420
|
-
|
|
9421
|
-
|
|
9422
|
-
|
|
9423
|
-
|
|
9424
|
-
|
|
9425
|
-
|
|
9426
|
-
|
|
9427
|
-
|
|
9428
|
-
|
|
9429
|
-
|
|
9430
|
-
|
|
9431
|
-
|
|
9432
|
-
|
|
9433
|
-
|
|
9434
|
-
|
|
9435
|
-
|
|
9436
|
-
|
|
9437
|
-
|
|
9945
|
+
// src/cli/commands/find-annotations.ts
|
|
9946
|
+
var findAnnotationsCommand = {
|
|
9947
|
+
name: "find-annotations",
|
|
9948
|
+
summary: "List every annotation in the codebase",
|
|
9949
|
+
internal: true,
|
|
9950
|
+
usage: "Usage: dld find-annotations\n\nPrint <file>:<line>:<DL-NNN> for every annotation, one per line.\n",
|
|
9951
|
+
run(args, io, ctx) {
|
|
9952
|
+
parseCommandArgs({ args: [...args], options: {} });
|
|
9953
|
+
const annotations = scanAnnotations(
|
|
9954
|
+
ctx,
|
|
9955
|
+
scanOptionsFor(loadProject(ctx)),
|
|
9956
|
+
(skipped) => io.stderr(`${formatSkipped(skipped)}
|
|
9957
|
+
`),
|
|
9958
|
+
// @decision(DL-074)
|
|
9959
|
+
(nearMiss) => ctx.warn(formatNearMiss(nearMiss))
|
|
9960
|
+
);
|
|
9961
|
+
for (const annotation of annotations) {
|
|
9962
|
+
io.stdout(`${formatAnnotation(annotation)}
|
|
9963
|
+
`);
|
|
9964
|
+
}
|
|
9965
|
+
return EXIT_OK;
|
|
9438
9966
|
}
|
|
9439
|
-
|
|
9440
|
-
|
|
9441
|
-
|
|
9442
|
-
|
|
9443
|
-
|
|
9967
|
+
};
|
|
9968
|
+
|
|
9969
|
+
// src/cli/commands/find-collisions.ts
|
|
9970
|
+
var findCollisionsCommand = {
|
|
9971
|
+
name: "find-collisions",
|
|
9972
|
+
summary: "List locally added decisions whose IDs are taken",
|
|
9973
|
+
internal: true,
|
|
9974
|
+
usage: `Usage: dld find-collisions [--base <ref>]
|
|
9975
|
+
|
|
9976
|
+
Print <path>\\t<DL-NNN> for each decision added on this branch whose ID is taken on the base
|
|
9977
|
+
branch or in an open pull request.
|
|
9978
|
+
|
|
9979
|
+
Options:
|
|
9980
|
+
--base <ref> Base ref (${DEFAULT_BASE_HELP})
|
|
9981
|
+
`,
|
|
9982
|
+
run(args, io, ctx) {
|
|
9983
|
+
const { values } = parseCommandArgs({ args: [...args], options: { base: { type: "string" } } });
|
|
9984
|
+
const project = loadProject(ctx);
|
|
9985
|
+
const base = baseOption(values.base, { ctx, project });
|
|
9986
|
+
const { collisions, skipped } = findCollisions(ctx, project, base);
|
|
9987
|
+
for (const { path, id } of collisions) io.stdout(`${path} ${id}
|
|
9988
|
+
`);
|
|
9989
|
+
if (skipped !== void 0) io.stderr(skippedNotice(skipped));
|
|
9990
|
+
return EXIT_OK;
|
|
9444
9991
|
}
|
|
9445
|
-
|
|
9446
|
-
|
|
9447
|
-
|
|
9448
|
-
|
|
9449
|
-
|
|
9450
|
-
|
|
9451
|
-
|
|
9452
|
-
}
|
|
9453
|
-
|
|
9454
|
-
const
|
|
9455
|
-
const
|
|
9456
|
-
|
|
9457
|
-
|
|
9458
|
-
|
|
9459
|
-
|
|
9460
|
-
|
|
9992
|
+
};
|
|
9993
|
+
|
|
9994
|
+
// src/core/audit.ts
|
|
9995
|
+
import { basename as basename7, relative as relative10, sep as sep6 } from "node:path";
|
|
9996
|
+
var MENTION = /DL-\d+/g;
|
|
9997
|
+
var toPosix2 = (path) => path.split(sep6).join("/");
|
|
9998
|
+
var idNumber2 = (id) => Number.parseInt(id.slice(3), 10);
|
|
9999
|
+
function recordsChangedSinceAudit(ctx, { paths }) {
|
|
10000
|
+
const git = gitAt(ctx, paths.root);
|
|
10001
|
+
const stored = stateString(readStateSection(ctx, paths, "audit"), "commit_hash");
|
|
10002
|
+
const commit2 = resolveStateCommit(git, stored);
|
|
10003
|
+
if (commit2 === void 0) return void 0;
|
|
10004
|
+
const records = recordsPathspec(paths);
|
|
10005
|
+
return /* @__PURE__ */ new Set([
|
|
10006
|
+
...nulSeparated(git("diff", "-z", "--name-only", commit2, "--", records)),
|
|
10007
|
+
...nulSeparated(git("ls-files", "-z", "--others", "--exclude-standard", "--", records))
|
|
10008
|
+
]);
|
|
9461
10009
|
}
|
|
9462
|
-
function
|
|
9463
|
-
const
|
|
9464
|
-
const
|
|
9465
|
-
const
|
|
9466
|
-
const
|
|
9467
|
-
|
|
9468
|
-
return ctx.fs.exists(full) ? ctx.fs.realPath(full) : full;
|
|
9469
|
-
};
|
|
9470
|
-
let blockFiles = [AGENTS_MD, CLAUDE_MD].filter(
|
|
9471
|
-
(file) => regular(file) && findBlock(read(file), file) !== void 0
|
|
10010
|
+
function findMissingAmends(ctx, project, { all }) {
|
|
10011
|
+
const { paths } = project;
|
|
10012
|
+
const changed = all ? void 0 : recordsChangedSinceAudit(ctx, project);
|
|
10013
|
+
const found = [];
|
|
10014
|
+
const files = listRecordFiles(ctx, paths.recordsDir).sort(
|
|
10015
|
+
(a, b) => recordNumber(a) - recordNumber(b)
|
|
9472
10016
|
);
|
|
9473
|
-
|
|
9474
|
-
const
|
|
9475
|
-
|
|
9476
|
-
|
|
9477
|
-
|
|
9478
|
-
|
|
9479
|
-
|
|
9480
|
-
|
|
9481
|
-
const
|
|
9482
|
-
const
|
|
9483
|
-
pending.set(file, updated);
|
|
9484
|
-
if (updated !== content) plan.writes.push({ path: file, content: updated });
|
|
9485
|
-
}
|
|
9486
|
-
const linkedDir = symlinkedPart(ctx, root, dirname3(CLAUDE_IMPORT_FILE));
|
|
9487
|
-
const ownsImport = linkedDir === void 0 && importFileVersion(read(CLAUDE_IMPORT_FILE)) !== void 0;
|
|
9488
|
-
const wantsImport = channels.has("claude-file") && blockFiles.includes(AGENTS_MD) && !SHARED_CLAUDE_FILES.some((file) => ctx.fs.lexists(join15(root, file)));
|
|
9489
|
-
if (ownsImport || wantsImport && linkedDir === void 0) {
|
|
9490
|
-
const content = renderImportFile(version2);
|
|
9491
|
-
pending.set(CLAUDE_IMPORT_FILE, content);
|
|
9492
|
-
if (read(CLAUDE_IMPORT_FILE) !== content) {
|
|
9493
|
-
plan.writes.push({ path: CLAUDE_IMPORT_FILE, content });
|
|
9494
|
-
}
|
|
9495
|
-
} else if (wantsImport) {
|
|
9496
|
-
plan.warnings.push(
|
|
9497
|
-
`${linkedDir} is a symlink, so dld-kit does not write ${CLAUDE_IMPORT_FILE}, which would import AGENTS.md for Claude Code. Claude Code reads the rule block in AGENTS.md by itself only while the project has no CLAUDE.md, ${CLAUDE_IMPORT_FILE} or CLAUDE.local.md.`
|
|
9498
|
-
);
|
|
9499
|
-
}
|
|
9500
|
-
const withBlock = new Set(blockFiles.map(resolved));
|
|
9501
|
-
const current = projectView(ctx, root);
|
|
9502
|
-
const view = {
|
|
9503
|
-
exists: (file) => pending.has(file) || current.exists(file),
|
|
9504
|
-
read: (file) => pending.get(file) ?? current.read(file)
|
|
9505
|
-
};
|
|
9506
|
-
const reads = (harness) => instructionFiles(harness, view)[0];
|
|
9507
|
-
const readsBlock = (harness) => instructionFiles(harness, view).some((file) => withBlock.has(resolved(file)));
|
|
9508
|
-
const ownerOf = (channel) => HARNESSES.find((h) => h.rule === channel);
|
|
9509
|
-
const loadsBlock = (channel) => {
|
|
9510
|
-
const owner = ownerOf(channel);
|
|
9511
|
-
return owner !== void 0 && readsBlock(owner);
|
|
9512
|
-
};
|
|
9513
|
-
if (blockFiles.length > 0) {
|
|
9514
|
-
plan.warnings.push(
|
|
9515
|
-
...blindWarnings(
|
|
9516
|
-
blockFiles,
|
|
9517
|
-
harnesses.filter((h) => h.rule === "block" && !readsBlock(h)),
|
|
9518
|
-
reads
|
|
9519
|
-
)
|
|
9520
|
-
);
|
|
9521
|
-
}
|
|
9522
|
-
for (const channel of ["claude-file", "agents-file"]) {
|
|
9523
|
-
const path = RULE_FILES[channel];
|
|
9524
|
-
if (loadsBlock(channel)) {
|
|
9525
|
-
if (ctx.fs.lexists(join15(root, path))) plan.removals.push(path);
|
|
9526
|
-
} else if (channels.has(channel)) {
|
|
9527
|
-
refuseSymlinkedDir(ctx, root, dirname3(path));
|
|
9528
|
-
const content = renderRuleFile(channel, text, version2);
|
|
9529
|
-
if (read(path) !== content) plan.writes.push({ path, content });
|
|
9530
|
-
}
|
|
10017
|
+
for (const file of files) {
|
|
10018
|
+
const rel = toPosix2(relative10(paths.root, file));
|
|
10019
|
+
if (changed !== void 0 && !changed.has(rel)) continue;
|
|
10020
|
+
const text = ctx.fs.readFile(file);
|
|
10021
|
+
const record = parseRecord(text, rel);
|
|
10022
|
+
const source = basename7(file, ".md");
|
|
10023
|
+
const declared = /* @__PURE__ */ new Set([...record.supersedes, ...record.amends]);
|
|
10024
|
+
const mentioned = new Set(recordBody(text).match(MENTION) ?? []);
|
|
10025
|
+
const refs = [...mentioned].filter((id) => id !== source && !declared.has(id)).sort((a, b) => idNumber2(a) - idNumber2(b));
|
|
10026
|
+
for (const referenced of refs) found.push({ source, referenced });
|
|
9531
10027
|
}
|
|
9532
|
-
|
|
9533
|
-
plan.warnings.push(...legacyBlockWarnings(read(CLAUDE_MD), ruleInstalled));
|
|
9534
|
-
return plan;
|
|
10028
|
+
return found;
|
|
9535
10029
|
}
|
|
9536
|
-
function
|
|
9537
|
-
const
|
|
9538
|
-
|
|
9539
|
-
|
|
9540
|
-
|
|
9541
|
-
};
|
|
10030
|
+
function updateAuditState(ctx, { paths }) {
|
|
10031
|
+
const timestamp = formatTimestamp(ctx.now());
|
|
10032
|
+
const commit2 = shortHead(ctx, paths.root);
|
|
10033
|
+
writeStateSection(ctx, paths, "audit", { last_run: timestamp, commit_hash: commit2 });
|
|
10034
|
+
return { timestamp, commit: commit2 };
|
|
9542
10035
|
}
|
|
9543
|
-
|
|
9544
|
-
|
|
9545
|
-
|
|
9546
|
-
|
|
9547
|
-
|
|
10036
|
+
|
|
10037
|
+
// src/cli/commands/find-missing-amends.ts
|
|
10038
|
+
var findMissingAmendsCommand = {
|
|
10039
|
+
name: "find-missing-amends",
|
|
10040
|
+
summary: "List decision IDs mentioned in a body but not declared",
|
|
10041
|
+
internal: true,
|
|
10042
|
+
usage: `Usage: dld find-missing-amends [--all]
|
|
10043
|
+
|
|
10044
|
+
Print <source-id>:<referenced-id> for each decision ID a record's body mentions without
|
|
10045
|
+
listing it in supersedes or amends. Only records changed since the last audit are checked.
|
|
10046
|
+
|
|
10047
|
+
Options:
|
|
10048
|
+
--all Check every record
|
|
10049
|
+
`,
|
|
10050
|
+
run(args, io, ctx) {
|
|
10051
|
+
const { values } = parseCommandArgs({ args: [...args], options: { all: { type: "boolean" } } });
|
|
10052
|
+
const found = findMissingAmends(ctx, loadProject(ctx), { all: values.all === true });
|
|
10053
|
+
for (const { source, referenced } of found) io.stdout(`${source}:${referenced}
|
|
10054
|
+
`);
|
|
10055
|
+
return EXIT_OK;
|
|
9548
10056
|
}
|
|
9549
|
-
|
|
9550
|
-
|
|
9551
|
-
|
|
9552
|
-
|
|
10057
|
+
};
|
|
10058
|
+
|
|
10059
|
+
// src/core/rename.ts
|
|
10060
|
+
import { Buffer as Buffer2 } from "node:buffer";
|
|
10061
|
+
import { join as join16, posix as posix4 } from "node:path";
|
|
10062
|
+
var idPattern = (id, flags = "") => new RegExp(`${id}(?![0-9])`, flags);
|
|
10063
|
+
function changedFiles(ctx, paths, commit2) {
|
|
10064
|
+
return nulSeparated(
|
|
10065
|
+
gitAt(ctx, paths.root)(
|
|
10066
|
+
"diff",
|
|
10067
|
+
"-z",
|
|
10068
|
+
"--find-renames",
|
|
10069
|
+
"--name-only",
|
|
10070
|
+
"--diff-filter=AMR",
|
|
10071
|
+
commit2
|
|
10072
|
+
)
|
|
10073
|
+
);
|
|
9553
10074
|
}
|
|
9554
|
-
|
|
9555
|
-
|
|
9556
|
-
const
|
|
9557
|
-
|
|
9558
|
-
|
|
9559
|
-
|
|
9560
|
-
|
|
9561
|
-
|
|
9562
|
-
|
|
9563
|
-
|
|
9564
|
-
|
|
9565
|
-
|
|
9566
|
-
|
|
9567
|
-
|
|
9568
|
-
|
|
9569
|
-
|
|
9570
|
-
|
|
9571
|
-
|
|
10075
|
+
function rewriteFile(ctx, path, edit) {
|
|
10076
|
+
if (!ctx.fs.isRegularFile(path)) return;
|
|
10077
|
+
const bytes = ctx.fs.readBytes(path);
|
|
10078
|
+
if (bytes.subarray(0, BINARY_SNIFF_BYTES).includes(0)) return;
|
|
10079
|
+
const text = Buffer2.from(bytes).toString("latin1");
|
|
10080
|
+
const edited = edit(text);
|
|
10081
|
+
if (edited === text) return;
|
|
10082
|
+
writeFileAtomic(ctx, path, Buffer2.from(edited, "latin1"), ctx.fs.fileMode(path));
|
|
10083
|
+
}
|
|
10084
|
+
function renameDecision(ctx, project, rename, base) {
|
|
10085
|
+
const { paths, config } = project;
|
|
10086
|
+
const { path, oldId, newId } = rename;
|
|
10087
|
+
const problem = renameProblem(paths, rename);
|
|
10088
|
+
if (problem !== void 0) throw new DldError(problem, EXIT_USAGE);
|
|
10089
|
+
const full = join16(paths.root, path);
|
|
10090
|
+
if (!ctx.fs.isRegularFile(full)) throw new DldError(`${path} not found.`);
|
|
10091
|
+
const record = parseRecord(ctx.fs.readFile(full), path);
|
|
10092
|
+
if (record.id !== oldId) throw new DldError(`${path} has id ${record.id}, not ${oldId}.`);
|
|
10093
|
+
const newPath = posix4.join(posix4.dirname(path), `${newId}.md`);
|
|
10094
|
+
const newFull = join16(paths.root, newPath);
|
|
10095
|
+
if (ctx.fs.lexists(newFull)) throw new DldError(`${newPath} already exists.`);
|
|
10096
|
+
verifyBase(ctx, paths, base);
|
|
10097
|
+
const since = mergeBase(ctx, paths, base);
|
|
10098
|
+
const scannable = new Set(listScannableFiles(ctx, scanOptionsFor(project)));
|
|
10099
|
+
gitAt(ctx, paths.root)("mv", "--", path, newPath);
|
|
10100
|
+
const substitute = (text) => text.replace(idPattern(oldId, "g"), newId);
|
|
10101
|
+
rewriteFile(ctx, newFull, (text) => substitute(setId(text, oldId, newId)));
|
|
10102
|
+
const decisions = `${decisionsPathspec(paths)}/`;
|
|
10103
|
+
const prefix = Buffer2.from(config.annotationPrefix, "utf8").toString("latin1");
|
|
10104
|
+
const oldAnnotation = `${prefix}(${oldId})`;
|
|
10105
|
+
const newAnnotation = `${prefix}(${newId})`;
|
|
10106
|
+
for (const file of changedFiles(ctx, paths, since)) {
|
|
10107
|
+
if (file === newPath) continue;
|
|
10108
|
+
if (file.startsWith(decisions)) {
|
|
10109
|
+
rewriteFile(ctx, join16(paths.root, file), substitute);
|
|
10110
|
+
} else if (scannable.has(file)) {
|
|
10111
|
+
rewriteFile(
|
|
10112
|
+
ctx,
|
|
10113
|
+
join16(paths.root, file),
|
|
10114
|
+
(text) => text.split(oldAnnotation).join(newAnnotation)
|
|
10115
|
+
);
|
|
9572
10116
|
}
|
|
9573
10117
|
}
|
|
9574
|
-
return
|
|
10118
|
+
return newPath;
|
|
9575
10119
|
}
|
|
9576
|
-
function
|
|
9577
|
-
const
|
|
9578
|
-
|
|
9579
|
-
|
|
9580
|
-
|
|
9581
|
-
|
|
9582
|
-
|
|
9583
|
-
const
|
|
9584
|
-
|
|
9585
|
-
|
|
10120
|
+
function findStaleMentions(ctx, { paths }, planText, base) {
|
|
10121
|
+
const renames = parseRenamePlan(paths, planText);
|
|
10122
|
+
if (renames.length === 0) throw new DldError("no rename plan on stdin.");
|
|
10123
|
+
verifyBase(ctx, paths, base);
|
|
10124
|
+
const decisions = `${decisionsPathspec(paths)}/`;
|
|
10125
|
+
const files = changedFiles(ctx, paths, mergeBase(ctx, paths, base)).flatMap((file) => {
|
|
10126
|
+
if (file.startsWith(decisions)) return [];
|
|
10127
|
+
const full = join16(paths.root, file);
|
|
10128
|
+
if (!ctx.fs.isRegularFile(full)) return [];
|
|
10129
|
+
const text = ctx.fs.readFile(full);
|
|
10130
|
+
if (text.slice(0, BINARY_SNIFF_BYTES).includes("\0")) return [];
|
|
10131
|
+
return [{ file, lines: text.split("\n").map((line) => line.replace(/\r$/, "")) }];
|
|
9586
10132
|
});
|
|
9587
|
-
|
|
9588
|
-
|
|
9589
|
-
|
|
9590
|
-
|
|
9591
|
-
|
|
9592
|
-
|
|
9593
|
-
|
|
9594
|
-
|
|
9595
|
-
throw new DldError(
|
|
9596
|
-
`${AGENTS_MD} is not a regular file (a symlink?); dld-kit does not edit through it. Point it at a regular file or add the dld-kit block by hand.`
|
|
9597
|
-
);
|
|
10133
|
+
const found = [];
|
|
10134
|
+
for (const { oldId, newId } of renames) {
|
|
10135
|
+
const pattern = idPattern(oldId);
|
|
10136
|
+
for (const { file, lines } of files) {
|
|
10137
|
+
lines.forEach((text, index) => {
|
|
10138
|
+
if (pattern.test(text)) found.push({ file, line: index + 1, oldId, newId, text });
|
|
10139
|
+
});
|
|
10140
|
+
}
|
|
9598
10141
|
}
|
|
9599
|
-
return
|
|
9600
|
-
}
|
|
9601
|
-
function switchWarnings(ctx, root, file, targets) {
|
|
9602
|
-
if (file !== AGENTS_MD || ctx.fs.lexists(join15(root, AGENTS_MD))) return [];
|
|
9603
|
-
const claudeMd = join15(root, CLAUDE_MD);
|
|
9604
|
-
if (!ctx.fs.exists(claudeMd) || ctx.fs.isDirectory(claudeMd)) return [];
|
|
9605
|
-
const switching = targets.filter((h) => !h.readsAll && h.instructions.includes(CLAUDE_MD));
|
|
9606
|
-
if (switching.length === 0) return [];
|
|
9607
|
-
const names = switching.map((h) => h.title).join(" and ");
|
|
9608
|
-
return [
|
|
9609
|
-
`dld-kit puts the rule block in a new AGENTS.md. ${names} will read AGENTS.md instead of CLAUDE.md from now on, so move anything ${switching.length === 1 ? "it needs" : "they need"} from CLAUDE.md into AGENTS.md.`
|
|
9610
|
-
];
|
|
10142
|
+
return found;
|
|
9611
10143
|
}
|
|
9612
|
-
|
|
9613
|
-
|
|
9614
|
-
const span = findBlock(claudeMd, CLAUDE_MD);
|
|
9615
|
-
const outside = span === void 0 ? claudeMd : claudeMd.slice(0, span.from) + claudeMd.slice(span.to);
|
|
9616
|
-
if (!outside.split(/\r?\n/).some((line) => LEGACY_HEADING.test(line))) return [];
|
|
9617
|
-
const section = "CLAUDE.md has a '## DLD (Decision-Linked Development)' section from an older dld-init.";
|
|
9618
|
-
return [
|
|
9619
|
-
ruleInstalled ? `${section} dld-kit now installs the rule separately, so that section can be removed.` : `${section} Install the rule with dld install-rule --agent <name> before removing it.`
|
|
9620
|
-
];
|
|
10144
|
+
function formatStaleMention({ file, line, oldId, newId, text }) {
|
|
10145
|
+
return `${file} ${line} ${oldId} ${newId} ${text}`;
|
|
9621
10146
|
}
|
|
9622
|
-
|
|
9623
|
-
|
|
9624
|
-
|
|
10147
|
+
|
|
10148
|
+
// src/cli/commands/find-stale-mentions.ts
|
|
10149
|
+
var findStaleMentionsCommand = {
|
|
10150
|
+
name: "find-stale-mentions",
|
|
10151
|
+
summary: "List remaining mentions of renamed decision IDs",
|
|
10152
|
+
internal: true,
|
|
10153
|
+
usage: `Usage: dld find-stale-mentions --base <ref> < plan
|
|
10154
|
+
|
|
10155
|
+
Read a rename plan (<path>\\t<DL-OLD>\\t<DL-NEW> per line) on standard input and print
|
|
10156
|
+
<path>\\t<line>\\t<DL-OLD>\\t<DL-NEW>\\t<text> for each remaining DL-OLD mention in changed
|
|
10157
|
+
files outside the decisions directory.
|
|
10158
|
+
|
|
10159
|
+
Options:
|
|
10160
|
+
--base <ref> Base ref for the local change set (required)
|
|
10161
|
+
`,
|
|
10162
|
+
run(args, io, ctx) {
|
|
10163
|
+
const { values } = parseCommandArgs({ args: [...args], options: { base: { type: "string" } } });
|
|
10164
|
+
const base = baseOption(values.base);
|
|
10165
|
+
const project = loadProject(ctx);
|
|
10166
|
+
for (const mention of findStaleMentions(ctx, project, ctx.readStdin(), base)) {
|
|
10167
|
+
io.stdout(`${formatStaleMention(mention)}
|
|
10168
|
+
`);
|
|
10169
|
+
}
|
|
10170
|
+
return EXIT_OK;
|
|
9625
10171
|
}
|
|
9626
|
-
|
|
9627
|
-
|
|
9628
|
-
|
|
9629
|
-
|
|
9630
|
-
|
|
9631
|
-
|
|
9632
|
-
|
|
9633
|
-
|
|
9634
|
-
|
|
9635
|
-
|
|
9636
|
-
|
|
9637
|
-
|
|
9638
|
-
|
|
9639
|
-
|
|
9640
|
-
|
|
9641
|
-
|
|
9642
|
-
|
|
10172
|
+
};
|
|
10173
|
+
|
|
10174
|
+
// src/cli/commands/init.ts
|
|
10175
|
+
import { join as join19, relative as relative11 } from "node:path";
|
|
10176
|
+
|
|
10177
|
+
// src/generate/install.ts
|
|
10178
|
+
import { dirname as dirname4, join as join18 } from "node:path";
|
|
10179
|
+
|
|
10180
|
+
// src/generate/generate.ts
|
|
10181
|
+
import { dirname as dirname3, join as join17 } from "node:path";
|
|
10182
|
+
var MANIFEST = "SKILL.md";
|
|
10183
|
+
var normalMode = (mode) => mode & 73 ? 493 : 420;
|
|
10184
|
+
function listFiles(ctx, dir, other = () => {
|
|
10185
|
+
}, prefix = "") {
|
|
10186
|
+
if (!ctx.fs.isDirectory(dir)) return [];
|
|
10187
|
+
const found = [];
|
|
10188
|
+
for (const entry of ctx.fs.readDir(dir).sort((a, b) => a.name < b.name ? -1 : 1)) {
|
|
10189
|
+
if (entry.name.startsWith(".")) continue;
|
|
10190
|
+
const rel = `${prefix}${entry.name}`;
|
|
10191
|
+
if (entry.isDirectory) {
|
|
10192
|
+
found.push(...listFiles(ctx, join17(dir, entry.name), other, `${rel}/`));
|
|
10193
|
+
} else {
|
|
10194
|
+
if (!entry.isFile) other(rel);
|
|
10195
|
+
found.push(rel);
|
|
9643
10196
|
}
|
|
9644
10197
|
}
|
|
9645
|
-
return
|
|
10198
|
+
return found;
|
|
9646
10199
|
}
|
|
9647
|
-
|
|
9648
|
-
|
|
9649
|
-
|
|
9650
|
-
|
|
9651
|
-
|
|
9652
|
-
);
|
|
10200
|
+
var utf8 = new TextDecoder("utf-8", { fatal: true });
|
|
10201
|
+
function readSupportingFile(ctx, path, source) {
|
|
10202
|
+
try {
|
|
10203
|
+
return utf8.decode(ctx.fs.readBytes(path));
|
|
10204
|
+
} catch (error) {
|
|
10205
|
+
if (error instanceof TypeError) throw new DldError(`${source}: not UTF-8 text`);
|
|
10206
|
+
throw error;
|
|
9653
10207
|
}
|
|
9654
10208
|
}
|
|
9655
|
-
function
|
|
9656
|
-
const
|
|
9657
|
-
|
|
9658
|
-
|
|
10209
|
+
function manifest(template, adapter, { body, usesDld }, version2) {
|
|
10210
|
+
const fields = adapter.fields.flatMap((field) => template.lines[field] ?? []);
|
|
10211
|
+
return [
|
|
10212
|
+
"---",
|
|
10213
|
+
...fields,
|
|
10214
|
+
...adapter.extraFrontmatter,
|
|
10215
|
+
...usesDld ? adapter.dldFrontmatter : [],
|
|
10216
|
+
"metadata:",
|
|
10217
|
+
` dld-kit-version: "${version2}"`,
|
|
10218
|
+
"---",
|
|
10219
|
+
`<!-- Generated by dld-kit from ${template.source}. Do not edit: updating the skills overwrites this file. -->`,
|
|
10220
|
+
body
|
|
10221
|
+
].join("\n");
|
|
10222
|
+
}
|
|
10223
|
+
function generateSkills(ctx, templatesDir, adapter, version2, { sourceDir = "templates/skills", extraFiles = /* @__PURE__ */ new Map() } = {}) {
|
|
10224
|
+
const skills = ctx.fs.readDir(templatesDir).filter((entry) => entry.isDirectory && !entry.name.startsWith(".")).map((entry) => entry.name).sort();
|
|
10225
|
+
const provided = /* @__PURE__ */ new Set();
|
|
10226
|
+
const support = /* @__PURE__ */ new Map();
|
|
10227
|
+
for (const skill of skills) {
|
|
10228
|
+
const files = listFiles(ctx, join17(templatesDir, skill), (rel) => {
|
|
10229
|
+
throw new DldError(`${sourceDir}/${skill}/${rel}: templates must be regular files`);
|
|
10230
|
+
}).filter((file) => file !== MANIFEST);
|
|
10231
|
+
support.set(skill, files);
|
|
10232
|
+
for (const file of files) provided.add(`${skill}/${file}`);
|
|
9659
10233
|
}
|
|
9660
|
-
|
|
9661
|
-
|
|
10234
|
+
for (const path of extraFiles.keys()) {
|
|
10235
|
+
if (provided.has(path)) throw new DldError(`${sourceDir}/${path}: also generated; remove it`);
|
|
10236
|
+
provided.add(path);
|
|
9662
10237
|
}
|
|
9663
|
-
|
|
9664
|
-
|
|
9665
|
-
|
|
9666
|
-
|
|
10238
|
+
const output = /* @__PURE__ */ new Map();
|
|
10239
|
+
for (const skill of skills) {
|
|
10240
|
+
const path = join17(templatesDir, skill, MANIFEST);
|
|
10241
|
+
const source = `${sourceDir}/${skill}/${MANIFEST}`;
|
|
10242
|
+
const template = parseTemplate(ctx.fs.readFile(path), skill, source);
|
|
10243
|
+
const rendered = renderBody(template, {
|
|
10244
|
+
exists: (ref) => provided.has(`${ref.skill}/${ref.path}`),
|
|
10245
|
+
script: (ref) => adapter.scriptRef(skill, ref),
|
|
10246
|
+
dld: () => adapter.dld(skill),
|
|
10247
|
+
dldSetup: () => adapter.dldSetup(skill)
|
|
10248
|
+
});
|
|
10249
|
+
if (!template.internal || adapter.internalManifest) {
|
|
10250
|
+
output.set(`${skill}/${MANIFEST}`, {
|
|
10251
|
+
content: manifest(template, adapter, rendered, version2),
|
|
10252
|
+
mode: 420
|
|
10253
|
+
});
|
|
10254
|
+
}
|
|
10255
|
+
for (const file of support.get(skill) ?? []) {
|
|
10256
|
+
const full = join17(templatesDir, skill, file);
|
|
10257
|
+
output.set(`${skill}/${file}`, {
|
|
10258
|
+
content: readSupportingFile(ctx, full, `${sourceDir}/${skill}/${file}`),
|
|
10259
|
+
mode: normalMode(ctx.fs.fileMode(full))
|
|
10260
|
+
});
|
|
9667
10261
|
}
|
|
9668
10262
|
}
|
|
9669
|
-
|
|
10263
|
+
for (const [path, file] of extraFiles) output.set(path, { ...file, mode: normalMode(file.mode) });
|
|
10264
|
+
return new Map([...output].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0));
|
|
9670
10265
|
}
|
|
9671
|
-
|
|
9672
|
-
|
|
9673
|
-
const
|
|
9674
|
-
|
|
9675
|
-
|
|
9676
|
-
|
|
9677
|
-
const
|
|
9678
|
-
|
|
9679
|
-
|
|
9680
|
-
|
|
9681
|
-
|
|
9682
|
-
|
|
9683
|
-
|
|
10266
|
+
var anyDldSkill = (skill) => skill.startsWith("dld-");
|
|
10267
|
+
function ownedSkill(path, files, generated, owns) {
|
|
10268
|
+
const skill = path.split("/", 1)[0] ?? "";
|
|
10269
|
+
return generated.has(skill) || owns(skill) || files.has(path);
|
|
10270
|
+
}
|
|
10271
|
+
function diffOutput(ctx, dir, files, owns = anyDldSkill) {
|
|
10272
|
+
const generated = new Set([...files.keys()].map((path) => path.split("/", 1)[0] ?? ""));
|
|
10273
|
+
const existing = new Set(
|
|
10274
|
+
listFiles(ctx, dir).filter((path) => ownedSkill(path, files, generated, owns))
|
|
10275
|
+
);
|
|
10276
|
+
const diff = { changed: [], missing: [], extra: [] };
|
|
10277
|
+
for (const [path, file] of files) {
|
|
10278
|
+
const full = join17(dir, path);
|
|
10279
|
+
if (!existing.has(path) || !ctx.fs.isRegularFile(full)) diff.missing.push(path);
|
|
10280
|
+
else if (ctx.fs.readFile(full) !== file.content || normalMode(ctx.fs.fileMode(full)) !== file.mode) {
|
|
10281
|
+
diff.changed.push(path);
|
|
9684
10282
|
}
|
|
9685
10283
|
}
|
|
9686
|
-
|
|
10284
|
+
diff.extra = [...existing].filter((path) => !files.has(path)).sort();
|
|
10285
|
+
return diff;
|
|
9687
10286
|
}
|
|
9688
|
-
function
|
|
9689
|
-
|
|
9690
|
-
|
|
9691
|
-
|
|
10287
|
+
function writeOutput(ctx, dir, files, owns = anyDldSkill) {
|
|
10288
|
+
const diff = diffOutput(ctx, dir, files, owns);
|
|
10289
|
+
for (const path of diff.extra) ctx.fs.remove(join17(dir, path));
|
|
10290
|
+
for (const path of [...diff.missing, ...diff.changed]) {
|
|
10291
|
+
const file = files.get(path);
|
|
10292
|
+
if (file === void 0) continue;
|
|
10293
|
+
const full = join17(dir, path);
|
|
10294
|
+
ctx.fs.mkdir(join17(full, ".."));
|
|
9692
10295
|
if (ctx.fs.lexists(full) && !ctx.fs.isRegularFile(full)) ctx.fs.remove(full);
|
|
9693
|
-
writeFileAtomic(ctx, full, content);
|
|
10296
|
+
writeFileAtomic(ctx, full, file.content, file.mode);
|
|
9694
10297
|
}
|
|
9695
|
-
|
|
9696
|
-
|
|
9697
|
-
|
|
9698
|
-
|
|
9699
|
-
|
|
9700
|
-
|
|
10298
|
+
removeEmptyParents(ctx, dir, diff.extra);
|
|
10299
|
+
return diff;
|
|
10300
|
+
}
|
|
10301
|
+
function removeEmptyParents(ctx, dir, paths) {
|
|
10302
|
+
const parents = /* @__PURE__ */ new Set();
|
|
10303
|
+
for (const path of paths) {
|
|
10304
|
+
for (let parent = dirname3(path); parent !== "."; parent = dirname3(parent)) parents.add(parent);
|
|
10305
|
+
}
|
|
10306
|
+
const deepestFirst = [...parents].sort((a, b) => b.split("/").length - a.split("/").length);
|
|
10307
|
+
for (const parent of deepestFirst) {
|
|
10308
|
+
const full = join17(dir, parent);
|
|
10309
|
+
if (ctx.fs.isDirectory(full) && ctx.fs.readDir(full).length === 0) ctx.fs.removeDir(full);
|
|
9701
10310
|
}
|
|
9702
10311
|
}
|
|
9703
10312
|
|
|
9704
10313
|
// src/generate/install.ts
|
|
9705
10314
|
function packageSource(ctx, cliPath2, command) {
|
|
9706
|
-
const templatesDir =
|
|
10315
|
+
const templatesDir = join18(dirname4(cliPath2), "..", "templates", "skills");
|
|
9707
10316
|
if (!ctx.fs.isDirectory(templatesDir)) {
|
|
9708
10317
|
throw new DldError(
|
|
9709
10318
|
`dld ${command} installs skills from the dld-kit npm package, but this copy of dld (${cliPath2}) has no templates beside it. Run it from the package instead: npx dld-kit@latest ${command}`
|
|
@@ -9711,16 +10320,51 @@ function packageSource(ctx, cliPath2, command) {
|
|
|
9711
10320
|
}
|
|
9712
10321
|
return { templatesDir, cli: ctx.fs.readFile(cliPath2) };
|
|
9713
10322
|
}
|
|
9714
|
-
var
|
|
9715
|
-
|
|
9716
|
-
|
|
10323
|
+
var SHIPPED_SKILLS = /* @__PURE__ */ new Set([
|
|
10324
|
+
"dld-adjust",
|
|
10325
|
+
"dld-audit",
|
|
10326
|
+
"dld-audit-auto",
|
|
10327
|
+
"dld-common",
|
|
10328
|
+
"dld-decide",
|
|
10329
|
+
"dld-implement",
|
|
10330
|
+
"dld-init",
|
|
10331
|
+
"dld-lookup",
|
|
10332
|
+
"dld-plan",
|
|
10333
|
+
"dld-reindex",
|
|
10334
|
+
"dld-retrofit",
|
|
10335
|
+
"dld-snapshot",
|
|
10336
|
+
"dld-status"
|
|
10337
|
+
]);
|
|
10338
|
+
var isShipped = (name) => SHIPPED_SKILLS.has(name);
|
|
10339
|
+
function dldEntries(ctx, root, layout) {
|
|
10340
|
+
const dir = join18(root, layout.dir);
|
|
9717
10341
|
if (!ctx.fs.isDirectory(dir)) return [];
|
|
9718
|
-
return ctx.fs.readDir(dir).filter((entry) => !entry.isFile &&
|
|
10342
|
+
return ctx.fs.readDir(dir).filter((entry) => !entry.isFile && entry.name.startsWith("dld-")).sort((a, b) => a.name < b.name ? -1 : 1);
|
|
10343
|
+
}
|
|
10344
|
+
function dldKitSkills(ctx, root, layout) {
|
|
10345
|
+
const dir = join18(root, layout.dir);
|
|
10346
|
+
const owned = /* @__PURE__ */ new Set();
|
|
10347
|
+
for (const entry of dldEntries(ctx, root, layout)) {
|
|
10348
|
+
if (isShipped(entry.name)) {
|
|
10349
|
+
owned.add(entry.name);
|
|
10350
|
+
continue;
|
|
10351
|
+
}
|
|
10352
|
+
if (!entry.isDirectory) continue;
|
|
10353
|
+
const skillMd = join18(dir, entry.name, "SKILL.md");
|
|
10354
|
+
if (!ctx.fs.isRegularFile(skillMd)) continue;
|
|
10355
|
+
const content = ctx.fs.readFile(skillMd);
|
|
10356
|
+
const notice2 = `Generated by dld-kit from templates/skills/${entry.name}/SKILL.md.`;
|
|
10357
|
+
if (SKILL_STAMP.test(content) && content.includes(notice2)) owned.add(entry.name);
|
|
10358
|
+
}
|
|
10359
|
+
return owned;
|
|
10360
|
+
}
|
|
10361
|
+
function ownedSkills(ctx, root, layout) {
|
|
10362
|
+
return [...dldKitSkills(ctx, root, layout)].sort();
|
|
9719
10363
|
}
|
|
9720
10364
|
function refuseSymlinkedSkills(ctx, root, layout) {
|
|
9721
|
-
const dir =
|
|
10365
|
+
const dir = join18(root, layout.dir);
|
|
9722
10366
|
if (!ctx.fs.isDirectory(dir)) return;
|
|
9723
|
-
const linked = ctx.fs.readDir(dir).filter((entry) =>
|
|
10367
|
+
const linked = ctx.fs.readDir(dir).filter((entry) => isShipped(entry.name) && !entry.isDirectory && !entry.isFile).map((entry) => `${layout.dir}/${entry.name}`).sort();
|
|
9724
10368
|
if (linked.length === 0) return;
|
|
9725
10369
|
throw new DldError(
|
|
9726
10370
|
`${linked.join(", ")} ${linked.length === 1 ? "is a symlink" : "are symlinks"}, so another installer, such as npx skills, manages these skills. Update them with npx skills update, or remove them and run this command again. To set up DLD without touching them, run the dld-init skill or dld install-rule.`
|
|
@@ -9728,7 +10372,7 @@ function refuseSymlinkedSkills(ctx, root, layout) {
|
|
|
9728
10372
|
}
|
|
9729
10373
|
var SKILLS_LOCK = "skills-lock.json";
|
|
9730
10374
|
function skillsLockWarning(ctx, root) {
|
|
9731
|
-
const path =
|
|
10375
|
+
const path = join18(root, SKILLS_LOCK);
|
|
9732
10376
|
if (!ctx.fs.isRegularFile(path)) return [];
|
|
9733
10377
|
let skills;
|
|
9734
10378
|
try {
|
|
@@ -9737,7 +10381,7 @@ function skillsLockWarning(ctx, root) {
|
|
|
9737
10381
|
return [];
|
|
9738
10382
|
}
|
|
9739
10383
|
if (typeof skills !== "object" || skills === null) return [];
|
|
9740
|
-
const listed = Object.keys(skills).filter(
|
|
10384
|
+
const listed = Object.keys(skills).filter(isShipped).sort();
|
|
9741
10385
|
if (listed.length === 0) return [];
|
|
9742
10386
|
return [
|
|
9743
10387
|
`${SKILLS_LOCK} lists ${listed.join(", ")}: npx skills manages those skills, so dld update and npx skills update overwrite each other's copies. Update them with one of the two.`
|
|
@@ -9764,8 +10408,8 @@ function installedStamps(ctx, root) {
|
|
|
9764
10408
|
for (const layout of LAYOUTS) {
|
|
9765
10409
|
for (const skill of ownedSkills(ctx, root, layout)) {
|
|
9766
10410
|
const path = `${layout.dir}/${skill}/SKILL.md`;
|
|
9767
|
-
if (!ctx.fs.isRegularFile(
|
|
9768
|
-
stamps.push({ path, version: SKILL_STAMP.exec(ctx.fs.readFile(
|
|
10411
|
+
if (!ctx.fs.isRegularFile(join18(root, path))) continue;
|
|
10412
|
+
stamps.push({ path, version: SKILL_STAMP.exec(ctx.fs.readFile(join18(root, path)))?.[1] });
|
|
9769
10413
|
}
|
|
9770
10414
|
}
|
|
9771
10415
|
return [...stamps, ...installedRuleStamps(ctx, root)];
|
|
@@ -9824,23 +10468,40 @@ function planInstall(ctx, root, request) {
|
|
|
9824
10468
|
const files = generateSkills(ctx, source.templatesDir, layout.adapter, request.version, {
|
|
9825
10469
|
extraFiles: cli
|
|
9826
10470
|
});
|
|
9827
|
-
|
|
9828
|
-
|
|
9829
|
-
|
|
9830
|
-
harnesses: request.harnesses
|
|
10471
|
+
const owned = dldKitSkills(ctx, root, layout);
|
|
10472
|
+
const owns = (skill) => owned.has(skill);
|
|
10473
|
+
return { layout, files, owns };
|
|
9831
10474
|
});
|
|
10475
|
+
const rule = planRule(
|
|
10476
|
+
ctx,
|
|
10477
|
+
root,
|
|
10478
|
+
request.rules,
|
|
10479
|
+
request.version,
|
|
10480
|
+
{ harnesses: request.harnesses },
|
|
10481
|
+
request.ruleText
|
|
10482
|
+
);
|
|
9832
10483
|
const warnings = layouts.length > 0 ? skillsLockWarning(ctx, root) : [];
|
|
10484
|
+
for (const { layout, files, owns } of skills) {
|
|
10485
|
+
const generated = new Set([...files.keys()].map((path) => path.split("/", 1)[0] ?? ""));
|
|
10486
|
+
for (const { name } of dldEntries(ctx, root, layout)) {
|
|
10487
|
+
if (generated.has(name) || owns(name)) continue;
|
|
10488
|
+
warnings.push(
|
|
10489
|
+
`${layout.dir}/${name} is not a dld-kit skill (no dld-kit stamp), so it is left in place.`
|
|
10490
|
+
);
|
|
10491
|
+
}
|
|
10492
|
+
}
|
|
9833
10493
|
return { skills, rule, warnings };
|
|
9834
10494
|
}
|
|
9835
10495
|
function applyInstall(ctx, root, plan) {
|
|
9836
|
-
const skills = plan.skills.map(({ layout, files }) => {
|
|
9837
|
-
const diff = writeOutput(ctx,
|
|
10496
|
+
const skills = plan.skills.map(({ layout, files, owns }) => {
|
|
10497
|
+
const diff = writeOutput(ctx, join18(root, layout.dir), files, owns);
|
|
9838
10498
|
const written = diff.changed.length + diff.missing.length;
|
|
9839
10499
|
return {
|
|
9840
10500
|
dir: layout.dir,
|
|
9841
10501
|
written,
|
|
9842
10502
|
removed: diff.extra.length,
|
|
9843
|
-
unchanged: files.size - written
|
|
10503
|
+
unchanged: files.size - written,
|
|
10504
|
+
removedPaths: diff.extra.map((path) => `${layout.dir}/${path}`)
|
|
9844
10505
|
};
|
|
9845
10506
|
});
|
|
9846
10507
|
applyRulePlan(ctx, root, plan.rule);
|
|
@@ -9852,85 +10513,6 @@ function applyInstall(ctx, root, plan) {
|
|
|
9852
10513
|
};
|
|
9853
10514
|
}
|
|
9854
10515
|
|
|
9855
|
-
// src/cli/install.ts
|
|
9856
|
-
var AGENT_HELP = `Agents: ${HARNESS_NAMES.join(", ")}. --agent takes a comma-separated list and can be repeated.`;
|
|
9857
|
-
function parseAgents(values) {
|
|
9858
|
-
const names = (values ?? []).flatMap((value) => value.split(",")).map((name) => name.trim());
|
|
9859
|
-
const harnesses = /* @__PURE__ */ new Set();
|
|
9860
|
-
for (const name of names) {
|
|
9861
|
-
if (name === "") continue;
|
|
9862
|
-
const harness = findHarness(name);
|
|
9863
|
-
if (harness === void 0) {
|
|
9864
|
-
throw new UsageError(`unknown agent '${name}'; expected one of: ${HARNESS_NAMES.join(", ")}`);
|
|
9865
|
-
}
|
|
9866
|
-
harnesses.add(harness);
|
|
9867
|
-
}
|
|
9868
|
-
return HARNESSES.filter((harness) => harnesses.has(harness));
|
|
9869
|
-
}
|
|
9870
|
-
async function selectHarnesses(io, detected, requested, yes) {
|
|
9871
|
-
const selected = /* @__PURE__ */ new Set([...detected.map((d) => d.harness), ...requested]);
|
|
9872
|
-
const ordered = () => HARNESSES.filter((harness) => selected.has(harness));
|
|
9873
|
-
if (io.prompt === void 0 || yes) {
|
|
9874
|
-
if (selected.size === 0) {
|
|
9875
|
-
throw new UsageError(
|
|
9876
|
-
`no agent detected in this project; name them with --agent (${HARNESS_NAMES.join(", ")})`
|
|
9877
|
-
);
|
|
9878
|
-
}
|
|
9879
|
-
return ordered();
|
|
9880
|
-
}
|
|
9881
|
-
const found = new Map(detected.map((d) => [d.harness, d.marker]));
|
|
9882
|
-
const width = Math.max(...HARNESS_NAMES.map((name) => name.length));
|
|
9883
|
-
for (; ; ) {
|
|
9884
|
-
const lines = HARNESSES.map((harness, i) => {
|
|
9885
|
-
const box = selected.has(harness) ? "[x]" : "[ ]";
|
|
9886
|
-
const marker = found.get(harness);
|
|
9887
|
-
const note = marker === void 0 ? "" : ` (found ${marker})`;
|
|
9888
|
-
return ` ${i + 1}. ${box} ${harness.name.padEnd(width)} ${harness.title}${note}
|
|
9889
|
-
`;
|
|
9890
|
-
});
|
|
9891
|
-
io.stdout(`Install DLD for these agents:
|
|
9892
|
-
${lines.join("")}`);
|
|
9893
|
-
const answer = (await io.prompt("Numbers to toggle, or Enter to continue: ")).trim();
|
|
9894
|
-
if (answer === "") {
|
|
9895
|
-
if (selected.size > 0) return ordered();
|
|
9896
|
-
io.stdout("Select at least one agent.\n");
|
|
9897
|
-
continue;
|
|
9898
|
-
}
|
|
9899
|
-
const picks = answer.split(/[\s,]+/).map((pick) => HARNESSES[Number(pick) - 1]);
|
|
9900
|
-
if (picks.some((harness) => harness === void 0)) {
|
|
9901
|
-
io.stdout(`Enter numbers from 1 to ${HARNESSES.length}.
|
|
9902
|
-
`);
|
|
9903
|
-
continue;
|
|
9904
|
-
}
|
|
9905
|
-
for (const harness of picks) {
|
|
9906
|
-
if (harness === void 0) continue;
|
|
9907
|
-
if (selected.has(harness)) selected.delete(harness);
|
|
9908
|
-
else selected.add(harness);
|
|
9909
|
-
}
|
|
9910
|
-
}
|
|
9911
|
-
}
|
|
9912
|
-
function cliPath(io) {
|
|
9913
|
-
if (io.cliPath === void 0) throw new DldError("cannot locate the running dld file");
|
|
9914
|
-
return io.cliPath;
|
|
9915
|
-
}
|
|
9916
|
-
function printReport(io, report2) {
|
|
9917
|
-
for (const { dir, written, removed, unchanged } of report2.skills) {
|
|
9918
|
-
io.stdout(`${dir}: ${written} written, ${removed} removed, ${unchanged} unchanged
|
|
9919
|
-
`);
|
|
9920
|
-
}
|
|
9921
|
-
for (const path of report2.ruleWritten) {
|
|
9922
|
-
io.stdout(
|
|
9923
|
-
path === CLAUDE_IMPORT_FILE ? `Wrote ${path} (imports AGENTS.md for Claude Code)
|
|
9924
|
-
` : `Wrote the DLD rule to ${path}
|
|
9925
|
-
`
|
|
9926
|
-
);
|
|
9927
|
-
}
|
|
9928
|
-
for (const path of report2.ruleRemoved) io.stdout(`Removed ${path} (the rule is in the block)
|
|
9929
|
-
`);
|
|
9930
|
-
for (const warning of report2.warnings) io.stderr(`Warning: ${warning}
|
|
9931
|
-
`);
|
|
9932
|
-
}
|
|
9933
|
-
|
|
9934
10516
|
// src/cli/commands/init.ts
|
|
9935
10517
|
var initCommand = {
|
|
9936
10518
|
name: "init",
|
|
@@ -9967,7 +10549,7 @@ ${AGENT_HELP}
|
|
|
9967
10549
|
throw new UsageError("--namespaces needs at least one namespace");
|
|
9968
10550
|
}
|
|
9969
10551
|
const root = findProjectRoot(ctx);
|
|
9970
|
-
if (ctx.fs.lexists(
|
|
10552
|
+
if (ctx.fs.lexists(join19(root, CONFIG_FILE))) {
|
|
9971
10553
|
throw new DldError(
|
|
9972
10554
|
`DLD is already set up here (${CONFIG_FILE} exists). Run dld update to refresh the skills and rule, or dld update --agent <name> to add an agent.`
|
|
9973
10555
|
);
|
|
@@ -9993,10 +10575,11 @@ ${AGENT_HELP}
|
|
|
9993
10575
|
createDirectories(ctx, project);
|
|
9994
10576
|
writeIndex(ctx, project.paths, renderIndex([], project.config.mode));
|
|
9995
10577
|
const report2 = applyInstall(ctx, root, plan);
|
|
9996
|
-
const index =
|
|
10578
|
+
const index = relative11(root, indexPath(project.paths));
|
|
9997
10579
|
io.stdout(`Created ${CONFIG_FILE} and ${index}
|
|
9998
10580
|
`);
|
|
9999
10581
|
printReport(io, report2);
|
|
10582
|
+
ensureIndexMergeAttribute(io, ctx, project);
|
|
10000
10583
|
io.stdout(
|
|
10001
10584
|
`
|
|
10002
10585
|
DLD is set up for: ${harnesses.map((harness) => harness.name).join(", ")}. Commit these files so everyone gets the same skills and rule.
|
|
@@ -10038,7 +10621,9 @@ ${AGENT_HELP}
|
|
|
10038
10621
|
rules,
|
|
10039
10622
|
version,
|
|
10040
10623
|
force: values.force === true,
|
|
10041
|
-
harnesses: requested
|
|
10624
|
+
harnesses: requested,
|
|
10625
|
+
// @decision(DL-074)
|
|
10626
|
+
ruleText: projectRuleText(ctx, root)
|
|
10042
10627
|
});
|
|
10043
10628
|
const report2 = applyInstall(ctx, root, plan);
|
|
10044
10629
|
printReport(io, report2);
|
|
@@ -10049,6 +10634,50 @@ ${AGENT_HELP}
|
|
|
10049
10634
|
}
|
|
10050
10635
|
};
|
|
10051
10636
|
|
|
10637
|
+
// src/core/proposed.ts
|
|
10638
|
+
import { basename as basename8, relative as relative12, sep as sep7 } from "node:path";
|
|
10639
|
+
function listProposed(ctx, project, base) {
|
|
10640
|
+
const { paths } = project;
|
|
10641
|
+
verifyBase(ctx, paths, base, " Fetch first or pass --base.");
|
|
10642
|
+
const atBase = integratedRecords(ctx, paths, base).byId;
|
|
10643
|
+
return listRecordFiles(ctx, paths.recordsDir).map((file) => {
|
|
10644
|
+
const path = relative12(paths.root, file).split(sep7).join("/");
|
|
10645
|
+
return { file, path, record: parseRecord(ctx.fs.readFile(file), path) };
|
|
10646
|
+
}).filter(({ record }) => record.status === "proposed").sort((a, b) => recordNumber(a.file) - recordNumber(b.file)).map(({ file, path, record }) => {
|
|
10647
|
+
const id = basename8(file, ".md");
|
|
10648
|
+
return { id, title: record.title, path, addedOnBranch: !atBase.has(id) };
|
|
10649
|
+
});
|
|
10650
|
+
}
|
|
10651
|
+
function formatProposed({ id, title, addedOnBranch }) {
|
|
10652
|
+
return `${id} ${addedOnBranch ? "branch" : "base"} ${title}`;
|
|
10653
|
+
}
|
|
10654
|
+
|
|
10655
|
+
// src/cli/commands/list-proposed.ts
|
|
10656
|
+
var listProposedCommand = {
|
|
10657
|
+
name: "list-proposed",
|
|
10658
|
+
summary: "List proposed decisions, marking those added on this branch",
|
|
10659
|
+
internal: true,
|
|
10660
|
+
usage: `Usage: dld list-proposed [--base <ref>]
|
|
10661
|
+
|
|
10662
|
+
Print <DL-NNN>\\t<branch|base>\\t<title> for each proposed decision, ascending by ID: branch
|
|
10663
|
+
when its file does not exist at the merge-base with the base branch (added on this branch,
|
|
10664
|
+
committed or not), base otherwise.
|
|
10665
|
+
|
|
10666
|
+
Options:
|
|
10667
|
+
--base <ref> Base ref (${DEFAULT_BASE_HELP})
|
|
10668
|
+
`,
|
|
10669
|
+
run(args, io, ctx) {
|
|
10670
|
+
const { values } = parseCommandArgs({ args: [...args], options: { base: { type: "string" } } });
|
|
10671
|
+
const project = loadProject(ctx);
|
|
10672
|
+
const base = baseOption(values.base, { ctx, project });
|
|
10673
|
+
for (const proposed of listProposed(ctx, project, base)) {
|
|
10674
|
+
io.stdout(`${formatProposed(proposed)}
|
|
10675
|
+
`);
|
|
10676
|
+
}
|
|
10677
|
+
return EXIT_OK;
|
|
10678
|
+
}
|
|
10679
|
+
};
|
|
10680
|
+
|
|
10052
10681
|
// src/cli/commands/list-taken-ids.ts
|
|
10053
10682
|
var listTakenIdsCommand = {
|
|
10054
10683
|
name: "list-taken-ids",
|
|
@@ -10060,12 +10689,13 @@ Print the decision IDs on the base branch and in records touched by open pull re
|
|
|
10060
10689
|
(via gh, when available), one per line, sorted.
|
|
10061
10690
|
|
|
10062
10691
|
Options:
|
|
10063
|
-
--base <ref> Base ref (
|
|
10692
|
+
--base <ref> Base ref (${DEFAULT_BASE_HELP})
|
|
10064
10693
|
`,
|
|
10065
10694
|
run(args, io, ctx) {
|
|
10066
10695
|
const { values } = parseCommandArgs({ args: [...args], options: { base: { type: "string" } } });
|
|
10067
|
-
const
|
|
10068
|
-
const
|
|
10696
|
+
const project = loadProject(ctx);
|
|
10697
|
+
const base = baseOption(values.base, { ctx, project });
|
|
10698
|
+
const { ids, skipped } = listTakenIds(ctx, project, base);
|
|
10069
10699
|
for (const id of ids) io.stdout(`${id}
|
|
10070
10700
|
`);
|
|
10071
10701
|
if (skipped !== void 0) io.stderr(skippedNotice(skipped));
|
|
@@ -10099,12 +10729,13 @@ Print <path>\\t<DL-OLD>\\t<DL-NEW> for each colliding local decision, assigning
|
|
|
10099
10729
|
IDs. Prints nothing when there are no collisions.
|
|
10100
10730
|
|
|
10101
10731
|
Options:
|
|
10102
|
-
--base <ref> Base ref (
|
|
10732
|
+
--base <ref> Base ref (${DEFAULT_BASE_HELP})
|
|
10103
10733
|
`,
|
|
10104
10734
|
run(args, io, ctx) {
|
|
10105
10735
|
const { values } = parseCommandArgs({ args: [...args], options: { base: { type: "string" } } });
|
|
10106
|
-
const
|
|
10107
|
-
const
|
|
10736
|
+
const project = loadProject(ctx);
|
|
10737
|
+
const base = baseOption(values.base, { ctx, project });
|
|
10738
|
+
const { renames, skipped } = planRenames(ctx, project, base);
|
|
10108
10739
|
for (const rename of renames) io.stdout(`${formatRename(rename)}
|
|
10109
10740
|
`);
|
|
10110
10741
|
if (skipped !== void 0) io.stderr(skippedNotice(skipped));
|
|
@@ -10113,6 +10744,7 @@ Options:
|
|
|
10113
10744
|
};
|
|
10114
10745
|
|
|
10115
10746
|
// src/cli/commands/regenerate-index.ts
|
|
10747
|
+
import { basename as basename9, relative as relative13 } from "node:path";
|
|
10116
10748
|
var regenerateIndexCommand = {
|
|
10117
10749
|
name: "regenerate-index",
|
|
10118
10750
|
summary: "Rebuild INDEX.md from the decision records",
|
|
@@ -10133,6 +10765,11 @@ Options:
|
|
|
10133
10765
|
if (!ctx.fs.isDirectory(paths.recordsDir)) {
|
|
10134
10766
|
throw new DldError(`records directory not found at ${paths.recordsDir}`);
|
|
10135
10767
|
}
|
|
10768
|
+
for (const files of duplicateRecordFiles(ctx, paths.recordsDir)) {
|
|
10769
|
+
const ids = [...new Set(files.map((path) => basename9(path, ".md")))].join(" / ");
|
|
10770
|
+
const shown = files.map((path) => relative13(paths.root, path)).join(", ");
|
|
10771
|
+
ctx.warn(`${ids} is in more than one file: ${shown}`);
|
|
10772
|
+
}
|
|
10136
10773
|
const rows = collectIndexRows(ctx, paths, values["include-base"]);
|
|
10137
10774
|
writeIndex(ctx, paths, renderIndex(rows, config.mode));
|
|
10138
10775
|
io.stdout(rows.length === 0 ? "INDEX.md regenerated (empty).\n" : "INDEX.md regenerated.\n");
|
|
@@ -10140,6 +10777,76 @@ Options:
|
|
|
10140
10777
|
}
|
|
10141
10778
|
};
|
|
10142
10779
|
|
|
10780
|
+
// src/core/related.ts
|
|
10781
|
+
import { basename as basename10, relative as relative14, sep as sep8 } from "node:path";
|
|
10782
|
+
var RELATIONS = ["amends", "supersedes", "amended-by", "superseded-by"];
|
|
10783
|
+
var idNumber3 = (id) => DECISION_ID.test(id) ? Number.parseInt(id.slice(3), 10) : Number.POSITIVE_INFINITY;
|
|
10784
|
+
function relatedDecisions(ctx, project, id) {
|
|
10785
|
+
const { paths } = project;
|
|
10786
|
+
const targetFile = findRecordFile(ctx, paths.recordsDir, id, paths.root);
|
|
10787
|
+
if (targetFile === void 0) throw new DldError(`decision ${id} not found.`);
|
|
10788
|
+
const records = listRecordFiles(ctx, paths.recordsDir).map((file) => {
|
|
10789
|
+
const path = relative14(paths.root, file).split(sep8).join("/");
|
|
10790
|
+
return { file, id: basename10(file, ".md"), record: parseRecord(ctx.fs.readFile(file), path) };
|
|
10791
|
+
});
|
|
10792
|
+
const byId = new Map(records.map((entry) => [entry.id, entry.record]));
|
|
10793
|
+
const target = records.find((entry) => entry.file === targetFile)?.record;
|
|
10794
|
+
if (target === void 0) throw new DldError(`decision ${id} not found.`);
|
|
10795
|
+
const related = [];
|
|
10796
|
+
const add = (relation, other) => {
|
|
10797
|
+
const record = byId.get(other);
|
|
10798
|
+
related.push({
|
|
10799
|
+
relation,
|
|
10800
|
+
id: other,
|
|
10801
|
+
status: record?.status ?? "missing",
|
|
10802
|
+
title: record?.title ?? ""
|
|
10803
|
+
});
|
|
10804
|
+
};
|
|
10805
|
+
for (const other of target.amends) add("amends", other);
|
|
10806
|
+
for (const other of target.supersedes) add("supersedes", other);
|
|
10807
|
+
for (const entry of records) {
|
|
10808
|
+
if (entry.file === targetFile) continue;
|
|
10809
|
+
if (entry.record.amends.includes(id)) add("amended-by", entry.id);
|
|
10810
|
+
if (entry.record.supersedes.includes(id)) add("superseded-by", entry.id);
|
|
10811
|
+
}
|
|
10812
|
+
return related.sort(
|
|
10813
|
+
(a, b) => idNumber3(a.id) - idNumber3(b.id) || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0) || RELATIONS.indexOf(a.relation) - RELATIONS.indexOf(b.relation)
|
|
10814
|
+
);
|
|
10815
|
+
}
|
|
10816
|
+
function formatRelated({ relation, id, status, title }) {
|
|
10817
|
+
return `${relation} ${id} ${status} ${title}`;
|
|
10818
|
+
}
|
|
10819
|
+
|
|
10820
|
+
// src/cli/commands/related-decisions.ts
|
|
10821
|
+
var relatedDecisionsCommand = {
|
|
10822
|
+
name: "related-decisions",
|
|
10823
|
+
summary: "List the decisions a decision amends or supersedes, and those that amend or supersede it",
|
|
10824
|
+
internal: true,
|
|
10825
|
+
usage: `Usage: dld related-decisions <DL-NNN>
|
|
10826
|
+
|
|
10827
|
+
Print <relation>\\t<DL-NNN>\\t<status>\\t<title> for each related decision, ascending by ID.
|
|
10828
|
+
Relations: amends and supersedes (from the decision's own frontmatter), amended-by and
|
|
10829
|
+
superseded-by (records whose amends or supersedes list it). A listed ID with no record has
|
|
10830
|
+
status missing. Exits 1 if the decision does not exist.
|
|
10831
|
+
`,
|
|
10832
|
+
run(args, io, ctx) {
|
|
10833
|
+
const { positionals } = parseCommandArgs({
|
|
10834
|
+
args: [...args],
|
|
10835
|
+
options: {},
|
|
10836
|
+
allowPositionals: true
|
|
10837
|
+
});
|
|
10838
|
+
const [id, ...extra] = positionals;
|
|
10839
|
+
if (id === void 0 || !DECISION_ID.test(id))
|
|
10840
|
+
throw new UsageError("expected one decision ID like DL-001");
|
|
10841
|
+
if (extra.length > 0) throw new UsageError(`unexpected argument '${extra[0]}'`);
|
|
10842
|
+
for (const related of relatedDecisions(ctx, loadProject(ctx), id)) {
|
|
10843
|
+
io.stdout(`${formatRelated(related)}
|
|
10844
|
+
`);
|
|
10845
|
+
}
|
|
10846
|
+
return EXIT_OK;
|
|
10847
|
+
}
|
|
10848
|
+
};
|
|
10849
|
+
|
|
10143
10850
|
// src/cli/commands/rename-decision.ts
|
|
10144
10851
|
var renameDecisionCommand = {
|
|
10145
10852
|
name: "rename-decision",
|
|
@@ -10154,7 +10861,7 @@ Options:
|
|
|
10154
10861
|
--old <DL-OLD> Current ID (required)
|
|
10155
10862
|
--new <DL-NEW> New ID (required)
|
|
10156
10863
|
--path <path> Record path relative to the project root (required)
|
|
10157
|
-
--base <ref> Base ref for the local change set (
|
|
10864
|
+
--base <ref> Base ref for the local change set (${DEFAULT_BASE_HELP})
|
|
10158
10865
|
`,
|
|
10159
10866
|
run(args, io, ctx) {
|
|
10160
10867
|
const { values } = parseCommandArgs({
|
|
@@ -10168,8 +10875,9 @@ Options:
|
|
|
10168
10875
|
});
|
|
10169
10876
|
const { old: oldId, new: newId, path } = values;
|
|
10170
10877
|
if (!oldId || !newId || !path) throw new DldError("--old, --new, and --path are required.");
|
|
10171
|
-
const
|
|
10172
|
-
const
|
|
10878
|
+
const project = loadProject(ctx);
|
|
10879
|
+
const base = baseOption(values.base, { ctx, project });
|
|
10880
|
+
const newPath = renameDecision(ctx, project, { path, oldId, newId }, base);
|
|
10173
10881
|
io.stdout(`${path} -> ${newPath}
|
|
10174
10882
|
`);
|
|
10175
10883
|
return EXIT_OK;
|
|
@@ -10177,19 +10885,63 @@ Options:
|
|
|
10177
10885
|
};
|
|
10178
10886
|
|
|
10179
10887
|
// src/cli/commands/resolve-base.ts
|
|
10888
|
+
import { join as join20 } from "node:path";
|
|
10180
10889
|
var resolveBaseCommand = {
|
|
10181
10890
|
name: "resolve-base",
|
|
10182
10891
|
summary: "Print the base ref to check decision IDs against",
|
|
10183
10892
|
internal: true,
|
|
10184
10893
|
usage: `Usage: dld resolve-base
|
|
10185
10894
|
|
|
10186
|
-
Print the
|
|
10187
|
-
origin/
|
|
10895
|
+
Print the base ref: base_branch from dld.config.yaml (origin/<name>, else <name>); else the
|
|
10896
|
+
current branch's upstream when it tracks a differently named branch; else origin/HEAD; else
|
|
10897
|
+
origin/main, then main. Only a ref that exists is printed; when none does, exit 1.
|
|
10188
10898
|
`,
|
|
10189
10899
|
run(args, io, ctx) {
|
|
10190
10900
|
parseCommandArgs({ args: [...args], options: {} });
|
|
10191
|
-
|
|
10901
|
+
const root = findProjectRoot(ctx);
|
|
10902
|
+
const config = ctx.fs.exists(join20(root, CONFIG_FILE)) ? loadConfig(ctx, root) : void 0;
|
|
10903
|
+
io.stdout(`${resolveBase(ctx, config?.baseBranch)}
|
|
10904
|
+
`);
|
|
10905
|
+
return EXIT_OK;
|
|
10906
|
+
}
|
|
10907
|
+
};
|
|
10908
|
+
|
|
10909
|
+
// src/cli/commands/restore-decision-prose.ts
|
|
10910
|
+
var restoreDecisionProseCommand = {
|
|
10911
|
+
name: "restore-decision-prose",
|
|
10912
|
+
summary: "Restore the body of decisions already on the base branch",
|
|
10913
|
+
internal: true,
|
|
10914
|
+
usage: `Usage: dld restore-decision-prose [--base <ref>] [--uncommitted] <DL-NNN> [DL-NNN ...]
|
|
10915
|
+
|
|
10916
|
+
Put back the body (the text after the frontmatter), id and timestamp of each decision as
|
|
10917
|
+
they are at the merge-base of the base branch and HEAD, keeping the rest of the current
|
|
10918
|
+
frontmatter. A deleted decision
|
|
10919
|
+
is restored whole and staged. Fails for a draft.
|
|
10920
|
+
|
|
10921
|
+
Options:
|
|
10922
|
+
--base <ref> Base ref (${DEFAULT_BASE_HELP})
|
|
10923
|
+
--uncommitted Restore from HEAD instead of the merge-base: undo only uncommitted edits
|
|
10924
|
+
`,
|
|
10925
|
+
run(args, io, ctx) {
|
|
10926
|
+
const { values, positionals: ids } = parseCommandArgs({
|
|
10927
|
+
args: [...args],
|
|
10928
|
+
options: { base: { type: "string" }, uncommitted: { type: "boolean" } },
|
|
10929
|
+
allowPositionals: true
|
|
10930
|
+
});
|
|
10931
|
+
if (ids.length === 0) throw new UsageError("expected at least one decision ID");
|
|
10932
|
+
const bad = ids.find((id) => !DECISION_ID.test(id));
|
|
10933
|
+
if (bad !== void 0) throw new UsageError(`expected decision IDs like DL-001, got '${bad}'`);
|
|
10934
|
+
const project = loadProject(ctx);
|
|
10935
|
+
const base = editBase(
|
|
10936
|
+
ctx,
|
|
10937
|
+
project,
|
|
10938
|
+
values.base === void 0 ? void 0 : baseOption(values.base)
|
|
10939
|
+
);
|
|
10940
|
+
const uncommitted = values.uncommitted === true;
|
|
10941
|
+
for (const path of restoreDecisionProse(ctx, project, base, ids, { uncommitted })) {
|
|
10942
|
+
io.stdout(`Restored ${path}
|
|
10192
10943
|
`);
|
|
10944
|
+
}
|
|
10193
10945
|
return EXIT_OK;
|
|
10194
10946
|
}
|
|
10195
10947
|
};
|
|
@@ -10238,7 +10990,7 @@ Agents: ${HARNESS_NAMES.join(", ")}.
|
|
|
10238
10990
|
};
|
|
10239
10991
|
|
|
10240
10992
|
// src/cli/commands/update.ts
|
|
10241
|
-
import { join as
|
|
10993
|
+
import { join as join21 } from "node:path";
|
|
10242
10994
|
var updateCommand = {
|
|
10243
10995
|
name: "update",
|
|
10244
10996
|
summary: "Refresh the installed DLD skills and rule to this version",
|
|
@@ -10261,9 +11013,10 @@ ${AGENT_HELP}
|
|
|
10261
11013
|
});
|
|
10262
11014
|
const requested = parseAgents(values.agent);
|
|
10263
11015
|
const root = findProjectRoot(ctx);
|
|
10264
|
-
if (!ctx.fs.lexists(
|
|
11016
|
+
if (!ctx.fs.lexists(join21(root, CONFIG_FILE))) {
|
|
10265
11017
|
throw new DldError(`DLD is not set up here (${CONFIG_FILE} not found). Run dld init first.`);
|
|
10266
11018
|
}
|
|
11019
|
+
const project = loadProject(ctx);
|
|
10267
11020
|
const installed = installedTargets(ctx, root);
|
|
10268
11021
|
const added = targetsFor(requested);
|
|
10269
11022
|
const layouts = /* @__PURE__ */ new Set([...installed.layouts, ...added.layouts]);
|
|
@@ -10279,11 +11032,14 @@ ${AGENT_HELP}
|
|
|
10279
11032
|
rules,
|
|
10280
11033
|
version,
|
|
10281
11034
|
force: values.force === true,
|
|
10282
|
-
harnesses: requested
|
|
11035
|
+
harnesses: requested,
|
|
11036
|
+
// @decision(DL-074)
|
|
11037
|
+
ruleText: ruleTextFor(project.config.annotationPrefix)
|
|
10283
11038
|
});
|
|
10284
11039
|
const report2 = applyInstall(ctx, root, plan);
|
|
10285
11040
|
report2.warnings.push(...missingAgentsRuleWarning({ layouts, rules }));
|
|
10286
11041
|
printReport(io, report2);
|
|
11042
|
+
ensureIndexMergeAttribute(io, ctx, project);
|
|
10287
11043
|
return EXIT_OK;
|
|
10288
11044
|
}
|
|
10289
11045
|
};
|
|
@@ -10373,7 +11129,15 @@ var verifyAnnotationsCommand = {
|
|
|
10373
11129
|
if (ids.length === 0) throw new UsageError("expected at least one decision ID");
|
|
10374
11130
|
const options = scanOptionsFor(loadProject(ctx));
|
|
10375
11131
|
const prefix = options.prefix;
|
|
10376
|
-
const missing = missingAnnotations(
|
|
11132
|
+
const missing = missingAnnotations(
|
|
11133
|
+
ctx,
|
|
11134
|
+
options,
|
|
11135
|
+
ids,
|
|
11136
|
+
(skipped) => io.stderr(`${formatSkipped(skipped)}
|
|
11137
|
+
`),
|
|
11138
|
+
// @decision(DL-074)
|
|
11139
|
+
(nearMiss) => ctx.warn(formatNearMiss(nearMiss))
|
|
11140
|
+
);
|
|
10377
11141
|
if (missing.length > 0) {
|
|
10378
11142
|
io.stdout(`MISSING annotations in source code for: ${missing.join(" ")}
|
|
10379
11143
|
`);
|
|
@@ -10394,14 +11158,18 @@ var COMMANDS = [
|
|
|
10394
11158
|
updateCommand,
|
|
10395
11159
|
installRuleCommand,
|
|
10396
11160
|
sessionContextCommand,
|
|
11161
|
+
checkDecisionEditsCommand,
|
|
10397
11162
|
createConfigCommand,
|
|
10398
11163
|
createDirectoriesCommand,
|
|
10399
11164
|
createEmptyIndexCommand,
|
|
10400
11165
|
nextIdCommand,
|
|
10401
11166
|
createDecisionCommand,
|
|
10402
11167
|
updateStatusCommand,
|
|
11168
|
+
listProposedCommand,
|
|
11169
|
+
relatedDecisionsCommand,
|
|
10403
11170
|
regenerateIndexCommand,
|
|
10404
11171
|
verifyAnnotationsCommand,
|
|
11172
|
+
restoreDecisionProseCommand,
|
|
10405
11173
|
findAnnotationsCommand,
|
|
10406
11174
|
findMissingAmendsCommand,
|
|
10407
11175
|
updateAuditStateCommand,
|
|
@@ -10422,7 +11190,7 @@ function usage(commands) {
|
|
|
10422
11190
|
const list = (group) => group.map((c) => ` ${c.name.padEnd(25)} ${c.summary}`).join("\n");
|
|
10423
11191
|
const setup = commands.filter((c) => !c.internal);
|
|
10424
11192
|
const internal = commands.filter((c) => c.internal);
|
|
10425
|
-
const sections = [`Setup commands:
|
|
11193
|
+
const sections = [`Setup and CI commands:
|
|
10426
11194
|
${list(setup)}`];
|
|
10427
11195
|
if (internal.length > 0) sections.push(`${INTERNAL_HEADING}:
|
|
10428
11196
|
${list(internal)}`);
|
|
@@ -10457,8 +11225,18 @@ function run(argv, io, ctx, commands = COMMANDS) {
|
|
|
10457
11225
|
${usage(commands)}`);
|
|
10458
11226
|
return EXIT_USAGE;
|
|
10459
11227
|
}
|
|
11228
|
+
const printed = /* @__PURE__ */ new Set();
|
|
11229
|
+
const runCtx = {
|
|
11230
|
+
...ctx,
|
|
11231
|
+
warn: (message) => {
|
|
11232
|
+
if (printed.has(message)) return;
|
|
11233
|
+
printed.add(message);
|
|
11234
|
+
io.stderr(`Warning: ${message}
|
|
11235
|
+
`);
|
|
11236
|
+
}
|
|
11237
|
+
};
|
|
10460
11238
|
try {
|
|
10461
|
-
const result = command.run(rest, io,
|
|
11239
|
+
const result = command.run(rest, io, runCtx);
|
|
10462
11240
|
if (typeof result === "number") return result;
|
|
10463
11241
|
return result.catch((error) => report(error, command, io));
|
|
10464
11242
|
} catch (error) {
|
|
@@ -10514,6 +11292,7 @@ var nodeFileSystem = {
|
|
|
10514
11292
|
lexists: (path) => fsCall("read", path, () => lstatSync(path, { throwIfNoEntry: false }) !== void 0),
|
|
10515
11293
|
isRegularFile: (path) => fsCall("read", path, () => lstatSync(path, { throwIfNoEntry: false })?.isFile() ?? false),
|
|
10516
11294
|
readFile: (path) => fsCall("read", path, () => readFileSync(path, "utf8")),
|
|
11295
|
+
fileSize: (path) => fsCall("read", path, () => lstatSync(path).size),
|
|
10517
11296
|
readBytes: (path) => fsCall("read", path, () => readFileSync(path)),
|
|
10518
11297
|
readDir: (path) => fsCall(
|
|
10519
11298
|
"read",
|
|
@@ -10597,7 +11376,12 @@ function createNodeContext(cwd, env) {
|
|
|
10597
11376
|
gh: nodeGh(cwd, env),
|
|
10598
11377
|
env,
|
|
10599
11378
|
readStdin: () => fsCall("read", "standard input", () => readFileSync(0, "utf8")),
|
|
10600
|
-
now: () => /* @__PURE__ */ new Date()
|
|
11379
|
+
now: () => /* @__PURE__ */ new Date(),
|
|
11380
|
+
// @decision(DL-079)
|
|
11381
|
+
warn: (message) => {
|
|
11382
|
+
process.stderr.write(`Warning: ${message}
|
|
11383
|
+
`);
|
|
11384
|
+
}
|
|
10601
11385
|
};
|
|
10602
11386
|
}
|
|
10603
11387
|
function hasCode(error, code) {
|