ctxr-cli 0.23.0 → 0.25.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 (197) hide show
  1. package/README.md +65 -5
  2. package/dist/commands/graph-diff.d.ts +33 -0
  3. package/dist/commands/graph-diff.d.ts.map +1 -0
  4. package/dist/commands/graph-diff.js +52 -0
  5. package/dist/commands/graph-diff.js.map +1 -0
  6. package/dist/commands/land.d.ts +50 -0
  7. package/dist/commands/land.d.ts.map +1 -0
  8. package/dist/commands/land.js +414 -0
  9. package/dist/commands/land.js.map +1 -0
  10. package/dist/commands/publish-check.d.ts +6 -0
  11. package/dist/commands/publish-check.d.ts.map +1 -1
  12. package/dist/commands/publish-check.js +78 -3
  13. package/dist/commands/publish-check.js.map +1 -1
  14. package/dist/commands/publish-gather.d.ts.map +1 -1
  15. package/dist/commands/publish-gather.js +4 -1
  16. package/dist/commands/publish-gather.js.map +1 -1
  17. package/dist/commands/publish-new.d.ts +5 -0
  18. package/dist/commands/publish-new.d.ts.map +1 -1
  19. package/dist/commands/publish-new.js +30 -10
  20. package/dist/commands/publish-new.js.map +1 -1
  21. package/dist/commands/publish-stamp.d.ts +32 -0
  22. package/dist/commands/publish-stamp.d.ts.map +1 -0
  23. package/dist/commands/publish-stamp.js +78 -0
  24. package/dist/commands/publish-stamp.js.map +1 -0
  25. package/dist/commands/publish-urls.d.ts +13 -0
  26. package/dist/commands/publish-urls.d.ts.map +1 -1
  27. package/dist/commands/publish-urls.js +23 -6
  28. package/dist/commands/publish-urls.js.map +1 -1
  29. package/dist/commands/review-list.d.ts +34 -0
  30. package/dist/commands/review-list.d.ts.map +1 -0
  31. package/dist/commands/review-list.js +49 -0
  32. package/dist/commands/review-list.js.map +1 -0
  33. package/dist/commands/serve.d.ts +3 -6
  34. package/dist/commands/serve.d.ts.map +1 -1
  35. package/dist/commands/serve.js +106 -9
  36. package/dist/commands/serve.js.map +1 -1
  37. package/dist/config/schema.d.ts +40 -0
  38. package/dist/config/schema.d.ts.map +1 -1
  39. package/dist/config/schema.js +39 -0
  40. package/dist/config/schema.js.map +1 -1
  41. package/dist/core/approval/decision.d.ts +73 -0
  42. package/dist/core/approval/decision.d.ts.map +1 -0
  43. package/dist/core/approval/decision.js +166 -0
  44. package/dist/core/approval/decision.js.map +1 -0
  45. package/dist/core/approval/file-diff.d.ts +32 -0
  46. package/dist/core/approval/file-diff.d.ts.map +1 -0
  47. package/dist/core/approval/file-diff.js +83 -0
  48. package/dist/core/approval/file-diff.js.map +1 -0
  49. package/dist/core/approval/identity.d.ts +34 -0
  50. package/dist/core/approval/identity.d.ts.map +1 -0
  51. package/dist/core/approval/identity.js +105 -0
  52. package/dist/core/approval/identity.js.map +1 -0
  53. package/dist/core/approval/landing-record.d.ts +17 -0
  54. package/dist/core/approval/landing-record.d.ts.map +1 -0
  55. package/dist/core/approval/landing-record.js +30 -0
  56. package/dist/core/approval/landing-record.js.map +1 -0
  57. package/dist/core/approval/resolver.d.ts +15 -0
  58. package/dist/core/approval/resolver.d.ts.map +1 -0
  59. package/dist/core/approval/resolver.js +41 -0
  60. package/dist/core/approval/resolver.js.map +1 -0
  61. package/dist/core/approval/sessions.d.ts +36 -0
  62. package/dist/core/approval/sessions.d.ts.map +1 -0
  63. package/dist/core/approval/sessions.js +76 -0
  64. package/dist/core/approval/sessions.js.map +1 -0
  65. package/dist/core/browse/approve.d.ts +45 -0
  66. package/dist/core/browse/approve.d.ts.map +1 -0
  67. package/dist/core/browse/approve.js +239 -0
  68. package/dist/core/browse/approve.js.map +1 -0
  69. package/dist/core/browse/diagram-assets.d.ts +15 -0
  70. package/dist/core/browse/diagram-assets.d.ts.map +1 -0
  71. package/dist/core/browse/diagram-assets.js +35 -0
  72. package/dist/core/browse/diagram-assets.js.map +1 -0
  73. package/dist/core/browse/page-url.d.ts +22 -0
  74. package/dist/core/browse/page-url.d.ts.map +1 -1
  75. package/dist/core/browse/page-url.js +31 -0
  76. package/dist/core/browse/page-url.js.map +1 -1
  77. package/dist/core/browse/render.d.ts +10 -1
  78. package/dist/core/browse/render.d.ts.map +1 -1
  79. package/dist/core/browse/render.js +23 -1
  80. package/dist/core/browse/render.js.map +1 -1
  81. package/dist/core/browse/review.d.ts +21 -0
  82. package/dist/core/browse/review.d.ts.map +1 -0
  83. package/dist/core/browse/review.js +52 -0
  84. package/dist/core/browse/review.js.map +1 -0
  85. package/dist/core/browse/templates.d.ts +4 -1
  86. package/dist/core/browse/templates.d.ts.map +1 -1
  87. package/dist/core/browse/templates.js +18 -2
  88. package/dist/core/browse/templates.js.map +1 -1
  89. package/dist/core/checks/organize-checks.d.ts +17 -0
  90. package/dist/core/checks/organize-checks.d.ts.map +1 -1
  91. package/dist/core/checks/organize-checks.js +116 -0
  92. package/dist/core/checks/organize-checks.js.map +1 -1
  93. package/dist/core/errors.d.ts +67 -0
  94. package/dist/core/errors.d.ts.map +1 -1
  95. package/dist/core/errors.js +160 -0
  96. package/dist/core/errors.js.map +1 -1
  97. package/dist/core/forge/credentials.d.ts +69 -0
  98. package/dist/core/forge/credentials.d.ts.map +1 -0
  99. package/dist/core/forge/credentials.js +197 -0
  100. package/dist/core/forge/credentials.js.map +1 -0
  101. package/dist/core/forge/github.d.ts +73 -0
  102. package/dist/core/forge/github.d.ts.map +1 -0
  103. package/dist/core/forge/github.js +101 -0
  104. package/dist/core/forge/github.js.map +1 -0
  105. package/dist/core/git/exec.d.ts +25 -0
  106. package/dist/core/git/exec.d.ts.map +1 -1
  107. package/dist/core/git/exec.js +100 -2
  108. package/dist/core/git/exec.js.map +1 -1
  109. package/dist/core/graph/diff-block.d.ts +6 -0
  110. package/dist/core/graph/diff-block.d.ts.map +1 -0
  111. package/dist/core/graph/diff-block.js +63 -0
  112. package/dist/core/graph/diff-block.js.map +1 -0
  113. package/dist/core/graph/diff-document.d.ts +38 -0
  114. package/dist/core/graph/diff-document.d.ts.map +1 -0
  115. package/dist/core/graph/diff-document.js +117 -0
  116. package/dist/core/graph/diff-document.js.map +1 -0
  117. package/dist/core/graph/diff.d.ts +123 -0
  118. package/dist/core/graph/diff.d.ts.map +1 -0
  119. package/dist/core/graph/diff.js +253 -0
  120. package/dist/core/graph/diff.js.map +1 -0
  121. package/dist/core/graph/tree.d.ts +24 -0
  122. package/dist/core/graph/tree.d.ts.map +1 -0
  123. package/dist/core/graph/tree.js +45 -0
  124. package/dist/core/graph/tree.js.map +1 -0
  125. package/dist/core/notes/list.d.ts +10 -0
  126. package/dist/core/notes/list.d.ts.map +1 -1
  127. package/dist/core/notes/list.js +35 -12
  128. package/dist/core/notes/list.js.map +1 -1
  129. package/dist/core/publish/filing.d.ts +12 -12
  130. package/dist/core/publish/filing.d.ts.map +1 -1
  131. package/dist/core/publish/filing.js +20 -18
  132. package/dist/core/publish/filing.js.map +1 -1
  133. package/dist/core/publish/lifespan.d.ts +37 -0
  134. package/dist/core/publish/lifespan.d.ts.map +1 -0
  135. package/dist/core/publish/lifespan.js +59 -0
  136. package/dist/core/publish/lifespan.js.map +1 -0
  137. package/dist/core/publish/represents.d.ts +72 -0
  138. package/dist/core/publish/represents.d.ts.map +1 -0
  139. package/dist/core/publish/represents.js +83 -0
  140. package/dist/core/publish/represents.js.map +1 -0
  141. package/dist/run.d.ts.map +1 -1
  142. package/dist/run.js +60 -4
  143. package/dist/run.js.map +1 -1
  144. package/dist/version.d.ts +1 -1
  145. package/dist/version.js +1 -1
  146. package/package.json +2 -1
  147. package/templates/approve/approve-button.md +1 -0
  148. package/templates/approve/author-heading.md +1 -0
  149. package/templates/approve/author-note.md +1 -0
  150. package/templates/approve/comment-label.md +1 -0
  151. package/templates/approve/comment-required.md +1 -0
  152. package/templates/approve/commits-heading.md +1 -0
  153. package/templates/approve/decision-heading.md +1 -0
  154. package/templates/approve/decision-note.md +1 -0
  155. package/templates/approve/diff-binary.md +1 -0
  156. package/templates/approve/diff-empty.md +1 -0
  157. package/templates/approve/diff-heading.md +1 -0
  158. package/templates/approve/facts-heading.md +1 -0
  159. package/templates/approve/forge-unreachable.md +1 -0
  160. package/templates/approve/github-app-manifest.json +15 -0
  161. package/templates/approve/head-missing.md +1 -0
  162. package/templates/approve/index-empty.md +1 -0
  163. package/templates/approve/index-heading.md +1 -0
  164. package/templates/approve/index-intro.md +1 -0
  165. package/templates/approve/landed.md +1 -0
  166. package/templates/approve/moved-head.md +1 -0
  167. package/templates/approve/no-pull.md +1 -0
  168. package/templates/approve/not-open.md +1 -0
  169. package/templates/approve/pages-heading.md +1 -0
  170. package/templates/approve/pages-note.md +1 -0
  171. package/templates/approve/record-failed.md +1 -0
  172. package/templates/approve/request-changes-button.md +1 -0
  173. package/templates/approve/state-line.md +1 -0
  174. package/templates/conventions/baseline-conventions.md +6 -0
  175. package/templates/graph/knowledge-changes/archived-item.md +1 -0
  176. package/templates/graph/knowledge-changes/archived.md +1 -0
  177. package/templates/graph/knowledge-changes/bare-route.md +2 -0
  178. package/templates/graph/knowledge-changes/block.md +10 -0
  179. package/templates/graph/knowledge-changes/counts.md +1 -0
  180. package/templates/graph/knowledge-changes/dangling-item.md +1 -0
  181. package/templates/graph/knowledge-changes/dangling.md +1 -0
  182. package/templates/graph/knowledge-changes/empty.md +1 -0
  183. package/templates/graph/knowledge-changes/reason-ambiguous.md +1 -0
  184. package/templates/graph/knowledge-changes/reason-not_found.md +1 -0
  185. package/templates/graph/knowledge-changes/review.md +4 -0
  186. package/templates/graph/review/moved-head.md +1 -0
  187. package/templates/land/resolver.md +12 -0
  188. package/templates/serve/diagrams.mjs +136 -0
  189. package/templates/serve/shell.html +1 -1
  190. package/templates/serve/style.css +90 -0
  191. package/templates/skills/ctxr-land.md +37 -31
  192. package/templates/skills/ctxr-organize-audit.md +19 -0
  193. package/templates/skills/ctxr-publish.md +100 -45
  194. package/templates/skills/ctxr-second-opinion/run.mjs +28 -17
  195. package/templates/skills/ctxr-second-opinion.md +19 -14
  196. package/templates/skills/ctxr-session-lifecycle.md +9 -0
  197. package/templates/skills/ctxr-submit.md +28 -17
@@ -0,0 +1,166 @@
1
+ /**
2
+ * change-approval spec (design.md D3): a decision lives on the forge, as a
3
+ * review by the recording login whose body carries a sentence for people and
4
+ * a marker for this module. The landing path's own records (a block, a
5
+ * resolved conflict awaiting a new decision) are comments by the same login
6
+ * with a marker of their own. Nothing else — no local file — holds either.
7
+ *
8
+ * Marker values never contain spaces (commit ids, emails, ISO times, slugs),
9
+ * so a marker is `key=value` tokens separated by single spaces.
10
+ */
11
+ export const DECISION_MARKER = 'ctxr-decision';
12
+ export const LANDING_MARKER = 'ctxr-land';
13
+ const MARKER_VERSION = 'v1';
14
+ function marker(name, fields) {
15
+ const tokens = Object.entries(fields).map(([key, value]) => `${key}=${value}`);
16
+ return `<!-- ${name} ${MARKER_VERSION} ${tokens.join(' ')} -->`;
17
+ }
18
+ /**
19
+ * Only a marker that ends the body counts. The body is built with the marker
20
+ * last, and a comment above it is the decider's own text — which must never be
21
+ * able to carry a marker that reads as a different decision, head or decider.
22
+ */
23
+ function parseMarker(name, body) {
24
+ const pattern = new RegExp(`<!-- ${name} ${MARKER_VERSION} ([^>]*?) -->\\s*$`);
25
+ const found = pattern.exec(body);
26
+ if (found === null)
27
+ return null;
28
+ const fields = {};
29
+ for (const token of found[1].split(' ')) {
30
+ const eq = token.indexOf('=');
31
+ if (eq <= 0)
32
+ return null;
33
+ fields[token.slice(0, eq)] = token.slice(eq + 1);
34
+ }
35
+ return fields;
36
+ }
37
+ const SHA = /^[0-9a-f]{40}$/;
38
+ const NO_SPACE = /^\S+$/;
39
+ function neutralizeComment(comment) {
40
+ return comment.replace(/<!--/g, '<! --').replace(/-->/g, '-- >');
41
+ }
42
+ /** The review body for a decision: one sentence, the comment when there is one, then the marker. */
43
+ export function decisionBody(decision) {
44
+ const short = decision.head.slice(0, 7);
45
+ const sentence = decision.carriedFrom !== null
46
+ ? `Carried over from ${decision.carriedFrom.slice(0, 7)} to ${short} by a clean rebase (patches unchanged). Approved by ${decision.by}.`
47
+ : decision.decision === 'approve'
48
+ ? `Approved by ${decision.by} on the review page, for commit ${short}.`
49
+ : `Changes requested by ${decision.by} on the review page, for commit ${short}.`;
50
+ const fields = { decision: decision.decision, head: decision.head, by: decision.by, at: decision.at };
51
+ if (decision.carriedFrom !== null)
52
+ fields['carried-from'] = decision.carriedFrom;
53
+ const parts = [sentence];
54
+ // A comment cannot open an HTML comment of its own, so it can neither hide text on the forge nor imitate a marker.
55
+ if (decision.comment !== null)
56
+ parts.push(neutralizeComment(decision.comment.trim()));
57
+ parts.push(marker(DECISION_MARKER, fields));
58
+ return parts.join('\n\n');
59
+ }
60
+ export function parseDecision(body) {
61
+ const fields = parseMarker(DECISION_MARKER, body);
62
+ if (fields === null)
63
+ return null;
64
+ const { decision, head, by, at } = fields;
65
+ if (decision !== 'approve' && decision !== 'request-changes')
66
+ return null;
67
+ if (head === undefined || !SHA.test(head))
68
+ return null;
69
+ if (by === undefined || !NO_SPACE.test(by) || at === undefined || !NO_SPACE.test(at))
70
+ return null;
71
+ const carriedFrom = fields['carried-from'] ?? null;
72
+ if (carriedFrom !== null && !SHA.test(carriedFrom))
73
+ return null;
74
+ let comment = null;
75
+ if (decision === 'request-changes') {
76
+ const paragraphs = body.slice(0, body.lastIndexOf(`<!-- ${DECISION_MARKER}`)).split('\n\n');
77
+ comment = paragraphs.slice(1).join('\n\n').trim();
78
+ }
79
+ return { decision, head, by, at, carriedFrom, comment };
80
+ }
81
+ /** The comment body for a landing record: a sentence for people, then the marker. */
82
+ export function landingBody(mark, sentence) {
83
+ return `${sentence}\n\n${marker(LANDING_MARKER, { state: mark.state, reason: mark.reason, head: mark.head, at: mark.at })}`;
84
+ }
85
+ export function parseLandingMark(body) {
86
+ const fields = parseMarker(LANDING_MARKER, body);
87
+ if (fields === null)
88
+ return null;
89
+ const { state, reason, head, at } = fields;
90
+ if (state !== 'blocked' && state !== 'needs-reapproval')
91
+ return null;
92
+ if (reason === undefined || head === undefined || !SHA.test(head) || at === undefined)
93
+ return null;
94
+ return { state, reason, head, at };
95
+ }
96
+ function timeOf(value) {
97
+ const t = value === null ? Number.NaN : Date.parse(value);
98
+ return Number.isNaN(t) ? 0 : t;
99
+ }
100
+ /**
101
+ * change-approval spec ("Only reviews by the recording identity count"): the
102
+ * latest well-formed decision by `recordingLogin` decides, and the latest
103
+ * landing record by the same login overrides it when it was posted later and
104
+ * is about the current head. Anything by any other login is not read at all.
105
+ *
106
+ * "Latest" is the forge's own posting time (stable-sorted, so the forge's order
107
+ * breaks ties), never the marker's `at`: a carry-over keeps the time the person
108
+ * decided (design.md D5) while being the newer record.
109
+ *
110
+ * An approval of a different head is reported as `needs-reapproval` with
111
+ * `approvalNeedsCarryCheck` set; whether a clean rebase carries it over is a
112
+ * git question this module does not ask (write-lifecycle spec).
113
+ */
114
+ export function resolveReviewState(input) {
115
+ let decision = null;
116
+ let decisionTime = -1;
117
+ for (const review of input.reviews) {
118
+ if (review.author !== input.recordingLogin)
119
+ continue;
120
+ const parsed = parseDecision(review.body);
121
+ if (parsed === null)
122
+ continue;
123
+ const t = timeOf(review.submittedAt);
124
+ if (t >= decisionTime) {
125
+ decision = parsed;
126
+ decisionTime = t;
127
+ }
128
+ }
129
+ let landing = null;
130
+ let landingTime = -1;
131
+ for (const comment of input.comments) {
132
+ if (comment.author !== input.recordingLogin)
133
+ continue;
134
+ const parsed = parseLandingMark(comment.body);
135
+ if (parsed === null)
136
+ continue;
137
+ const t = timeOf(comment.createdAt);
138
+ if (t >= landingTime) {
139
+ landing = parsed;
140
+ landingTime = t;
141
+ }
142
+ }
143
+ if (landing !== null && landing.head === input.head && landingTime >= decisionTime) {
144
+ return { state: landing.state, decision, landing, approvalNeedsCarryCheck: false };
145
+ }
146
+ if (decision === null)
147
+ return { state: 'awaiting', decision, landing: null, approvalNeedsCarryCheck: false };
148
+ if (decision.decision === 'request-changes')
149
+ return { state: 'changes-requested', decision, landing: null, approvalNeedsCarryCheck: false };
150
+ if (decision.head === input.head)
151
+ return { state: 'approved', decision, landing: null, approvalNeedsCarryCheck: false };
152
+ return { state: 'needs-reapproval', decision, landing: null, approvalNeedsCarryCheck: true };
153
+ }
154
+ /** change-approval spec: an exact email, or an `@domain` entry matching that domain exactly. */
155
+ export function isDecider(email, deciders) {
156
+ const normalized = email.toLowerCase();
157
+ const at = normalized.lastIndexOf('@');
158
+ if (at <= 0)
159
+ return false;
160
+ const domain = normalized.slice(at);
161
+ return deciders.some((entry) => {
162
+ const e = entry.toLowerCase();
163
+ return e.startsWith('@') ? domain === e : normalized === e;
164
+ });
165
+ }
166
+ //# sourceMappingURL=decision.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decision.js","sourceRoot":"","sources":["../../../src/core/approval/decision.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ import type { GitRunner } from '../git/exec.js';
2
+ /**
3
+ * change-approval spec ("The approval page shows the change itself"): every
4
+ * file under the store root that differs between the base and the pull
5
+ * request's head, as removed and added lines. Read from git's object store
6
+ * with a three-dot range, so the base is the merge base and the worktree's
7
+ * uncommitted work never enters it; derived cache paths are excluded.
8
+ */
9
+ export interface DiffLine {
10
+ kind: 'add' | 'del' | 'ctx';
11
+ text: string;
12
+ }
13
+ export interface FileDiff {
14
+ path: string;
15
+ /** Set when git detected a rename. */
16
+ oldPath: string | null;
17
+ status: 'added' | 'removed' | 'modified' | 'moved';
18
+ binary: boolean;
19
+ hunks: {
20
+ header: string;
21
+ lines: DiffLine[];
22
+ }[];
23
+ }
24
+ export declare function readFileDiffs(git: GitRunner, storeRoot: string, base: string, head: string, derivedPaths: readonly string[]): Promise<FileDiff[]>;
25
+ /** Parses `git diff` output. Exported for tests. */
26
+ export declare function parseUnifiedDiff(text: string): FileDiff[];
27
+ /** Each file as a heading and a block of marked lines — readable with no script and no colour. */
28
+ export declare function renderFileDiffs(files: readonly FileDiff[], labels: {
29
+ binary: string;
30
+ noFiles: string;
31
+ }): string;
32
+ //# sourceMappingURL=file-diff.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-diff.d.ts","sourceRoot":"","sources":["../../../src/core/approval/file-diff.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAGhD;;;;;;GAMG;AACH,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC;IAC5B,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,sCAAsC;IACtC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,MAAM,EAAE,OAAO,GAAG,SAAS,GAAG,UAAU,GAAG,OAAO,CAAC;IACnD,MAAM,EAAE,OAAO,CAAC;IAChB,KAAK,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,QAAQ,EAAE,CAAA;KAAE,EAAE,CAAC;CAChD;AAED,wBAAsB,aAAa,CAAC,GAAG,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAOvJ;AAOD,oDAAoD;AACpD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,EAAE,CAoCzD;AAKD,kGAAkG;AAClG,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,QAAQ,EAAE,EAAE,MAAM,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAkB/G"}
@@ -0,0 +1,83 @@
1
+ import { escapeHtml } from '../browse/render.js';
2
+ export async function readFileDiffs(git, storeRoot, base, head, derivedPaths) {
3
+ const excludes = derivedPaths.map((p) => `:(exclude)${p.replace(/\/+$/, '')}`);
4
+ const { stdout } = await git.run(['diff', '--no-color', '--no-ext-diff', '-M', '--relative', `${base}...${head}`, '--', '.', ...excludes], { cwd: storeRoot });
5
+ return parseUnifiedDiff(stdout);
6
+ }
7
+ function unquote(raw) {
8
+ if (!raw.startsWith('"'))
9
+ return raw;
10
+ return JSON.parse(raw.replace(/\\([0-7]{3})/g, (_m, oct) => `\\u00${Number.parseInt(oct, 8).toString(16).padStart(2, '0')}`));
11
+ }
12
+ /** Parses `git diff` output. Exported for tests. */
13
+ export function parseUnifiedDiff(text) {
14
+ const files = [];
15
+ let current = null;
16
+ let hunk = null;
17
+ for (const line of text.split('\n')) {
18
+ if (line.startsWith('diff --git ')) {
19
+ const match = /^diff --git a\/(.*) b\/(.*)$/.exec(line);
20
+ current = { path: unquote(match?.[2] ?? line), oldPath: null, status: 'modified', binary: false, hunks: [] };
21
+ hunk = null;
22
+ files.push(current);
23
+ continue;
24
+ }
25
+ if (current === null)
26
+ continue;
27
+ if (hunk === null) {
28
+ if (line.startsWith('new file mode'))
29
+ current.status = 'added';
30
+ else if (line.startsWith('deleted file mode'))
31
+ current.status = 'removed';
32
+ else if (line.startsWith('rename from ')) {
33
+ current.oldPath = unquote(line.slice('rename from '.length));
34
+ current.status = 'moved';
35
+ }
36
+ else if (line.startsWith('rename to '))
37
+ current.path = unquote(line.slice('rename to '.length));
38
+ else if (line.startsWith('Binary files '))
39
+ current.binary = true;
40
+ else if (line.startsWith('@@')) {
41
+ hunk = { header: line, lines: [] };
42
+ current.hunks.push(hunk);
43
+ }
44
+ continue;
45
+ }
46
+ if (line.startsWith('@@')) {
47
+ hunk = { header: line, lines: [] };
48
+ current.hunks.push(hunk);
49
+ }
50
+ else if (line.startsWith('+'))
51
+ hunk.lines.push({ kind: 'add', text: line.slice(1) });
52
+ else if (line.startsWith('-'))
53
+ hunk.lines.push({ kind: 'del', text: line.slice(1) });
54
+ else if (line.startsWith(' '))
55
+ hunk.lines.push({ kind: 'ctx', text: line.slice(1) });
56
+ // "" and the trailing empty line carry nothing to show.
57
+ }
58
+ return files;
59
+ }
60
+ const MARK = { add: '+', del: '−', ctx: ' ' };
61
+ const LABEL = { add: 'added', del: 'removed', ctx: 'unchanged' };
62
+ /** Each file as a heading and a block of marked lines — readable with no script and no colour. */
63
+ export function renderFileDiffs(files, labels) {
64
+ if (files.length === 0)
65
+ return `<p class="ctxr-empty">${escapeHtml(labels.noFiles)}</p>`;
66
+ return files
67
+ .map((file) => {
68
+ const title = file.oldPath === null ? file.path : `${file.oldPath} → ${file.path}`;
69
+ const body = file.binary
70
+ ? `<p class="ctxr-empty">${escapeHtml(labels.binary)}</p>`
71
+ : file.hunks
72
+ .map((h) => {
73
+ const rows = h.lines
74
+ .map((l) => `<span class="ctxr-diff-${l.kind}" title="${LABEL[l.kind]}">${MARK[l.kind]} ${escapeHtml(l.text)}</span>`)
75
+ .join('\n');
76
+ return `<pre class="ctxr-diff"><span class="ctxr-diff-hunk">${escapeHtml(h.header)}</span>\n${rows}</pre>`;
77
+ })
78
+ .join('\n');
79
+ return `<section class="ctxr-diff-file"><h3><code>${escapeHtml(title)}</code> <span class="ctxr-diff-status">${escapeHtml(file.status)}</span></h3>\n${body}</section>`;
80
+ })
81
+ .join('\n');
82
+ }
83
+ //# sourceMappingURL=file-diff.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-diff.js","sourceRoot":"","sources":["../../../src/core/approval/file-diff.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ import type { ChangeApprovalConfig } from '../../config/schema.js';
2
+ /**
3
+ * change-approval spec ("Every approval route requires a verified identity
4
+ * assertion", design.md D2): the signed token the access proxy in front of
5
+ * `ctxr serve` forwards on every request, verified here for every request —
6
+ * signature against the configured key set, then issuer, audience, expiry and
7
+ * an email claim. It fails closed: a missing header, an unknown key, a key set
8
+ * that cannot be fetched, or any claim out of line is a refusal, never a pass.
9
+ *
10
+ * Only RS256 and ES256, the algorithms access proxies sign with. `none` and
11
+ * every HMAC algorithm are refused outright, so a token cannot choose to be
12
+ * checked against a secret this server does not hold.
13
+ */
14
+ export type IdentityResult = {
15
+ ok: true;
16
+ email: string;
17
+ } | {
18
+ ok: false;
19
+ reason: string;
20
+ };
21
+ export interface IdentityVerifier {
22
+ verify(headerValue: string | string[] | undefined): Promise<IdentityResult>;
23
+ }
24
+ type FetchLike = (url: string) => Promise<{
25
+ ok: boolean;
26
+ status: number;
27
+ json(): Promise<unknown>;
28
+ }>;
29
+ export declare function createIdentityVerifier(identity: ChangeApprovalConfig['identity'], opts?: {
30
+ fetch?: FetchLike;
31
+ now?: () => Date;
32
+ }): IdentityVerifier;
33
+ export {};
34
+ //# sourceMappingURL=identity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identity.d.ts","sourceRoot":"","sources":["../../../src/core/approval/identity.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAEnE;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,cAAc,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzF,MAAM,WAAW,gBAAgB;IAC/B,MAAM,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;CAC7E;AAED,KAAK,SAAS,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC,CAAA;CAAE,CAAC,CAAC;AAgBrG,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,oBAAoB,CAAC,UAAU,CAAC,EAC1C,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,SAAS,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,IAAI,CAAA;CAAO,GACjD,gBAAgB,CA+ElB"}
@@ -0,0 +1,105 @@
1
+ import { createPublicKey, verify } from 'node:crypto';
2
+ const ALGORITHMS = {
3
+ RS256: { hash: 'sha256' },
4
+ ES256: { hash: 'sha256', dsaEncoding: 'ieee-p1363' },
5
+ };
6
+ /** Seconds of clock skew allowed on `exp` and `nbf`. */
7
+ const SKEW_SECONDS = 30;
8
+ /** An unknown key id re-fetches the key set at most this often, so a stream of forged ids cannot turn into a stream of fetches. */
9
+ const REFETCH_INTERVAL_MS = 10_000;
10
+ function decodeSegment(segment) {
11
+ return JSON.parse(Buffer.from(segment, 'base64url').toString('utf8'));
12
+ }
13
+ export function createIdentityVerifier(identity, opts = {}) {
14
+ const fetchImpl = opts.fetch ?? ((url) => fetch(url));
15
+ const now = opts.now ?? (() => new Date());
16
+ let keys = new Map();
17
+ let lastFetch = 0;
18
+ async function refreshKeys() {
19
+ lastFetch = Date.now();
20
+ const response = await fetchImpl(identity.jwks_url);
21
+ if (!response.ok)
22
+ throw new Error(`key set fetch answered ${response.status}`);
23
+ const body = (await response.json());
24
+ const next = new Map();
25
+ for (const jwk of body.keys ?? []) {
26
+ if (typeof jwk.kid !== 'string')
27
+ continue;
28
+ try {
29
+ next.set(jwk.kid, createPublicKey({ key: jwk, format: 'jwk' }));
30
+ }
31
+ catch {
32
+ // A key this runtime cannot read verifies nothing.
33
+ }
34
+ }
35
+ keys = next;
36
+ }
37
+ async function keyFor(kid) {
38
+ const known = keys.get(kid);
39
+ if (known !== undefined)
40
+ return known;
41
+ if (Date.now() - lastFetch < REFETCH_INTERVAL_MS && keys.size > 0)
42
+ return null;
43
+ await refreshKeys();
44
+ return keys.get(kid) ?? null;
45
+ }
46
+ return {
47
+ async verify(headerValue) {
48
+ const token = Array.isArray(headerValue) ? headerValue[0] : headerValue;
49
+ if (token === undefined || token === '')
50
+ return { ok: false, reason: 'no identity assertion' };
51
+ const parts = token.split('.');
52
+ if (parts.length !== 3)
53
+ return { ok: false, reason: 'malformed assertion' };
54
+ const [headerPart, payloadPart, signaturePart] = parts;
55
+ let header;
56
+ let claims;
57
+ try {
58
+ header = decodeSegment(headerPart);
59
+ claims = decodeSegment(payloadPart);
60
+ }
61
+ catch {
62
+ return { ok: false, reason: 'malformed assertion' };
63
+ }
64
+ const algorithm = typeof header.alg === 'string' ? ALGORITHMS[header.alg] : undefined;
65
+ if (algorithm === undefined)
66
+ return { ok: false, reason: 'unsupported signing algorithm' };
67
+ if (typeof header.kid !== 'string')
68
+ return { ok: false, reason: 'assertion names no key' };
69
+ let key;
70
+ try {
71
+ key = await keyFor(header.kid);
72
+ }
73
+ catch (err) {
74
+ return { ok: false, reason: `key set unavailable: ${err instanceof Error ? err.message : String(err)}` };
75
+ }
76
+ if (key === null)
77
+ return { ok: false, reason: 'assertion signed by an unknown key' };
78
+ const signed = Buffer.from(`${headerPart}.${payloadPart}`, 'utf8');
79
+ const signature = Buffer.from(signaturePart, 'base64url');
80
+ let valid = false;
81
+ try {
82
+ valid = verify(algorithm.hash, signed, algorithm.dsaEncoding ? { key, dsaEncoding: algorithm.dsaEncoding } : key, signature);
83
+ }
84
+ catch {
85
+ valid = false;
86
+ }
87
+ if (!valid)
88
+ return { ok: false, reason: 'signature does not verify' };
89
+ if (claims.iss !== identity.issuer)
90
+ return { ok: false, reason: 'wrong issuer' };
91
+ const audiences = Array.isArray(claims.aud) ? claims.aud : [claims.aud];
92
+ if (!audiences.includes(identity.audience))
93
+ return { ok: false, reason: 'wrong audience' };
94
+ const nowSeconds = now().getTime() / 1000;
95
+ if (typeof claims.exp !== 'number' || claims.exp + SKEW_SECONDS < nowSeconds)
96
+ return { ok: false, reason: 'expired' };
97
+ if (typeof claims.nbf === 'number' && claims.nbf - SKEW_SECONDS > nowSeconds)
98
+ return { ok: false, reason: 'not yet valid' };
99
+ if (typeof claims.email !== 'string' || !/^[^@\s]+@[^@\s]+$/.test(claims.email))
100
+ return { ok: false, reason: 'no email claim' };
101
+ return { ok: true, email: claims.email };
102
+ },
103
+ };
104
+ }
105
+ //# sourceMappingURL=identity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identity.js","sourceRoot":"","sources":["../../../src/core/approval/identity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,EAAkB,MAAM,aAAa,CAAC;AAuBtE,MAAM,UAAU,GAA2E;IACzF,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;IACzB,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,YAAY,EAAE;CACrD,CAAC;AAEF,wDAAwD;AACxD,MAAM,YAAY,GAAG,EAAE,CAAC;AACxB,mIAAmI;AACnI,MAAM,mBAAmB,GAAG,MAAM,CAAC;AAEnC,SAAS,aAAa,CAAC,OAAe;IACpC,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;AACxE,CAAC;AAED,MAAM,UAAU,sBAAsB,CACpC,QAA0C,EAC1C,IAAI,GAA4C,EAAE;IAElD,MAAM,SAAS,GAAc,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;IACjE,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;IAC3C,IAAI,IAAI,GAAG,IAAI,GAAG,EAAqB,CAAC;IACxC,IAAI,SAAS,GAAG,CAAC,CAAC;IAElB,KAAK,UAAU,WAAW;QACxB,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,MAAM,QAAQ,GAAG,MAAM,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QACpD,IAAI,CAAC,QAAQ,CAAC,EAAE;YAAE,MAAM,IAAI,KAAK,CAAC,0BAA0B,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;QAC/E,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAyC,CAAC;QAC7E,MAAM,IAAI,GAAG,IAAI,GAAG,EAAqB,CAAC;QAC1C,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,IAAI,IAAI,EAAE,EAAE,CAAC;YAClC,IAAI,OAAO,GAAG,CAAC,GAAG,KAAK,QAAQ;gBAAE,SAAS;YAC1C,IAAI,CAAC;gBACH,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,eAAe,CAAC,EAAE,GAAG,EAAE,GAAY,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;YAC3E,CAAC;YAAC,MAAM,CAAC;gBACP,mDAAmD;YACrD,CAAC;QACH,CAAC;QACD,IAAI,GAAG,IAAI,CAAC;IACd,CAAC;IAED,KAAK,UAAU,MAAM,CAAC,GAAW;QAC/B,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC5B,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO,KAAK,CAAC;QACtC,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,GAAG,mBAAmB,IAAI,IAAI,CAAC,IAAI,GAAG,CAAC;YAAE,OAAO,IAAI,CAAC;QAC/E,MAAM,WAAW,EAAE,CAAC;QACpB,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC;IAC/B,CAAC;IAED,OAAO;QACL,KAAK,CAAC,MAAM,CAAC,WAAW;YACtB,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC;YACxE,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,EAAE;gBAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,uBAAuB,EAAE,CAAC;YAC/F,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YAC/B,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,qBAAqB,EAAE,CAAC;YAC5E,MAAM,CAAC,UAAU,EAAE,WAAW,EAAE,aAAa,CAAC,GAAG,KAAiC,CAAC;YAEnF,IAAI,MAAwC,CAAC;YAC7C,IAAI,MAA+B,CAAC;YACpC,IAAI,CAAC;gBACH,MAAM,GAAG,aAAa,CAAC,UAAU,CAAkB,CAAC;gBACpD,MAAM,GAAG,aAAa,CAAC,WAAW,CAA4B,CAAC;YACjE,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,qBAAqB,EAAE,CAAC;YACtD,CAAC;YACD,MAAM,SAAS,GAAG,OAAO,MAAM,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;YACtF,IAAI,SAAS,KAAK,SAAS;gBAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,+BAA+B,EAAE,CAAC;YAC3F,IAAI,OAAO,MAAM,CAAC,GAAG,KAAK,QAAQ;gBAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,wBAAwB,EAAE,CAAC;YAE3F,IAAI,GAAqB,CAAC;YAC1B,IAAI,CAAC;gBACH,GAAG,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACjC,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,wBAAwB,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YAC3G,CAAC;YACD,IAAI,GAAG,KAAK,IAAI;gBAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,oCAAoC,EAAE,CAAC;YAErF,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,UAAU,IAAI,WAAW,EAAE,EAAE,MAAM,CAAC,CAAC;YACnE,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,aAAa,EAAE,WAAW,CAAC,CAAC;YAC1D,IAAI,KAAK,GAAG,KAAK,CAAC;YAClB,IAAI,CAAC;gBACH,KAAK,GAAG,MAAM,CAAC,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,WAAW,EAAE,SAAS,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;YAC/H,CAAC;YAAC,MAAM,CAAC;gBACP,KAAK,GAAG,KAAK,CAAC;YAChB,CAAC;YACD,IAAI,CAAC,KAAK;gBAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,2BAA2B,EAAE,CAAC;YAEtE,IAAI,MAAM,CAAC,GAAG,KAAK,QAAQ,CAAC,MAAM;gBAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC;YACjF,MAAM,SAAS,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACxE,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,QAAQ,CAAC;gBAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC;YAC3F,MAAM,UAAU,GAAG,GAAG,EAAE,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC;YAC1C,IAAI,OAAO,MAAM,CAAC,GAAG,KAAK,QAAQ,IAAI,MAAM,CAAC,GAAG,GAAG,YAAY,GAAG,UAAU;gBAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;YACtH,IAAI,OAAO,MAAM,CAAC,GAAG,KAAK,QAAQ,IAAI,MAAM,CAAC,GAAG,GAAG,YAAY,GAAG,UAAU;gBAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,CAAC;YAC5H,IAAI,OAAO,MAAM,CAAC,KAAK,KAAK,QAAQ,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;gBAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC;YAChI,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC;QAC3C,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * write-lifecycle spec: where `ctxr land` records a landing for the approval
3
+ * page to read once the worktree is gone. A derived cache path, beside the
4
+ * graph document's, written atomically and never staged.
5
+ */
6
+ export declare const LANDINGS_RELATIVE_PATH = ".contexture/cache/landings";
7
+ export interface LandingRecord {
8
+ worktree: string;
9
+ branch: string;
10
+ pull_request: number;
11
+ merged_commit: string | null;
12
+ landed_at: string;
13
+ }
14
+ export declare function writeLandingRecord(storeRoot: string, record: LandingRecord): Promise<void>;
15
+ /** The record for `worktree`, or null — including for a name that is not a plain directory name. */
16
+ export declare function readLandingRecord(storeRoot: string, worktree: string): Promise<LandingRecord | null>;
17
+ //# sourceMappingURL=landing-record.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"landing-record.d.ts","sourceRoot":"","sources":["../../../src/core/approval/landing-record.ts"],"names":[],"mappings":"AAIA;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,+BAA+B,CAAC;AAEnE,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;CACnB;AAMD,wBAAsB,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAGhG;AAED,oGAAoG;AACpG,wBAAsB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAQ1G"}
@@ -0,0 +1,30 @@
1
+ import { mkdir, readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { writeFileAtomic } from '../fs/atomic.js';
4
+ /**
5
+ * write-lifecycle spec: where `ctxr land` records a landing for the approval
6
+ * page to read once the worktree is gone. A derived cache path, beside the
7
+ * graph document's, written atomically and never staged.
8
+ */
9
+ export const LANDINGS_RELATIVE_PATH = '.contexture/cache/landings';
10
+ function recordPath(storeRoot, worktree) {
11
+ return path.join(storeRoot, LANDINGS_RELATIVE_PATH, `${worktree}.json`);
12
+ }
13
+ export async function writeLandingRecord(storeRoot, record) {
14
+ await mkdir(path.join(storeRoot, LANDINGS_RELATIVE_PATH), { recursive: true });
15
+ await writeFileAtomic(recordPath(storeRoot, record.worktree), `${JSON.stringify(record, null, 2)}\n`);
16
+ }
17
+ /** The record for `worktree`, or null — including for a name that is not a plain directory name. */
18
+ export async function readLandingRecord(storeRoot, worktree) {
19
+ if (worktree === '' || worktree.includes('/') || worktree.includes('\\') || worktree.startsWith('.'))
20
+ return null;
21
+ try {
22
+ return JSON.parse(await readFile(recordPath(storeRoot, worktree), 'utf8'));
23
+ }
24
+ catch (err) {
25
+ if (err.code === 'ENOENT')
26
+ return null;
27
+ throw err;
28
+ }
29
+ }
30
+ //# sourceMappingURL=landing-record.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"landing-record.js","sourceRoot":"","sources":["../../../src/core/approval/landing-record.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAElD;;;;GAIG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,4BAA4B,CAAC;AAUnE,SAAS,UAAU,CAAC,SAAiB,EAAE,QAAgB;IACrD,OAAO,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,sBAAsB,EAAE,GAAG,QAAQ,OAAO,CAAC,CAAC;AAC1E,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,SAAiB,EAAE,MAAqB;IAC/E,MAAM,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,sBAAsB,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC/E,MAAM,eAAe,CAAC,UAAU,CAAC,SAAS,EAAE,MAAM,CAAC,QAAQ,CAAC,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;AACxG,CAAC;AAED,oGAAoG;AACpG,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,SAAiB,EAAE,QAAgB;IACzE,IAAI,QAAQ,KAAK,EAAE,IAAI,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,QAAQ,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAClH,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,UAAU,CAAC,SAAS,EAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,CAAkB,CAAC;IAC9F,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAK,GAA6B,CAAC,IAAI,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAC;QAClE,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC"}
@@ -0,0 +1,15 @@
1
+ export declare function resolverPrompt(pullNumber: number, base: string, defaultBranch: string): string;
2
+ /**
3
+ * design.md D6: the one place `ctxr land` spawns the store's configured
4
+ * conflict resolver — an argument vector, never a shell string, run in the
5
+ * checkout to resolve, with the prompt as its last argument. Its output goes to
6
+ * `output` (the command's stderr), so stdout keeps the one envelope.
7
+ */
8
+ export declare function runResolver(input: {
9
+ argv: readonly string[];
10
+ prompt: string;
11
+ cwd: string;
12
+ env: Readonly<Record<string, string | undefined>>;
13
+ output: NodeJS.WritableStream;
14
+ }): Promise<number>;
15
+ //# sourceMappingURL=resolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolver.d.ts","sourceRoot":"","sources":["../../../src/core/approval/resolver.ts"],"names":[],"mappings":"AAmBA,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,MAAM,CAK9F;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE;IACjC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;IAClD,MAAM,EAAE,MAAM,CAAC,cAAc,CAAC;CAC/B,GAAG,OAAO,CAAC,MAAM,CAAC,CASlB"}
@@ -0,0 +1,41 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { readFileSync } from 'node:fs';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { packagedTemplate } from '../templates.js';
5
+ /**
6
+ * write-lifecycle spec: the resolver gets "the conflict playbook the shipped
7
+ * lifecycle skill carries" — read out of that skill's own template, so the
8
+ * playbook has one source and the resolver can never follow an older copy.
9
+ */
10
+ function lifecyclePlaybook(defaultBranch) {
11
+ const skill = readFileSync(fileURLToPath(new URL('../../../templates/skills/ctxr-session-lifecycle.md', import.meta.url)), 'utf8');
12
+ const start = skill.indexOf('## Conflict playbook');
13
+ if (start === -1)
14
+ throw new Error('the lifecycle skill template carries no "## Conflict playbook" section');
15
+ const next = skill.indexOf('\n## ', start + 1);
16
+ const section = skill.slice(skill.indexOf('\n', start) + 1, next === -1 ? undefined : next).trim();
17
+ return section.replaceAll('__DEFAULT_BRANCH__', defaultBranch);
18
+ }
19
+ export function resolverPrompt(pullNumber, base, defaultBranch) {
20
+ return packagedTemplate('land', 'resolver')
21
+ .replaceAll('__PR__', () => String(pullNumber))
22
+ .replaceAll('__BASE__', () => base)
23
+ .replace('__PLAYBOOK__', () => lifecyclePlaybook(defaultBranch));
24
+ }
25
+ /**
26
+ * design.md D6: the one place `ctxr land` spawns the store's configured
27
+ * conflict resolver — an argument vector, never a shell string, run in the
28
+ * checkout to resolve, with the prompt as its last argument. Its output goes to
29
+ * `output` (the command's stderr), so stdout keeps the one envelope.
30
+ */
31
+ export function runResolver(input) {
32
+ const [command, ...args] = input.argv;
33
+ return new Promise((resolve) => {
34
+ const child = spawn(command, [...args, input.prompt], { cwd: input.cwd, env: input.env, stdio: ['ignore', 'pipe', 'pipe'] });
35
+ child.stdout.on('data', (chunk) => input.output.write(chunk));
36
+ child.stderr.on('data', (chunk) => input.output.write(chunk));
37
+ child.on('error', () => resolve(127));
38
+ child.on('close', (code) => resolve(code ?? 1));
39
+ });
40
+ }
41
+ //# sourceMappingURL=resolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolver.js","sourceRoot":"","sources":["../../../src/core/approval/resolver.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEnD;;;;GAIG;AACH,SAAS,iBAAiB,CAAC,aAAqB;IAC9C,MAAM,KAAK,GAAG,YAAY,CAAC,aAAa,CAAC,IAAI,GAAG,CAAC,qDAAqD,EAAE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;IACnI,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,sBAAsB,CAAC,CAAC;IACpD,IAAI,KAAK,KAAK,CAAC,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,wEAAwE,CAAC,CAAC;IAC5G,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC;IAC/C,MAAM,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC;IACnG,OAAO,OAAO,CAAC,UAAU,CAAC,oBAAoB,EAAE,aAAa,CAAC,CAAC;AACjE,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,UAAkB,EAAE,IAAY,EAAE,aAAqB;IACpF,OAAO,gBAAgB,CAAC,MAAM,EAAE,UAAU,CAAC;SACxC,UAAU,CAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;SAC9C,UAAU,CAAC,UAAU,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC;SAClC,OAAO,CAAC,cAAc,EAAE,GAAG,EAAE,CAAC,iBAAiB,CAAC,aAAa,CAAC,CAAC,CAAC;AACrE,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,KAM3B;IACC,MAAM,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC;IACtC,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,MAAM,KAAK,GAAG,KAAK,CAAC,OAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,EAAE,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,CAAC,GAAwB,EAAE,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;QACnJ,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;QACtE,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;QACtE,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;QACtC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC;IAClD,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,36 @@
1
+ import type { StoreConfig } from '../../config/schema.js';
2
+ import type { GitRunner } from '../git/exec.js';
3
+ import { type ForgeClient, type PullRequest } from '../forge/github.js';
4
+ import { type ReviewState } from './decision.js';
5
+ export interface SessionWorktree {
6
+ /** The worktree's directory name — the key the review and approval routes use. */
7
+ name: string;
8
+ path: string;
9
+ branch: string;
10
+ }
11
+ export interface SessionPull extends SessionWorktree {
12
+ pull: PullRequest;
13
+ review: ReviewState;
14
+ }
15
+ /** The session worktrees git knows, recognized as `ctxr session list` recognizes them. */
16
+ export declare function sessionWorktrees(git: GitRunner, repoCwd: string, config: StoreConfig): Promise<SessionWorktree[]>;
17
+ /** `origin/<default>` when the repository has that ref, else the local default branch — the review route's own rule. */
18
+ export declare function baseRef(git: GitRunner, cwd: string, defaultBranch: string): Promise<string>;
19
+ /**
20
+ * write-lifecycle spec (design.md D5): whether `currentHead` carries over the
21
+ * approval of `approvedHead` — each head's commits since it left `base`, compared
22
+ * by patch identity, commit for commit. Anything that cannot be computed (an
23
+ * approved head no longer in this repository) is "no".
24
+ */
25
+ export declare function carriesOver(git: GitRunner, cwd: string, base: string, approvedHead: string, currentHead: string): Promise<boolean>;
26
+ /** Every forge call here, turned into the usage error the spec asks for when the forge cannot be read. */
27
+ export declare function forgeRead<T>(call: () => Promise<T>): Promise<T>;
28
+ /** One pull request's decision state, with an approval of an earlier head upgraded when the current head carries it over. */
29
+ export declare function reviewStateOf(git: GitRunner, cwd: string, base: string, forge: ForgeClient, recordingLogin: string, pull: PullRequest): Promise<ReviewState>;
30
+ /**
31
+ * change-approval spec: every session worktree whose branch has an open pull
32
+ * request, with its decision state. A worktree with no open pull request is
33
+ * left out.
34
+ */
35
+ export declare function sessionPulls(git: GitRunner, repoCwd: string, config: StoreConfig, forge: ForgeClient, recordingLogin: string): Promise<SessionPull[]>;
36
+ //# sourceMappingURL=sessions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../../../src/core/approval/sessions.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAE1D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAGhD,OAAO,EAAqB,KAAK,WAAW,EAAE,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAE3F,OAAO,EAAsB,KAAK,WAAW,EAAE,MAAM,eAAe,CAAC;AAErE,MAAM,WAAW,eAAe;IAC9B,kFAAkF;IAClF,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,WAAY,SAAQ,eAAe;IAClD,IAAI,EAAE,WAAW,CAAC;IAClB,MAAM,EAAE,WAAW,CAAC;CACrB;AAED,0FAA0F;AAC1F,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC,CAQvH;AAED,wHAAwH;AACxH,wBAAsB,OAAO,CAAC,GAAG,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAGjG;AAED;;;;;GAKG;AACH,wBAAsB,WAAW,CAAC,GAAG,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAUxI;AAED,0GAA0G;AAC1G,wBAAsB,SAAS,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAOrE;AAED,6HAA6H;AAC7H,wBAAsB,aAAa,CACjC,GAAG,EAAE,SAAS,EACd,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,WAAW,EAClB,cAAc,EAAE,MAAM,EACtB,IAAI,EAAE,WAAW,GAChB,OAAO,CAAC,WAAW,CAAC,CAOtB;AAED;;;;GAIG;AACH,wBAAsB,YAAY,CAChC,GAAG,EAAE,SAAS,EACd,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,WAAW,EACnB,KAAK,EAAE,WAAW,EAClB,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC,WAAW,EAAE,CAAC,CAYxB"}