@deftai/directive-core 0.92.0 → 0.94.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/dist/cache/fetch.d.ts +29 -0
  2. package/dist/cache/fetch.js +131 -4
  3. package/dist/cache/operations.d.ts +8 -0
  4. package/dist/cache/operations.js +67 -4
  5. package/dist/check/cached-orchestrator.js +11 -0
  6. package/dist/check/consumer-gate-integrity.d.ts +68 -0
  7. package/dist/check/consumer-gate-integrity.js +265 -0
  8. package/dist/check/index.d.ts +1 -0
  9. package/dist/check/index.js +1 -0
  10. package/dist/check/orchestrator.js +10 -0
  11. package/dist/content-contracts/skills/greptile-detector.d.ts +9 -0
  12. package/dist/content-contracts/skills/greptile-detector.js +9 -2
  13. package/dist/doctor/index.d.ts +1 -0
  14. package/dist/doctor/index.js +1 -0
  15. package/dist/doctor/main.js +69 -0
  16. package/dist/doctor/openclaw-l2-adapter.d.ts +26 -0
  17. package/dist/doctor/openclaw-l2-adapter.js +199 -0
  18. package/dist/finish-loop/pr-finish-loop.js +1 -0
  19. package/dist/hooks/dispatcher.d.ts +24 -2
  20. package/dist/hooks/dispatcher.js +122 -11
  21. package/dist/hooks/readonly.d.ts +14 -0
  22. package/dist/hooks/readonly.js +126 -0
  23. package/dist/index.d.ts +2 -0
  24. package/dist/index.js +2 -0
  25. package/dist/init-deposit/hygiene.js +22 -0
  26. package/dist/init-deposit/index.d.ts +3 -0
  27. package/dist/init-deposit/index.js +3 -0
  28. package/dist/init-deposit/init-deposit.js +10 -0
  29. package/dist/init-deposit/refresh.js +11 -0
  30. package/dist/init-deposit/scaffold.js +3 -133
  31. package/dist/init-deposit/skill-discovery-deposit.d.ts +65 -0
  32. package/dist/init-deposit/skill-discovery-deposit.js +287 -0
  33. package/dist/init-deposit/skill-discovery-hosts.d.ts +94 -0
  34. package/dist/init-deposit/skill-discovery-hosts.js +217 -0
  35. package/dist/init-deposit/slash-deposit.d.ts +46 -0
  36. package/dist/init-deposit/slash-deposit.js +174 -0
  37. package/dist/intake/issue-ingest.d.ts +20 -0
  38. package/dist/intake/issue-ingest.js +58 -0
  39. package/dist/policy/host-slash-commands.d.ts +28 -0
  40. package/dist/policy/host-slash-commands.js +103 -0
  41. package/dist/policy/index.d.ts +2 -0
  42. package/dist/policy/index.js +61 -7
  43. package/dist/policy/min-greptile-confidence.d.ts +57 -0
  44. package/dist/policy/min-greptile-confidence.js +123 -0
  45. package/dist/pr-merge-readiness/compute.d.ts +11 -1
  46. package/dist/pr-merge-readiness/compute.js +17 -4
  47. package/dist/pr-merge-readiness/evaluate.d.ts +8 -1
  48. package/dist/pr-merge-readiness/evaluate.js +7 -4
  49. package/dist/pr-merge-readiness/mergeability.d.ts +7 -1
  50. package/dist/pr-merge-readiness/mergeability.js +14 -2
  51. package/dist/pr-monitor/main.d.ts +2 -0
  52. package/dist/pr-monitor/main.js +31 -1
  53. package/dist/pr-monitor/monitor.js +5 -1
  54. package/dist/pr-monitor/readiness.js +3 -1
  55. package/dist/pr-monitor/types.d.ts +8 -0
  56. package/dist/pr-wait-mergeable/cascade.d.ts +8 -0
  57. package/dist/pr-wait-mergeable/cascade.js +28 -1
  58. package/dist/pr-wait-mergeable/main.d.ts +2 -0
  59. package/dist/pr-wait-mergeable/main.js +16 -0
  60. package/dist/pr-wait-mergeable/types.d.ts +3 -1
  61. package/dist/pr-wait-mergeable/wrappers.d.ts +6 -0
  62. package/dist/pr-wait-mergeable/wrappers.js +3 -0
  63. package/dist/pr-watch/constants.d.ts +4 -1
  64. package/dist/pr-watch/constants.js +5 -2
  65. package/dist/pr-watch/main.js +1 -0
  66. package/dist/pr-watch/probe.d.ts +1 -1
  67. package/dist/pr-watch/probe.js +4 -1
  68. package/dist/pr-watch/types.d.ts +7 -1
  69. package/dist/pr-watch/watch.js +3 -2
  70. package/dist/review-monitor/index.d.ts +1 -0
  71. package/dist/review-monitor/index.js +1 -0
  72. package/dist/review-monitor/l4-owner.d.ts +46 -0
  73. package/dist/review-monitor/l4-owner.js +262 -0
  74. package/dist/slash/emitters.d.ts +102 -0
  75. package/dist/slash/emitters.js +148 -0
  76. package/dist/slash/generator.d.ts +98 -0
  77. package/dist/slash/generator.js +145 -0
  78. package/dist/slash/index.d.ts +16 -0
  79. package/dist/slash/index.js +16 -0
  80. package/dist/slash/openclaw-adapter.d.ts +64 -0
  81. package/dist/slash/openclaw-adapter.js +198 -0
  82. package/dist/slash/openclaw-deposit.d.ts +73 -0
  83. package/dist/slash/openclaw-deposit.js +279 -0
  84. package/dist/slash/openclaw-slugs.d.ts +52 -0
  85. package/dist/slash/openclaw-slugs.js +126 -0
  86. package/dist/slash/product-set.d.ts +50 -0
  87. package/dist/slash/product-set.js +142 -0
  88. package/dist/tool-events/classify.js +1 -0
  89. package/dist/triage/welcome/writers.js +16 -7
  90. package/dist/umbrella-current-shape/index.d.ts +45 -0
  91. package/dist/umbrella-current-shape/index.js +162 -8
  92. package/dist/vbrief-reconcile/index.d.ts +2 -2
  93. package/dist/vbrief-reconcile/index.js +1 -1
  94. package/dist/vbrief-reconcile/types.d.ts +14 -0
  95. package/dist/vbrief-reconcile/umbrellas.d.ts +31 -2
  96. package/dist/vbrief-reconcile/umbrellas.js +231 -19
  97. package/dist/vbrief-validate/plan-hooks.d.ts +4 -0
  98. package/dist/vbrief-validate/plan-hooks.js +50 -0
  99. package/dist/xbrief/create.d.ts +36 -0
  100. package/dist/xbrief/create.js +285 -0
  101. package/dist/xbrief/index.d.ts +14 -0
  102. package/dist/xbrief/index.js +42 -0
  103. package/dist/xbrief/paths.d.ts +37 -0
  104. package/dist/xbrief/paths.js +123 -0
  105. package/dist/xbrief/styles.d.ts +36 -0
  106. package/dist/xbrief/styles.js +235 -0
  107. package/dist/xbrief/types.d.ts +50 -0
  108. package/dist/xbrief/types.js +17 -0
  109. package/dist/xbrief/verify.d.ts +30 -0
  110. package/dist/xbrief/verify.js +251 -0
  111. package/package.json +11 -3
@@ -0,0 +1,285 @@
1
+ /**
2
+ * xbrief:create — write a dense xBRIEF artifact at --out (#3057).
3
+ *
4
+ * Required: --format (json|md|both), --out
5
+ * Optional: --style, --title, --id, --status, --description, --force, --project-root
6
+ *
7
+ * Does NOT move lifecycle folders. Not scope:promote / activate / complete.
8
+ */
9
+ import { readFileSync } from "node:fs";
10
+ import { basename } from "node:path";
11
+ import { ContainedWriteError, ContainedWriteErrorCode, containedWrite, } from "../fs/contained-write.js";
12
+ import { validateVbriefSchema } from "../vbrief-validate/schema.js";
13
+ import { resolveXbriefOutPaths, XbriefPathError } from "./paths.js";
14
+ import { buildStyleDocument, renderMarkdown } from "./styles.js";
15
+ import { DEFAULT_XBRIEF_SIZE_CAP_BYTES, isXbriefFormat, isXbriefStyle, } from "./types.js";
16
+ export const CREATE_USAGE = "Usage: deft xbrief:create -- --format <json|md|both> --out <path> [--style <scope|playbook|mission|project>] [--title <t>] [--id <id>] [--status <s>] [--description <d>] [--from-json <path>] [--force] [--project-root <dir>]\n" +
17
+ " Write a dense xBRIEF artifact at --out. Required: --format and --out.\n" +
18
+ " create ≠ lifecycle: does not promote/activate/complete (use scope:* for lifecycle).\n";
19
+ function fail(stderr, exitCode = 2) {
20
+ return { exitCode, stdout: "", stderr };
21
+ }
22
+ function parseFlagValue(argv, i, name) {
23
+ const eq = argv[i]?.startsWith(`${name}=`) ? argv[i].slice(name.length + 1) : undefined;
24
+ if (eq !== undefined) {
25
+ if (eq.length === 0)
26
+ return { error: `argument ${name}: expected one argument\n` };
27
+ return { value: eq, next: i };
28
+ }
29
+ const next = argv[i + 1];
30
+ if (next === undefined || next.startsWith("-")) {
31
+ return { error: `argument ${name}: expected one argument\n` };
32
+ }
33
+ return { value: next, next: i + 1 };
34
+ }
35
+ /** Parse create CLI argv into options (or error string). */
36
+ export function parseCreateArgv(argv) {
37
+ let format;
38
+ let out;
39
+ let style = "scope";
40
+ let title;
41
+ let id;
42
+ let status;
43
+ let description;
44
+ let fromJson;
45
+ let force = false;
46
+ let projectRoot = process.cwd();
47
+ for (let i = 0; i < argv.length; i += 1) {
48
+ const arg = argv[i];
49
+ if (arg === undefined)
50
+ continue;
51
+ if (arg === "-h" || arg === "--help") {
52
+ return { error: CREATE_USAGE };
53
+ }
54
+ if (arg === "--force") {
55
+ force = true;
56
+ continue;
57
+ }
58
+ if (arg === "--format" || arg.startsWith("--format=")) {
59
+ const parsed = parseFlagValue(argv, i, "--format");
60
+ if ("error" in parsed)
61
+ return parsed;
62
+ format = parsed.value;
63
+ i = parsed.next;
64
+ continue;
65
+ }
66
+ if (arg === "--out" || arg.startsWith("--out=")) {
67
+ const parsed = parseFlagValue(argv, i, "--out");
68
+ if ("error" in parsed)
69
+ return parsed;
70
+ out = parsed.value;
71
+ i = parsed.next;
72
+ continue;
73
+ }
74
+ if (arg === "--style" || arg.startsWith("--style=")) {
75
+ const parsed = parseFlagValue(argv, i, "--style");
76
+ if ("error" in parsed)
77
+ return parsed;
78
+ style = parsed.value;
79
+ i = parsed.next;
80
+ continue;
81
+ }
82
+ if (arg === "--title" || arg.startsWith("--title=")) {
83
+ const parsed = parseFlagValue(argv, i, "--title");
84
+ if ("error" in parsed)
85
+ return parsed;
86
+ title = parsed.value;
87
+ i = parsed.next;
88
+ continue;
89
+ }
90
+ if (arg === "--id" || arg.startsWith("--id=")) {
91
+ const parsed = parseFlagValue(argv, i, "--id");
92
+ if ("error" in parsed)
93
+ return parsed;
94
+ id = parsed.value;
95
+ i = parsed.next;
96
+ continue;
97
+ }
98
+ if (arg === "--status" || arg.startsWith("--status=")) {
99
+ const parsed = parseFlagValue(argv, i, "--status");
100
+ if ("error" in parsed)
101
+ return parsed;
102
+ status = parsed.value;
103
+ i = parsed.next;
104
+ continue;
105
+ }
106
+ if (arg === "--description" || arg.startsWith("--description=")) {
107
+ const parsed = parseFlagValue(argv, i, "--description");
108
+ if ("error" in parsed)
109
+ return parsed;
110
+ description = parsed.value;
111
+ i = parsed.next;
112
+ continue;
113
+ }
114
+ if (arg === "--from-json" || arg.startsWith("--from-json=")) {
115
+ const parsed = parseFlagValue(argv, i, "--from-json");
116
+ if ("error" in parsed)
117
+ return parsed;
118
+ fromJson = parsed.value;
119
+ i = parsed.next;
120
+ continue;
121
+ }
122
+ if (arg === "--project-root" || arg.startsWith("--project-root=")) {
123
+ const parsed = parseFlagValue(argv, i, "--project-root");
124
+ if ("error" in parsed)
125
+ return parsed;
126
+ projectRoot = parsed.value;
127
+ i = parsed.next;
128
+ continue;
129
+ }
130
+ if (arg === "--")
131
+ continue;
132
+ return { error: `unrecognized argument: ${arg}\n${CREATE_USAGE}` };
133
+ }
134
+ if (format === undefined) {
135
+ return { error: `missing required --format (json|md|both)\n${CREATE_USAGE}` };
136
+ }
137
+ if (!isXbriefFormat(format)) {
138
+ return { error: `invalid --format ${format} (expected json|md|both)\n${CREATE_USAGE}` };
139
+ }
140
+ if (out === undefined || out.length === 0) {
141
+ return { error: `missing required --out\n${CREATE_USAGE}` };
142
+ }
143
+ if (!isXbriefStyle(style)) {
144
+ return {
145
+ error: `invalid --style ${style} (expected scope|playbook|mission|project)\n${CREATE_USAGE}`,
146
+ };
147
+ }
148
+ return {
149
+ format,
150
+ out,
151
+ style,
152
+ title,
153
+ id,
154
+ status,
155
+ description,
156
+ fromJson,
157
+ force,
158
+ projectRoot,
159
+ };
160
+ }
161
+ function loadFromJson(path) {
162
+ try {
163
+ const raw = readFileSync(path, "utf8");
164
+ const data = JSON.parse(raw);
165
+ if (typeof data !== "object" || data === null || Array.isArray(data)) {
166
+ return { ok: false, error: `--from-json must be a JSON object: ${path}\n` };
167
+ }
168
+ return { ok: true, doc: data };
169
+ }
170
+ catch (err) {
171
+ const msg = err instanceof Error ? err.message : String(err);
172
+ return { ok: false, error: `failed to read --from-json ${path}: ${msg}\n` };
173
+ }
174
+ }
175
+ function defaultTitle(style, outStem) {
176
+ const base = basename(outStem);
177
+ if (base.length > 0)
178
+ return base;
179
+ return `Untitled ${style}`;
180
+ }
181
+ /** Core create implementation (testable). */
182
+ export function createXbrief(options) {
183
+ let paths;
184
+ try {
185
+ paths = resolveXbriefOutPaths({
186
+ projectRoot: options.projectRoot,
187
+ out: options.out,
188
+ format: options.format,
189
+ cwd: options.cwd,
190
+ home: options.home,
191
+ env: options.env,
192
+ });
193
+ }
194
+ catch (err) {
195
+ if (err instanceof XbriefPathError) {
196
+ return fail(`${err.message}\n`, 1);
197
+ }
198
+ throw err;
199
+ }
200
+ let doc;
201
+ if (options.fromJson !== undefined) {
202
+ const loaded = loadFromJson(options.fromJson);
203
+ if (!loaded.ok)
204
+ return fail(loaded.error, 1);
205
+ doc = loaded.doc;
206
+ }
207
+ else {
208
+ const title = options.title ?? defaultTitle(options.style, paths.stemAbs);
209
+ doc = buildStyleDocument({
210
+ style: options.style,
211
+ title,
212
+ id: options.id,
213
+ status: options.status,
214
+ description: options.description,
215
+ now: options.now,
216
+ });
217
+ }
218
+ const schemaErrors = validateVbriefSchema(doc, paths.stemAbs);
219
+ if (schemaErrors.length > 0) {
220
+ return fail(`xbrief:create schema invalid:\n${schemaErrors.map((e) => ` - ${e}`).join("\n")}\n`, 1);
221
+ }
222
+ const sizeCap = options.sizeCapBytes ?? DEFAULT_XBRIEF_SIZE_CAP_BYTES;
223
+ const jsonText = `${JSON.stringify(doc, null, 2)}\n`;
224
+ const mdText = renderMarkdown(doc, options.style);
225
+ if (Buffer.byteLength(jsonText, "utf8") > sizeCap) {
226
+ return fail(`xbrief:create refused: json payload exceeds size cap (${sizeCap} bytes)\n`, 1);
227
+ }
228
+ if (Buffer.byteLength(mdText, "utf8") > sizeCap) {
229
+ return fail(`xbrief:create refused: md payload exceeds size cap (${sizeCap} bytes)\n`, 1);
230
+ }
231
+ const mode = options.force === true ? "replace" : "create";
232
+ const written = [];
233
+ try {
234
+ if (paths.jsonAbs !== null) {
235
+ containedWrite({
236
+ root: paths.projectRoot,
237
+ target: paths.jsonAbs,
238
+ data: jsonText,
239
+ mode,
240
+ });
241
+ written.push(paths.jsonAbs);
242
+ }
243
+ if (paths.mdAbs !== null) {
244
+ containedWrite({
245
+ root: paths.projectRoot,
246
+ target: paths.mdAbs,
247
+ data: mdText,
248
+ mode,
249
+ });
250
+ written.push(paths.mdAbs);
251
+ }
252
+ }
253
+ catch (err) {
254
+ if (err instanceof ContainedWriteError) {
255
+ if (err.code === ContainedWriteErrorCode.EXISTS) {
256
+ return fail(`xbrief:create refused: target exists (${err.target}); pass --force to replace\n`, 1);
257
+ }
258
+ if (err.code === ContainedWriteErrorCode.ESCAPE) {
259
+ return fail(`xbrief:create path refused: ${err.message}\n`, 1);
260
+ }
261
+ return fail(`xbrief:create write failed: ${err.message}\n`, 1);
262
+ }
263
+ throw err;
264
+ }
265
+ const lines = [
266
+ `OK xbrief:create format=${options.format} style=${options.style}`,
267
+ ...written.map((p) => ` wrote ${p}`),
268
+ " note: create is not a lifecycle move (scope:* handles promote/activate/complete)",
269
+ ];
270
+ return { exitCode: 0, stdout: `${lines.join("\n")}\n`, stderr: "" };
271
+ }
272
+ /** CLI entry for dispatch wrapper. */
273
+ export function runXbriefCreateCli(argv) {
274
+ const parsed = parseCreateArgv(argv);
275
+ if ("error" in parsed) {
276
+ const isHelp = parsed.error === CREATE_USAGE;
277
+ return {
278
+ exitCode: isHelp ? 0 : 2,
279
+ stdout: isHelp ? CREATE_USAGE : "",
280
+ stderr: isHelp ? "" : parsed.error,
281
+ };
282
+ }
283
+ return createXbrief(parsed);
284
+ }
285
+ //# sourceMappingURL=create.js.map
@@ -0,0 +1,14 @@
1
+ /**
2
+ * On-demand xBRIEF create/verify surface (#3057).
3
+ *
4
+ * create/verify write or check dense SoT artifacts at --out.
5
+ * Scope lifecycle remains scope:promote / activate / complete / etc.
6
+ */
7
+ export { CREATE_USAGE, type CreateOptions, createXbrief, parseCreateArgv, runXbriefCreateCli, } from "./create.js";
8
+ export { expandUserPath, resolveXbriefOutPaths, stripXbriefSuffix, XbriefPathError, } from "./paths.js";
9
+ export { type BuildDocumentInput, buildStyleDocument, MD_REQUIRED_SECTIONS, parseMarkdownMeta, renderMarkdown, } from "./styles.js";
10
+ export { DEFAULT_XBRIEF_SIZE_CAP_BYTES, isXbriefFormat, isXbriefStyle, XBRIEF_FORMATS, XBRIEF_STYLES, type XbriefCliResult, type XbriefDocument, type XbriefFormat, type XbriefPaths, type XbriefStyle, } from "./types.js";
11
+ export { parseVerifyArgv, runXbriefVerifyCli, VERIFY_USAGE, type VerifyOptions, verifyXbrief, } from "./verify.js";
12
+ /** Dispatch-compatible main for a combined xbrief-cli module (create|verify). */
13
+ export declare function main(argv?: string[]): number;
14
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,42 @@
1
+ /**
2
+ * On-demand xBRIEF create/verify surface (#3057).
3
+ *
4
+ * create/verify write or check dense SoT artifacts at --out.
5
+ * Scope lifecycle remains scope:promote / activate / complete / etc.
6
+ */
7
+ import { runXbriefCreateCli } from "./create.js";
8
+ import { runXbriefVerifyCli } from "./verify.js";
9
+ export { CREATE_USAGE, createXbrief, parseCreateArgv, runXbriefCreateCli, } from "./create.js";
10
+ export { expandUserPath, resolveXbriefOutPaths, stripXbriefSuffix, XbriefPathError, } from "./paths.js";
11
+ export { buildStyleDocument, MD_REQUIRED_SECTIONS, parseMarkdownMeta, renderMarkdown, } from "./styles.js";
12
+ export { DEFAULT_XBRIEF_SIZE_CAP_BYTES, isXbriefFormat, isXbriefStyle, XBRIEF_FORMATS, XBRIEF_STYLES, } from "./types.js";
13
+ export { parseVerifyArgv, runXbriefVerifyCli, VERIFY_USAGE, verifyXbrief, } from "./verify.js";
14
+ /** Dispatch-compatible main for a combined xbrief-cli module (create|verify). */
15
+ export function main(argv = process.argv.slice(2)) {
16
+ const [verb, ...rest] = argv;
17
+ if (verb === "create") {
18
+ const result = runXbriefCreateCli(rest);
19
+ if (result.stdout)
20
+ process.stdout.write(result.stdout);
21
+ if (result.stderr)
22
+ process.stderr.write(result.stderr);
23
+ return result.exitCode;
24
+ }
25
+ if (verb === "verify") {
26
+ const result = runXbriefVerifyCli(rest);
27
+ if (result.stdout)
28
+ process.stdout.write(result.stdout);
29
+ if (result.stderr)
30
+ process.stderr.write(result.stderr);
31
+ return result.exitCode;
32
+ }
33
+ if (verb === "-h" || verb === "--help" || verb === undefined) {
34
+ process.stdout.write("Usage: deft xbrief:create|xbrief:verify -- --format <json|md|both> --out <path> [options]\n" +
35
+ " create/verify dense xBRIEF artifacts (not scope lifecycle).\n" +
36
+ " See: deft xbrief:create --help | deft xbrief:verify --help\n");
37
+ return verb === undefined ? 2 : 0;
38
+ }
39
+ process.stderr.write(`unknown xbrief subcommand: ${verb}\n`);
40
+ return 2;
41
+ }
42
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Portable path expand + project-root containment for xbrief:create/verify (#3057).
3
+ *
4
+ * Uses real path APIs (node:path / node:os) — no slash-only logic.
5
+ * Default containment under project root fails closed on escape.
6
+ */
7
+ import type { XbriefFormat, XbriefPaths } from "./types.js";
8
+ export declare class XbriefPathError extends Error {
9
+ readonly code: string;
10
+ constructor(message: string, code?: string);
11
+ }
12
+ /**
13
+ * Expand simple user path forms portably:
14
+ * - `~` / `~/…` / `~\…` → home directory
15
+ * - `%VAR%` (Windows-class env expansion) when present in the string
16
+ *
17
+ * Relative paths are left relative (caller resolves against project root or cwd).
18
+ */
19
+ export declare function expandUserPath(input: string, options?: {
20
+ home?: string;
21
+ env?: NodeJS.ProcessEnv;
22
+ }): string;
23
+ /** Strip known xBRIEF suffixes to recover a write stem. */
24
+ export declare function stripXbriefSuffix(pathLike: string): string;
25
+ /**
26
+ * Resolve `--out` under `projectRoot` with user-path expansion and fail-closed
27
+ * containment. Returns absolute stem + format-specific artifact paths.
28
+ */
29
+ export declare function resolveXbriefOutPaths(input: {
30
+ projectRoot: string;
31
+ out: string;
32
+ format: XbriefFormat;
33
+ cwd?: string;
34
+ home?: string;
35
+ env?: NodeJS.ProcessEnv;
36
+ }): XbriefPaths;
37
+ //# sourceMappingURL=paths.d.ts.map
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Portable path expand + project-root containment for xbrief:create/verify (#3057).
3
+ *
4
+ * Uses real path APIs (node:path / node:os) — no slash-only logic.
5
+ * Default containment under project root fails closed on escape.
6
+ */
7
+ import { homedir } from "node:os";
8
+ import { isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
9
+ import { ContainedWriteError, ContainedWriteErrorCode, resolveContainedTarget, } from "../fs/contained-write.js";
10
+ export class XbriefPathError extends Error {
11
+ code;
12
+ constructor(message, code = "XBRIEF_PATH_ERROR") {
13
+ super(message);
14
+ this.name = "XbriefPathError";
15
+ this.code = code;
16
+ }
17
+ }
18
+ /**
19
+ * Expand simple user path forms portably:
20
+ * - `~` / `~/…` / `~\…` → home directory
21
+ * - `%VAR%` (Windows-class env expansion) when present in the string
22
+ *
23
+ * Relative paths are left relative (caller resolves against project root or cwd).
24
+ */
25
+ export function expandUserPath(input, options = {}) {
26
+ if (input.length === 0) {
27
+ throw new XbriefPathError("empty path", "XBRIEF_PATH_EMPTY");
28
+ }
29
+ const home = options.home ?? homedir();
30
+ const env = options.env ?? process.env;
31
+ let expanded = input.replace(/%([^%]+)%/g, (match, name) => {
32
+ const value = env[name];
33
+ return value !== undefined && value.length > 0 ? value : match;
34
+ });
35
+ if (expanded === "~") {
36
+ expanded = home;
37
+ }
38
+ else if (expanded.startsWith("~/") || expanded.startsWith("~\\")) {
39
+ expanded = join(home, expanded.slice(2));
40
+ }
41
+ // Env expansion may inject foreign separators (e.g. %USERPROFILE%\foo on Linux CI).
42
+ // Re-normalize with path.normalize after collapsing to platform-native separators.
43
+ const collapsed = expanded.replace(/\\/g, "/");
44
+ return normalize(collapsed);
45
+ }
46
+ /** Strip known xBRIEF suffixes to recover a write stem. */
47
+ export function stripXbriefSuffix(pathLike) {
48
+ const lower = pathLike.toLowerCase();
49
+ if (lower.endsWith(".xbrief.json")) {
50
+ return pathLike.slice(0, -".xbrief.json".length);
51
+ }
52
+ if (lower.endsWith(".xbrief.md")) {
53
+ return pathLike.slice(0, -".xbrief.md".length);
54
+ }
55
+ return pathLike;
56
+ }
57
+ /**
58
+ * Resolve `--out` under `projectRoot` with user-path expansion and fail-closed
59
+ * containment. Returns absolute stem + format-specific artifact paths.
60
+ */
61
+ export function resolveXbriefOutPaths(input) {
62
+ const projectRoot = resolve(input.projectRoot);
63
+ const expanded = expandUserPath(input.out, { home: input.home, env: input.env });
64
+ // Absolute (post-expand) paths must still nest under projectRoot.
65
+ // Relative paths resolve against projectRoot (not bare cwd) so containment is stable.
66
+ let candidate;
67
+ if (isAbsolute(expanded)) {
68
+ candidate = resolve(expanded);
69
+ }
70
+ else {
71
+ // When out is explicitly cwd-relative (./…), resolve against cwd then re-check root.
72
+ const cwd = input.cwd ?? process.cwd();
73
+ if (expanded === "." ||
74
+ expanded.startsWith(`.${sep}`) ||
75
+ expanded.startsWith("./") ||
76
+ expanded.startsWith(".\\")) {
77
+ candidate = resolve(cwd, expanded);
78
+ }
79
+ else {
80
+ candidate = resolve(projectRoot, expanded);
81
+ }
82
+ }
83
+ const stemRaw = stripXbriefSuffix(candidate);
84
+ let stemAbs;
85
+ try {
86
+ // resolveContainedTarget requires nested path (not the root itself).
87
+ // If stem equals root, fail with a clear message.
88
+ const rel = relative(projectRoot, stemAbsPlaceholder(projectRoot, stemRaw));
89
+ if (rel.length === 0) {
90
+ throw new XbriefPathError(`xbrief path refused: --out resolves to project root (need a file stem under the root)`, "XBRIEF_PATH_ESCAPE");
91
+ }
92
+ stemAbs = resolveContainedTarget(projectRoot, isAbsolute(stemRaw) ? stemRaw : relative(projectRoot, stemRaw));
93
+ }
94
+ catch (err) {
95
+ if (err instanceof XbriefPathError)
96
+ throw err;
97
+ if (err instanceof ContainedWriteError) {
98
+ throw new XbriefPathError(`xbrief path refused: --out escapes project root (${err.target})`, err.code === ContainedWriteErrorCode.ESCAPE ? "XBRIEF_PATH_ESCAPE" : err.code);
99
+ }
100
+ throw err;
101
+ }
102
+ const jsonAbs = input.format === "md" ? null : `${stemAbs}.xbrief.json`;
103
+ const mdAbs = input.format === "json" ? null : `${stemAbs}.xbrief.md`;
104
+ // Re-validate artifact paths under root (stem + suffix must stay nested).
105
+ for (const p of [jsonAbs, mdAbs]) {
106
+ if (p === null)
107
+ continue;
108
+ try {
109
+ resolveContainedTarget(projectRoot, p);
110
+ }
111
+ catch (err) {
112
+ if (err instanceof ContainedWriteError) {
113
+ throw new XbriefPathError(`xbrief path refused: artifact escapes project root (${p})`, "XBRIEF_PATH_ESCAPE");
114
+ }
115
+ throw err;
116
+ }
117
+ }
118
+ return { projectRoot, stemAbs, jsonAbs, mdAbs };
119
+ }
120
+ function stemAbsPlaceholder(projectRoot, stemRaw) {
121
+ return isAbsolute(stemRaw) ? resolve(stemRaw) : resolve(projectRoot, stemRaw);
122
+ }
123
+ //# sourceMappingURL=paths.js.map
@@ -0,0 +1,36 @@
1
+ /**
2
+ * P0 xBRIEF style templates and markdown section maps (#3057).
3
+ *
4
+ * One schema spine (xBRIEF v0.8); markdown is a dense render of the same plan,
5
+ * not a second dialect.
6
+ */
7
+ import type { XbriefDocument, XbriefStyle } from "./types.js";
8
+ /** Required H2 section titles in the markdown form per style. */
9
+ export declare const MD_REQUIRED_SECTIONS: Readonly<Record<XbriefStyle, readonly string[]>>;
10
+ export interface BuildDocumentInput {
11
+ style: XbriefStyle;
12
+ title: string;
13
+ id?: string;
14
+ status?: string;
15
+ description?: string;
16
+ now?: Date;
17
+ /** Optional narrative overrides merged over style defaults. */
18
+ narratives?: Record<string, string>;
19
+ items?: unknown[];
20
+ }
21
+ /** Build a minimal valid xBRIEF v0.8 document for the given P0 style. */
22
+ export declare function buildStyleDocument(input: BuildDocumentInput): XbriefDocument;
23
+ /** Render dense markdown from the JSON spine (one dialect). */
24
+ export declare function renderMarkdown(doc: XbriefDocument, style: XbriefStyle): string;
25
+ /**
26
+ * Lightweight parse of md form for verify: extract title, status, id, and H2 sections.
27
+ * Not a full reverse dialect — verify uses it for required-section + consistency checks.
28
+ */
29
+ export declare function parseMarkdownMeta(md: string): {
30
+ title: string | null;
31
+ status: string | null;
32
+ id: string | null;
33
+ style: string | null;
34
+ sections: Set<string>;
35
+ };
36
+ //# sourceMappingURL=styles.d.ts.map