mainflow 0.20.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { $ as setFindingStatus, A as addAction, C as setCriterionText, D as updateTask, E as supersedeDecision, F as setSessionArchived, J as createFinding, K as appendOps, L as setSessionStatus, M as handoffSession, O as sessionBriefing, P as readSession, Q as setFindingArchived, S as setCriterion, V as updateAction, X as planIssue, Y as createIssue, Z as reopenIssue, _ as createTask, b as setArchived, ct as TASK_FILE, et as setIssueArchived, f as acceptDecision, g as createMainline, h as createDecision, l as migrateHub, m as addRepo, nt as loadHub, p as addHub, pt as findHubRoot, q as closeIssue, st as MAINLINE_FILE, tt as HubError, ut as WriteError, v as dropChecklistItem, w as setMainlineStatus, x as setChecklistItem, y as initHub, z as startSession } from "./host-BsLqiI0_.js";
3
- import { F as parseRef, I as refExample } from "./dist-BrotcvKi.js";
2
+ import { $ as reopenIssue, A as addAction, C as setCriterionText, D as updateTask, E as supersedeDecision, F as setSessionArchived, J as appendOps, L as setSessionStatus, M as handoffSession, O as sessionBriefing, P as readSession, Q as planIssue, S as setCriterion, V as updateAction, X as createFinding, Y as closeIssue, Z as createIssue, _ as createTask, b as setArchived, et as setFindingArchived, f as acceptDecision, ft as WriteError, g as createMainline, gt as findHubRoot, h as createDecision, it as loadHub, l as migrateHub, lt as MAINLINE_FILE, m as addRepo, nt as setIssueArchived, p as addHub, rt as HubError, tt as setFindingStatus, ut as TASK_FILE, v as dropChecklistItem, w as setMainlineStatus, x as setChecklistItem, y as initHub, z as startSession } from "./host-0_zkFbzM.js";
3
+ import { F as parseRef, I as refExample } from "./dist-DCYzrl7J.js";
4
4
  import { t as listProgress } from "./progress-1BfPg2zN.js";
5
5
  import { COMMAND_SPECS, OPTIONS } from "./facts.js";
6
- import { runHook } from "./hooks.js";
7
- import { l as CLI_VERSION, r as launchUi, s as startUiInBackground } from "./launch-CBXoFsFC.js";
6
+ import { hookFallback, parsePayload, runHook } from "./hooks.js";
7
+ import { l as CLI_VERSION, r as launchUi, s as startUiInBackground } from "./launch-B8-NVG2g.js";
8
8
  import { readFileSync, realpathSync } from "node:fs";
9
9
  import { join, relative } from "node:path";
10
10
  import { fileURLToPath } from "node:url";
@@ -85,16 +85,24 @@ const RUNS = {
85
85
  }).status})`);
86
86
  },
87
87
  hook: ({ args }) => {
88
- let payload = {};
88
+ let input = "";
89
89
  try {
90
- payload = JSON.parse(readFileSync(0, "utf8") || "{}");
91
- } catch {
92
- console.error("mainflow hook: stdin is not JSON; nothing to do");
90
+ input = readFileSync(0, "utf8");
91
+ } catch {}
92
+ const payload = parsePayload(input);
93
+ if (!payload) {
94
+ console.error("mainflow hook: stdin is not a JSON object; nothing to do");
95
+ const fallback = hookFallback(args[0], args[1], {
96
+ payload: {},
97
+ cli: CLI
98
+ }, { kind: "unreadable-input" });
99
+ if (fallback) console.log(fallback);
93
100
  return 0;
94
101
  }
95
102
  const { stdout, error } = runHook(args[0], args[1], {
96
103
  payload,
97
104
  envFile: process.env.CLAUDE_ENV_FILE,
105
+ projectDir: process.env.CLAUDE_PROJECT_DIR,
98
106
  cli: CLI,
99
107
  startUi: startUiInBackground
100
108
  });
@@ -1,4 +1,349 @@
1
1
  import { z } from "zod";
2
+ //#region ../schema/dist/grants.js
3
+ /** What a mainline can authorize ahead of time. */
4
+ const GRANT_ACTIONS = [
5
+ "commit",
6
+ "push",
7
+ "merge-pr",
8
+ "publish",
9
+ "release",
10
+ "install-prompts",
11
+ "sync-hubs"
12
+ ];
13
+ /** How each action's target is written, for error messages and the framework facts. */
14
+ const GRANT_TARGET_FORMS = {
15
+ commit: {
16
+ form: "{repo}[:{branch}]",
17
+ example: "mainflow:main"
18
+ },
19
+ push: {
20
+ form: "{repo}[:{branch}]",
21
+ example: "mainflow:main"
22
+ },
23
+ "merge-pr": {
24
+ form: "{repo}[:{branch}]",
25
+ example: "mainflow:main"
26
+ },
27
+ publish: {
28
+ form: "{package}@{semver range}",
29
+ example: "mainflow@0.21.x"
30
+ },
31
+ release: {
32
+ form: "{repo}@{tag pattern}",
33
+ example: "mainflow@v0.21.*"
34
+ },
35
+ "install-prompts": {
36
+ form: "{unit}",
37
+ example: "user"
38
+ },
39
+ "sync-hubs": {
40
+ form: "{hub} or *",
41
+ example: "carlos-space"
42
+ }
43
+ };
44
+ const KEBAB = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
45
+ /** A git branch name, `*` standing for any run of characters. */
46
+ const BRANCH = /^(?!\/)(?!.*\/\/)(?!.*\.\.)[A-Za-z0-9._/*-]+(?<!\/)$/;
47
+ /** npm package names: lowercase, optionally scoped. */
48
+ const PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._~-]*\/)?[a-z0-9][a-z0-9._~-]*$/;
49
+ /** A tag pattern: no whitespace, `*` and `?` as in a shell glob. */
50
+ const TAG_PATTERN = /^[^\s~^:\\[]+$/;
51
+ const fail = (action, why) => ({
52
+ ok: false,
53
+ message: `${why}; a ${action} target is written ${GRANT_TARGET_FORMS[action].form}, e.g. ${GRANT_TARGET_FORMS[action].example}`
54
+ });
55
+ /** Split a target by its action's form, or say what is wrong with it. */
56
+ function parseGrantTarget(action, target) {
57
+ switch (action) {
58
+ case "commit":
59
+ case "push":
60
+ case "merge-pr": {
61
+ const colon = target.indexOf(":");
62
+ const repo = colon < 0 ? target : target.slice(0, colon);
63
+ const branch = colon < 0 ? void 0 : target.slice(colon + 1);
64
+ if (!repo) return fail(action, "the repo name is missing");
65
+ if (branch !== void 0 && !BRANCH.test(branch)) return fail(action, `"${branch}" is not a branch name`);
66
+ return {
67
+ ok: true,
68
+ target: {
69
+ action,
70
+ repo,
71
+ ...branch !== void 0 ? { branch } : {}
72
+ }
73
+ };
74
+ }
75
+ case "publish": {
76
+ const at = target.lastIndexOf("@");
77
+ if (at <= 0) return fail(action, "the version range is missing");
78
+ const name = target.slice(0, at);
79
+ const range = target.slice(at + 1);
80
+ if (!range.trim()) return fail(action, "the version range is missing");
81
+ if (!PACKAGE_NAME.test(name)) return fail(action, `"${name}" is not an npm package name`);
82
+ if (!parseRange(range)) return fail(action, `"${range}" is not a semver range this CLI reads (exact, x-range, ^, ~, comparators, ||)`);
83
+ return {
84
+ ok: true,
85
+ target: {
86
+ action,
87
+ name,
88
+ range
89
+ }
90
+ };
91
+ }
92
+ case "release": {
93
+ const at = target.indexOf("@");
94
+ if (at <= 0) return fail(action, "the tag pattern is missing");
95
+ const repo = target.slice(0, at);
96
+ const tag = target.slice(at + 1);
97
+ if (!TAG_PATTERN.test(tag)) return fail(action, `"${tag}" is not a tag pattern`);
98
+ return {
99
+ ok: true,
100
+ target: {
101
+ action,
102
+ repo,
103
+ tag
104
+ }
105
+ };
106
+ }
107
+ case "install-prompts": return KEBAB.test(target) ? {
108
+ ok: true,
109
+ target: {
110
+ action,
111
+ unit: target
112
+ }
113
+ } : fail(action, `"${target}" is not a unit name`);
114
+ case "sync-hubs": return target === "*" || KEBAB.test(target) ? {
115
+ ok: true,
116
+ target: {
117
+ action,
118
+ hub: target
119
+ }
120
+ } : fail(action, `"${target}" is not a hub name`);
121
+ }
122
+ }
123
+ /** The repository a target names, for the actions that name one. */
124
+ const grantRepo = (target) => "repo" in target ? target.repo : void 0;
125
+ /** Whether a grant's target covers the request. Only the actions a hook can resolve have requests. */
126
+ function grantCovers(target, request) {
127
+ if (target.action !== request.action) return false;
128
+ if (target.action === "publish" && request.action === "publish") return target.name === request.name && satisfies(request.version, target.range);
129
+ if (target.action === "release" && request.action === "release") return request.repos.includes(target.repo) && globMatch(target.tag, request.tag);
130
+ return false;
131
+ }
132
+ /**
133
+ * The one way each hook-enforced action may be written for a grant to allow it; the hook asks for any other
134
+ * writing, since what a free-form shell call publishes cannot be predicted reliably.
135
+ */
136
+ const GRANT_FORMS = {
137
+ publish: "在套件目錄單獨執行 `pnpm publish --ignore-scripts` 或 `npm publish --ignore-scripts`(可加 --access public|restricted、--tag <名稱>、--dry-run、--provenance,pnpm 另可加 --no-git-checks)",
138
+ release: "單獨執行 `gh release create <tag> -R <owner/repo>`(可加 --title、--notes、--generate-notes、--draft、--prerelease、--verify-tag)"
139
+ };
140
+ /** The fixed form a grant allows, filled in with its target; undefined for actions the hook does not allow. */
141
+ function grantForm(target) {
142
+ if (target.action === "publish") return `在 ${target.name} 的套件目錄單獨執行 \`pnpm publish --ignore-scripts\` 或 \`npm publish --ignore-scripts\``;
143
+ if (target.action === "release") return `單獨執行 \`gh release create <符合 ${target.tag} 的 tag> -R <${target.repo} 的 GitHub owner/repo>\``;
144
+ }
145
+ /** How much of a request a reason or an ops entry names. */
146
+ const DESCRIBED = 200;
147
+ /** One line naming a request, for reasons and the ops log. */
148
+ function describeRequest(request) {
149
+ const text = request.action === "publish" ? `publish ${request.name}@${request.version}` : `release ${request.repos.join("/") || "(unknown repo)"}@${request.tag}`;
150
+ return text.length > DESCRIBED ? `${text.slice(0, DESCRIBED)}…` : text;
151
+ }
152
+ /** Whether `text` matches a pattern where `*` is any run of characters and `?` one character, neither crossing `/`. */
153
+ function globMatch(pattern, text) {
154
+ const source = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]");
155
+ return new RegExp(`^${source}$`).test(text);
156
+ }
157
+ const VERSION = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
158
+ const PARTIAL = /^v?(\d+|[xX*])(?:\.(\d+|[xX*]))?(?:\.(\d+|[xX*]))?(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
159
+ const preParts = (pre) => pre ? pre.split(".").map((p) => /^\d+$/.test(p) ? Number(p) : p) : [];
160
+ function parseVersion(text) {
161
+ const m = VERSION.exec(text.trim());
162
+ return m ? {
163
+ major: Number(m[1]),
164
+ minor: Number(m[2]),
165
+ patch: Number(m[3]),
166
+ pre: preParts(m[4])
167
+ } : null;
168
+ }
169
+ function compareVersions(a, b) {
170
+ for (const key of [
171
+ "major",
172
+ "minor",
173
+ "patch"
174
+ ]) if (a[key] !== b[key]) return a[key] - b[key];
175
+ if (a.pre.length === 0 || b.pre.length === 0) return (a.pre.length === 0 ? 1 : 0) - (b.pre.length === 0 ? 1 : 0);
176
+ for (let i = 0; i < Math.max(a.pre.length, b.pre.length); i++) {
177
+ const x = a.pre[i];
178
+ const y = b.pre[i];
179
+ if (x === void 0) return -1;
180
+ if (y === void 0) return 1;
181
+ if (x === y) continue;
182
+ if (typeof x === "number" && typeof y === "number") return x - y;
183
+ if (typeof x === "number") return -1;
184
+ if (typeof y === "number") return 1;
185
+ return x < y ? -1 : 1;
186
+ }
187
+ return 0;
188
+ }
189
+ const v = (major, minor, patch, pre = []) => ({
190
+ major,
191
+ minor,
192
+ patch,
193
+ pre
194
+ });
195
+ /** The lowest version above every release of the given line, e.g. 2.0.0-0 above 1.x. */
196
+ const floor = (major, minor, patch) => v(major, minor, patch, [0]);
197
+ function parsePartial(text) {
198
+ if (text === "" || text === "*" || text === "x" || text === "X") return { pre: [] };
199
+ const m = PARTIAL.exec(text);
200
+ if (!m) return null;
201
+ const num = (s) => s === void 0 || /^[xX*]$/.test(s) ? void 0 : Number(s);
202
+ const major = num(m[1]);
203
+ const minor = major === void 0 ? void 0 : num(m[2]);
204
+ const patch = minor === void 0 ? void 0 : num(m[3]);
205
+ if (m[4] && patch === void 0) return null;
206
+ return {
207
+ major,
208
+ minor,
209
+ patch,
210
+ pre: preParts(m[4])
211
+ };
212
+ }
213
+ /** The comparators one range item stands for, or null when it cannot be read. */
214
+ function comparators(item) {
215
+ const m = /^(\^|~>?|<=|>=|<|>|=)?\s*(.*)$/.exec(item);
216
+ const op = m[1] ?? "";
217
+ const p = parsePartial(m[2]);
218
+ if (!p) return null;
219
+ const { major, minor, patch, pre } = p;
220
+ const low = v(major ?? 0, minor ?? 0, patch ?? 0, pre);
221
+ switch (op) {
222
+ case "":
223
+ case "=":
224
+ if (major === void 0) return [];
225
+ if (minor === void 0) return [{
226
+ op: ">=",
227
+ version: low
228
+ }, {
229
+ op: "<",
230
+ version: floor(major + 1, 0, 0)
231
+ }];
232
+ if (patch === void 0) return [{
233
+ op: ">=",
234
+ version: low
235
+ }, {
236
+ op: "<",
237
+ version: floor(major, minor + 1, 0)
238
+ }];
239
+ return [{
240
+ op: "=",
241
+ version: low
242
+ }];
243
+ case "^": {
244
+ if (major === void 0) return [];
245
+ const high = major > 0 || minor === void 0 ? floor(major + 1, 0, 0) : minor > 0 || patch === void 0 ? floor(0, minor + 1, 0) : floor(0, 0, patch + 1);
246
+ return [{
247
+ op: ">=",
248
+ version: low
249
+ }, {
250
+ op: "<",
251
+ version: high
252
+ }];
253
+ }
254
+ case "~":
255
+ case "~>": {
256
+ if (major === void 0) return [];
257
+ const high = minor === void 0 ? floor(major + 1, 0, 0) : floor(major, minor + 1, 0);
258
+ return [{
259
+ op: ">=",
260
+ version: low
261
+ }, {
262
+ op: "<",
263
+ version: high
264
+ }];
265
+ }
266
+ case ">=": return major === void 0 ? [] : [{
267
+ op: ">=",
268
+ version: low
269
+ }];
270
+ case "<": return major === void 0 ? [{
271
+ op: "<",
272
+ version: floor(0, 0, 0)
273
+ }] : [{
274
+ op: "<",
275
+ version: patch === void 0 ? floor(major, minor ?? 0, 0) : low
276
+ }];
277
+ case ">":
278
+ if (major === void 0) return [{
279
+ op: "<",
280
+ version: floor(0, 0, 0)
281
+ }];
282
+ if (minor === void 0) return [{
283
+ op: ">=",
284
+ version: v(major + 1, 0, 0)
285
+ }];
286
+ if (patch === void 0) return [{
287
+ op: ">=",
288
+ version: v(major, minor + 1, 0)
289
+ }];
290
+ return [{
291
+ op: ">",
292
+ version: low
293
+ }];
294
+ case "<=":
295
+ if (major === void 0) return [];
296
+ if (minor === void 0) return [{
297
+ op: "<",
298
+ version: floor(major + 1, 0, 0)
299
+ }];
300
+ if (patch === void 0) return [{
301
+ op: "<",
302
+ version: floor(major, minor + 1, 0)
303
+ }];
304
+ return [{
305
+ op: "<=",
306
+ version: low
307
+ }];
308
+ }
309
+ return null;
310
+ }
311
+ /** A range as alternatives of comparator sets, or null when any part of it cannot be read. */
312
+ function parseRange(range) {
313
+ const alternatives = range.trim().split(/\s*\|\|\s*/);
314
+ const sets = [];
315
+ for (const alt of alternatives) {
316
+ if (alt.trim() === "") return null;
317
+ if (/\s-\s/.test(alt)) return null;
318
+ const items = alt.trim().replace(/(\^|~>?|<=|>=|<|>|=)\s+/g, "$1").split(/\s+/).filter(Boolean);
319
+ const set = [];
320
+ for (const item of items) {
321
+ const parsed = comparators(item);
322
+ if (!parsed) return null;
323
+ set.push(...parsed);
324
+ }
325
+ sets.push(set);
326
+ }
327
+ return sets;
328
+ }
329
+ const holds = (c, version) => {
330
+ const d = compareVersions(version, c.version);
331
+ return c.op === "=" ? d === 0 : c.op === "<" ? d < 0 : c.op === "<=" ? d <= 0 : c.op === ">" ? d > 0 : d >= 0;
332
+ };
333
+ /** The upper bounds this file adds for x-ranges, caret and tilde; they name no prerelease of their own. */
334
+ const isFloor = (c) => c.op === "<" && c.version.pre.length === 1 && c.version.pre[0] === 0;
335
+ /** Whether `version` satisfies `range`; false when either cannot be read. */
336
+ function satisfies(version, range) {
337
+ const parsed = parseVersion(version);
338
+ const sets = parseRange(range);
339
+ if (!parsed || !sets) return false;
340
+ return sets.some((set) => {
341
+ if (!set.every((c) => holds(c, parsed))) return false;
342
+ if (parsed.pre.length === 0) return true;
343
+ return set.some((c) => c.version.pre.length > 0 && !isFloor(c) && c.version.major === parsed.major && c.version.minor === parsed.minor && c.version.patch === parsed.patch);
344
+ });
345
+ }
346
+ //#endregion
2
347
  //#region ../schema/dist/index.js
3
348
  const FORMAT_VERSION = 2;
4
349
  const ID_PATTERNS = {
@@ -9,7 +354,8 @@ const ID_PATTERNS = {
9
354
  decision: /^d\d{3}$/,
10
355
  action: /^a\d{3}$/,
11
356
  issue: /^i\d{3}$/,
12
- finding: /^f\d{3}$/
357
+ finding: /^f\d{3}$/,
358
+ grant: /^g\d{3}$/
13
359
  };
14
360
  const CHECKLIST_REF = /^m\d{3}\/t\d{3}#c\d{3}$/;
15
361
  const SLUG = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
@@ -357,6 +703,23 @@ const costSchema = z.object({
357
703
  actual: z.number().nonnegative().optional(),
358
704
  note: text.optional()
359
705
  });
706
+ /**
707
+ * One thing the mainline authorizes ahead of time: an action plus the target it covers. It applies only while
708
+ * a session's action in progress serves this mainline and the mainline is still open.
709
+ */
710
+ const grantSchema = z.object({
711
+ id: id("grant"),
712
+ action: z.enum(GRANT_ACTIONS),
713
+ target: text,
714
+ note: text.optional()
715
+ }).superRefine((g, ctx) => {
716
+ const parsed = parseGrantTarget(g.action, g.target);
717
+ if (!parsed.ok) ctx.addIssue({
718
+ code: "custom",
719
+ path: ["target"],
720
+ message: parsed.message
721
+ });
722
+ });
360
723
  const mainlineSchema = z.object({
361
724
  id: id("mainline"),
362
725
  title: text,
@@ -371,7 +734,8 @@ const mainlineSchema = z.object({
371
734
  amount: z.number().positive(),
372
735
  unit: text
373
736
  }).optional(),
374
- costs: z.array(costSchema).optional()
737
+ costs: z.array(costSchema).optional(),
738
+ grants: z.array(grantSchema).optional()
375
739
  });
376
740
  const decisionSchema = z.object({
377
741
  id: id("decision"),
@@ -537,4 +901,4 @@ function parseOpsLine(line) {
537
901
  return parsed.success ? parsed.data : null;
538
902
  }
539
903
  //#endregion
540
- export { formatOpsEntry as A, waitingForRef as B, SURVEY_SECTION as C, configSchema as D, WORK_STATUS as E, parseRef as F, refExample as I, refMainline as L, mainlineSchema as M, opsEntrySchema as N, decisionSchema as O, parseOpsLine as P, sessionSchema as R, SURVEY_LINES as S, TASK_SECTIONS as T, PRIORITIES as _, DECISION_STATUS as a, SESSION_STATUS as b, FINDING_STATUS as c, ISSUE_SECTIONS as d, ISSUE_STATUS as f, MERGE_STATES as g, MAINLINE_STATUS as h, DECISION_SECTIONS as i, issueSchema as j, findingSchema as k, FORMAT_VERSION as l, MAINLINE_SECTIONS as m, CLOSED_STATUS as n, DEFAULT_CATEGORIES as o, LINK_RELS as p, DECIDERS as r, FINDING_SECTIONS as s, CHECKLIST_REF as t, ID_PATTERNS as u, REF_EXAMPLES as v, TASK_REQUIRED_SECTIONS as w, SLUG as x, RESERVED_HUB_NAMES as y, taskSchema as z };
904
+ export { formatOpsEntry as A, waitingForRef as B, SURVEY_SECTION as C, configSchema as D, WORK_STATUS as E, parseRef as F, grantCovers as G, GRANT_FORMS as H, refExample as I, parseGrantTarget as J, grantForm as K, refMainline as L, mainlineSchema as M, opsEntrySchema as N, decisionSchema as O, parseOpsLine as P, sessionSchema as R, SURVEY_LINES as S, TASK_SECTIONS as T, GRANT_TARGET_FORMS as U, GRANT_ACTIONS as V, describeRequest as W, PRIORITIES as _, DECISION_STATUS as a, SESSION_STATUS as b, FINDING_STATUS as c, ISSUE_SECTIONS as d, ISSUE_STATUS as f, MERGE_STATES as g, MAINLINE_STATUS as h, DECISION_SECTIONS as i, issueSchema as j, findingSchema as k, FORMAT_VERSION as l, MAINLINE_SECTIONS as m, CLOSED_STATUS as n, DEFAULT_CATEGORIES as o, LINK_RELS as p, grantRepo as q, DECIDERS as r, FINDING_SECTIONS as s, CHECKLIST_REF as t, ID_PATTERNS as u, REF_EXAMPLES as v, TASK_REQUIRED_SECTIONS as w, SLUG as x, RESERVED_HUB_NAMES as y, taskSchema as z };
package/dist/facts.d.ts CHANGED
@@ -1,28 +1,4 @@
1
- import { z } from "zod";
2
- //#region ../schema/dist/index.d.ts
3
- export declare const FORMAT_VERSION = 2;
4
- export declare const ID_PATTERNS: {
5
- readonly mainline: RegExp;
6
- readonly task: RegExp;
7
- readonly checklistItem: RegExp;
8
- readonly criterion: RegExp;
9
- readonly decision: RegExp;
10
- readonly action: RegExp;
11
- readonly issue: RegExp;
12
- readonly finding: RegExp;
13
- };
14
- export declare const CHECKLIST_REF: RegExp;
15
- export declare const SLUG: RegExp;
16
- /** One source the survey checks: the line's label, what it holds, and how it may be left unchecked. */
17
- interface SurveyLine {
18
- label: string;
19
- holds: string;
20
- /** The word that marks the source as not checked, followed by the reason, e.g. 不適用:{理由}. Absent when the source is always checked. */
21
- skip?: string;
22
- /** Text a checked line must contain, e.g. the environment a spike ran in. */
23
- requires?: string;
24
- }
25
- //#endregion
1
+ import { a as SurveyLine, i as SLUG, n as FORMAT_VERSION, r as ID_PATTERNS, t as CHECKLIST_REF } from "./index-CV30LkNZ.js";
26
2
  //#region src/facts.d.ts
27
3
  export interface CommandSpec {
28
4
  usage: string;
@@ -500,6 +476,24 @@ export declare const REFERENCES: readonly [{
500
476
  export declare const LINKS: {
501
477
  readonly rels: readonly ["depends-on", "relates", "split-from"];
502
478
  };
479
+ /**
480
+ * What a mainline's grants authorize: each action with the form of its target. A grant applies only while the
481
+ * session's action in progress serves the mainline and the mainline is active or planning. The PreToolUse hook
482
+ * allows `publish` and `release` only when the command is written in the fixed form in `forms`; the other
483
+ * actions are for the executor to read.
484
+ */
485
+ export declare const GRANTS: {
486
+ readonly actions: readonly ["commit", "push", "merge-pr", "publish", "release", "install-prompts", "sync-hubs"];
487
+ readonly targets: Record<"commit" | "install-prompts" | "merge-pr" | "publish" | "push" | "release" | "sync-hubs", {
488
+ form: string;
489
+ example: string;
490
+ }>;
491
+ readonly enforcedByHook: readonly ['publish', 'release'];
492
+ readonly forms: {
493
+ readonly publish: '在套件目錄單獨執行 `pnpm publish --ignore-scripts` 或 `npm publish --ignore-scripts`(可加 --access public|restricted、--tag <名稱>、--dry-run、--provenance,pnpm 另可加 --no-git-checks)';
494
+ readonly release: '單獨執行 `gh release create <tag> -R <owner/repo>`(可加 --title、--notes、--generate-notes、--draft、--prerelease、--verify-tag)';
495
+ };
496
+ };
503
497
  /** Where each kind of entry lives under the hub root. */
504
498
  export declare const LAYOUT: {
505
499
  readonly config: '.mainflow/config.yaml';
@@ -551,13 +545,15 @@ export interface FieldSpec {
551
545
  }
552
546
  /** Every front matter field of each document and list entry; facts.test keeps it equal to the schema. */
553
547
  export declare const FIELDS: {
554
- readonly mainline: readonly [...FieldSpec[], FieldSpec, FieldSpec, FieldSpec, FieldSpec, FieldSpec];
548
+ readonly mainline: readonly [...FieldSpec[], FieldSpec, FieldSpec, FieldSpec, FieldSpec, FieldSpec, FieldSpec];
555
549
  readonly task: readonly [...FieldSpec[], FieldSpec, FieldSpec, FieldSpec, FieldSpec, FieldSpec, FieldSpec, FieldSpec, FieldSpec, FieldSpec, FieldSpec, FieldSpec, FieldSpec];
556
550
  readonly decision: readonly [...FieldSpec[], FieldSpec, FieldSpec, FieldSpec, FieldSpec, FieldSpec];
557
551
  readonly issue: readonly [...FieldSpec[], FieldSpec, FieldSpec, FieldSpec, FieldSpec];
558
552
  readonly finding: readonly [...FieldSpec[], FieldSpec, FieldSpec, FieldSpec];
559
553
  readonly criterion: FieldSpec[];
560
554
  readonly checklistItem: readonly [...FieldSpec[], FieldSpec];
555
+ readonly grant: readonly [FieldSpec, FieldSpec, FieldSpec, FieldSpec];
561
556
  readonly link: readonly [FieldSpec, FieldSpec, FieldSpec];
562
557
  };
563
- //#endregion
558
+ //#endregion
559
+ export { CHECKLIST_REF, FORMAT_VERSION, ID_PATTERNS, SLUG };
package/dist/facts.js CHANGED
@@ -1,4 +1,4 @@
1
- import { C as SURVEY_SECTION, E as WORK_STATUS, S as SURVEY_LINES, T as TASK_SECTIONS, _ as PRIORITIES, a as DECISION_STATUS, b as SESSION_STATUS, c as FINDING_STATUS, d as ISSUE_SECTIONS, f as ISSUE_STATUS, g as MERGE_STATES, h as MAINLINE_STATUS, i as DECISION_SECTIONS, l as FORMAT_VERSION, m as MAINLINE_SECTIONS, n as CLOSED_STATUS, o as DEFAULT_CATEGORIES, p as LINK_RELS, r as DECIDERS, s as FINDING_SECTIONS, t as CHECKLIST_REF, u as ID_PATTERNS, w as TASK_REQUIRED_SECTIONS, x as SLUG } from "./dist-BrotcvKi.js";
1
+ import { C as SURVEY_SECTION, E as WORK_STATUS, H as GRANT_FORMS, S as SURVEY_LINES, T as TASK_SECTIONS, U as GRANT_TARGET_FORMS, V as GRANT_ACTIONS, _ as PRIORITIES, a as DECISION_STATUS, b as SESSION_STATUS, c as FINDING_STATUS, d as ISSUE_SECTIONS, f as ISSUE_STATUS, g as MERGE_STATES, h as MAINLINE_STATUS, i as DECISION_SECTIONS, l as FORMAT_VERSION, m as MAINLINE_SECTIONS, n as CLOSED_STATUS, o as DEFAULT_CATEGORIES, p as LINK_RELS, r as DECIDERS, s as FINDING_SECTIONS, t as CHECKLIST_REF, u as ID_PATTERNS, w as TASK_REQUIRED_SECTIONS, x as SLUG } from "./dist-DCYzrl7J.js";
2
2
  //#region src/facts.ts
3
3
  const COMMAND_SPECS = {
4
4
  init: {
@@ -408,6 +408,18 @@ const REFERENCES = [
408
408
  ];
409
409
  /** Relation types a document's links take; readers derive the reverse direction. */
410
410
  const LINKS = { rels: LINK_RELS };
411
+ /**
412
+ * What a mainline's grants authorize: each action with the form of its target. A grant applies only while the
413
+ * session's action in progress serves the mainline and the mainline is active or planning. The PreToolUse hook
414
+ * allows `publish` and `release` only when the command is written in the fixed form in `forms`; the other
415
+ * actions are for the executor to read.
416
+ */
417
+ const GRANTS = {
418
+ actions: GRANT_ACTIONS,
419
+ targets: GRANT_TARGET_FORMS,
420
+ enforcedByHook: ["publish", "release"],
421
+ forms: GRANT_FORMS
422
+ };
411
423
  /** Where each kind of entry lives under the hub root. */
412
424
  const LAYOUT = {
413
425
  config: ".mainflow/config.yaml",
@@ -500,7 +512,8 @@ const FIELDS = {
500
512
  field("archivedAt", false, "封存的日期,和狀態分開"),
501
513
  field("criteria", true, "解決條件清單,每條的欄位見 criterion"),
502
514
  field("budget", false, "預算的金額與單位"),
503
- field("costs", false, "花費清單,每筆有日期、項目、單位,可附預估、實際、備註")
515
+ field("costs", false, "花費清單,每筆有日期、項目、單位,可附預估、實際、備註"),
516
+ field("grants", false, "預先授權清單,每條的欄位見 grant;只在會話進行中的行動服務這條主脈絡、且主脈絡是 active 或 planning 時生效")
504
517
  ],
505
518
  task: [
506
519
  ...shared(...EVERY),
@@ -540,6 +553,12 @@ const FIELDS = {
540
553
  ],
541
554
  criterion: LIST_ITEM,
542
555
  checklistItem: [...LIST_ITEM, field("serves", false, "這個項目推進的解決條件,r{NNN} 清單")],
556
+ grant: [
557
+ field("id", true, "清單內不重複的編號 g{NNN}"),
558
+ field("action", true, "授權的動作:commit、push、merge-pr、publish、release、install-prompts、sync-hubs"),
559
+ field("target", true, "授權的目標,格式依動作而定,見 GRANTS.targets;倉庫名稱需在 config.yaml 登記"),
560
+ field("note", false, "一句話說明")
561
+ ],
543
562
  link: [
544
563
  field("rel", true, "關係類型:depends-on、relates、split-from,反向由讀者推出"),
545
564
  field("to", true, "對方的參照,不能在同一條主脈絡內;其他 hub 的加 {hub}: 前綴"),
@@ -547,4 +566,4 @@ const FIELDS = {
547
566
  ]
548
567
  };
549
568
  //#endregion
550
- export { CHECKLIST_REF, CLASSIFICATION, CLOSED_STATUSES, COMMAND_SPECS, FIELDS, FORMAT_VERSION, ID_PATTERNS, LAYOUT, LINKS, OPTIONS, REFERENCES, SECTIONS, SLUG, STATUSES, SURVEY };
569
+ export { CHECKLIST_REF, CLASSIFICATION, CLOSED_STATUSES, COMMAND_SPECS, FIELDS, FORMAT_VERSION, GRANTS, ID_PATTERNS, LAYOUT, LINKS, OPTIONS, REFERENCES, SECTIONS, SLUG, STATUSES, SURVEY };
@@ -0,0 +1,73 @@
1
+ import "./index-CV30LkNZ.js";
2
+ //#region ../schema/dist/grants.d.ts
3
+ /** An operation about to run, resolved to what a grant is written against. */
4
+ type GrantRequest = {
5
+ action: 'publish';
6
+ name: string;
7
+ version: string;
8
+ } |
9
+ /** `repos` holds every config.yaml name whose checkout points at the repository, usually one */
10
+ {
11
+ action: 'release';
12
+ repos: string[];
13
+ tag: string;
14
+ };
15
+ //#endregion
16
+ //#region src/grant-commands.d.ts
17
+ export declare const quote: (text: string) => string;
18
+ /** A call in one of the fixed forms, not yet checked against the package or the repository. */
19
+ export type Fixed = {
20
+ action: 'publish';
21
+ tool: 'npm' | 'pnpm';
22
+ text: string;
23
+ } | {
24
+ action: 'release';
25
+ tag: string;
26
+ repo: string;
27
+ text: string;
28
+ };
29
+ /** none: nothing grantable, stay silent. refused: not a fixed form, so ask. fixed: one of the two forms. */
30
+ export type Scan = {
31
+ kind: 'none';
32
+ } | {
33
+ kind: 'refused';
34
+ reason: string;
35
+ } | {
36
+ kind: 'fixed';
37
+ call: Fixed;
38
+ };
39
+ /** A git tag a release may create: no path separators, no `..`. */
40
+ export declare const TAG: RegExp;
41
+ /** Whether a Bash call is one this hook decides and, if so, whether it is in a fixed form. */
42
+ export declare function scanCommand(command: string): Scan;
43
+ export interface ResolveContext {
44
+ /** Directory the call runs in, already checked by scopedCwd */
45
+ cwd: string;
46
+ /** Hub root; config.yaml repo paths are relative to it */
47
+ root: string;
48
+ /** config.yaml `repos`: name to path */
49
+ repos: Record<string, string>;
50
+ }
51
+ export type Resolved = {
52
+ ok: true;
53
+ request: GrantRequest;
54
+ } | {
55
+ ok: false;
56
+ reason: string;
57
+ };
58
+ /**
59
+ * The payload's cwd after realpath, when it exists and lies in the hub or in a repository config.yaml lists; a
60
+ * listed repository counts as part of the hub even when it sits outside the hub directory, as ../mainflow does.
61
+ */
62
+ export declare function scopedCwd(cwd: unknown, root: string, repos: Record<string, string>): {
63
+ ok: true;
64
+ dir: string;
65
+ } | {
66
+ ok: false;
67
+ reason: string;
68
+ };
69
+ /** The origin remote of a checkout as github.com/owner/repo in lowercase, or null when it is not on github.com. */
70
+ export declare function githubOrigin(dir: string): string | null;
71
+ /** What a fixed-form call would publish or release; refused when the package or repository does not qualify. */
72
+ export declare function resolveFixed(call: Fixed, ctx: ResolveContext): Resolved;
73
+ //#endregion