scrumrun 1.5.2 → 2.1.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 (112) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/CORE.md +231 -308
  3. package/DECISIONS.md +151 -11
  4. package/LICENSE +21 -0
  5. package/MIGRATION-1-to-2.md +120 -0
  6. package/README.md +147 -78
  7. package/SPEC.md +259 -251
  8. package/bin/claude-install.js +18 -132
  9. package/bin/scrumrun.js +1367 -174
  10. package/docs/COMMANDS.md +71 -0
  11. package/docs/ENTITY-MODEL.md +38 -0
  12. package/docs/RELEASE-SCORECARD.md +43 -0
  13. package/docs/RELEASE.md +51 -0
  14. package/docs/SCHEMA.md +89 -0
  15. package/docs/SEMANTIC-MEMORY.md +68 -0
  16. package/docs/TROUBLESHOOTING.md +78 -0
  17. package/lib/code-intel/adapter.js +20 -0
  18. package/lib/code-intel/javascript.js +199 -0
  19. package/lib/code-intel/learning.js +66 -0
  20. package/lib/code-intel/scanner.js +126 -0
  21. package/lib/commands/manifest.js +118 -0
  22. package/lib/commands/render.js +78 -0
  23. package/lib/memory/index.js +659 -0
  24. package/lib/memory/markdown.js +51 -0
  25. package/lib/memory/service.js +300 -0
  26. package/lib/runtime/budgets.js +23 -0
  27. package/lib/runtime/canonical-snapshot.js +110 -0
  28. package/lib/runtime/context.js +101 -0
  29. package/lib/runtime/orchestrator.js +303 -0
  30. package/lib/runtime/policy-engine.js +184 -0
  31. package/lib/runtime/request-engine.js +132 -0
  32. package/lib/runtime/run-ledger.js +324 -0
  33. package/lib/security/secrets.js +23 -0
  34. package/lib/v2/artifacts.js +363 -0
  35. package/lib/v2/conformance.js +214 -0
  36. package/lib/v2/migration.js +1221 -0
  37. package/lib/v2/project-store.js +44 -0
  38. package/lib/v2/run-ledger-migration.js +240 -0
  39. package/lib/v2/schema.js +148 -0
  40. package/lib/v2/transaction.js +254 -0
  41. package/package.json +16 -6
  42. package/scripts/generate-contract-docs.js +124 -0
  43. package/templates/project/.scrumrun/config.md +4 -7
  44. package/templates/project/.scrumrun/guardrails.md +31 -0
  45. package/templates/project/.scrumrun/map.md +5 -16
  46. package/templates/project/.scrumrun/memory/decisions/.gitkeep +1 -0
  47. package/templates/project/.scrumrun/memory/dossiers/.gitkeep +1 -0
  48. package/templates/project/.scrumrun/memory/insights/.gitkeep +1 -0
  49. package/templates/project/.scrumrun/memory/knowledge/.gitkeep +1 -0
  50. package/templates/project/.scrumrun/method.json +7 -0
  51. package/templates/project/.scrumrun/project.md +6 -12
  52. package/templates/project/.scrumrun/runs/.gitkeep +1 -0
  53. package/templates/project/.scrumrun/sprints/.gitkeep +1 -0
  54. package/templates/project/.scrumrun/state.md +14 -0
  55. package/templates/project/.scrumrun/tasks/.gitkeep +1 -0
  56. package/templates/project/AGENTS.md +16 -49
  57. package/templates/project-lean/AGENTS.md +18 -0
  58. package/templates/shared/skills/scrumrun/SKILL.md +211 -0
  59. package/templates/codex/prompts/sc-agent.md +0 -14
  60. package/templates/codex/prompts/sc-backlog.md +0 -13
  61. package/templates/codex/prompts/sc-challenge.md +0 -57
  62. package/templates/codex/prompts/sc-config.md +0 -18
  63. package/templates/codex/prompts/sc-context.md +0 -24
  64. package/templates/codex/prompts/sc-decisions.md +0 -8
  65. package/templates/codex/prompts/sc-feature.md +0 -16
  66. package/templates/codex/prompts/sc-fix.md +0 -21
  67. package/templates/codex/prompts/sc-goal.md +0 -14
  68. package/templates/codex/prompts/sc-golden.md +0 -14
  69. package/templates/codex/prompts/sc-help.md +0 -12
  70. package/templates/codex/prompts/sc-init.md +0 -14
  71. package/templates/codex/prompts/sc-intake.md +0 -22
  72. package/templates/codex/prompts/sc-know.md +0 -75
  73. package/templates/codex/prompts/sc-map.md +0 -13
  74. package/templates/codex/prompts/sc-review.md +0 -13
  75. package/templates/codex/prompts/sc-sprint.md +0 -28
  76. package/templates/codex/prompts/sc-study.md +0 -23
  77. package/templates/codex/prompts/sc-uninstall.md +0 -14
  78. package/templates/codex/prompts/sc-update.md +0 -8
  79. package/templates/codex/prompts/sc-vault.md +0 -27
  80. package/templates/codex/skills/scrumrun/SKILL.md +0 -412
  81. package/templates/opencode/commands/sc-agent.md +0 -14
  82. package/templates/opencode/commands/sc-backlog.md +0 -13
  83. package/templates/opencode/commands/sc-challenge.md +0 -57
  84. package/templates/opencode/commands/sc-config.md +0 -18
  85. package/templates/opencode/commands/sc-context.md +0 -24
  86. package/templates/opencode/commands/sc-decisions.md +0 -8
  87. package/templates/opencode/commands/sc-feature.md +0 -16
  88. package/templates/opencode/commands/sc-fix.md +0 -21
  89. package/templates/opencode/commands/sc-goal.md +0 -14
  90. package/templates/opencode/commands/sc-golden.md +0 -14
  91. package/templates/opencode/commands/sc-help.md +0 -12
  92. package/templates/opencode/commands/sc-init.md +0 -14
  93. package/templates/opencode/commands/sc-intake.md +0 -22
  94. package/templates/opencode/commands/sc-know.md +0 -75
  95. package/templates/opencode/commands/sc-map.md +0 -13
  96. package/templates/opencode/commands/sc-review.md +0 -13
  97. package/templates/opencode/commands/sc-sprint.md +0 -28
  98. package/templates/opencode/commands/sc-study.md +0 -23
  99. package/templates/opencode/commands/sc-uninstall.md +0 -14
  100. package/templates/opencode/commands/sc-update.md +0 -8
  101. package/templates/opencode/commands/sc-vault.md +0 -27
  102. package/templates/opencode/skills/scrumrun/SKILL.md +0 -412
  103. package/templates/project/.scrumrun/agents.md +0 -36
  104. package/templates/project/.scrumrun/backlog.md +0 -7
  105. package/templates/project/.scrumrun/context.md +0 -61
  106. package/templates/project/.scrumrun/goals/main/decisions.md +0 -9
  107. package/templates/project/.scrumrun/goals/main/history.md +0 -51
  108. package/templates/project/.scrumrun/goals/main/sprint.md +0 -54
  109. package/templates/project/.scrumrun/golden-rules.md +0 -9
  110. package/templates/project/.scrumrun/knowledge.md +0 -15
  111. package/templates/project/.scrumrun/runbook.md +0 -101
  112. package/templates/project/.scrumrun/token-policy.md +0 -43
@@ -0,0 +1,324 @@
1
+ "use strict";
2
+
3
+ const {
4
+ ARTIFACT_TRANSITIONS,
5
+ RUN_EVENT_TYPES,
6
+ RUN_EVIDENCE_KINDS,
7
+ RUN_LEDGER_VERSION
8
+ } = require("../v2/schema");
9
+ const { containsSecret } = require("../security/secrets");
10
+
11
+ const EVENT_ID = /^(RUN-\d{3,})-EVT-(\d{3,})$/;
12
+ const RFC3339 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?(?:Z|[+-]\d{2}:\d{2})$/;
13
+
14
+ function instant(value = new Date()) {
15
+ const parsed = value instanceof Date ? value : new Date(value);
16
+ if (!Number.isFinite(parsed.getTime())) throw new Error("Run event timestamp is invalid.");
17
+ return parsed.toISOString();
18
+ }
19
+
20
+ function dateInstant(value) {
21
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value || "")) throw new Error(`Legacy Run event date is invalid: ${value || "missing"}`);
22
+ return `${value}T00:00:00.000Z`;
23
+ }
24
+
25
+ function eventId(runId, sequence) {
26
+ return `${runId}-EVT-${String(sequence).padStart(3, "0")}`;
27
+ }
28
+
29
+ function normalizeEvidence(evidence = []) {
30
+ if (!Array.isArray(evidence)) throw new Error("Run event evidence must be an array.");
31
+ return evidence.map((item) => {
32
+ if (typeof item === "string") return { kind: "note", summary: item.trim() };
33
+ if (!item || typeof item !== "object" || Array.isArray(item)) return item;
34
+ const normalized = { kind: item.kind };
35
+ if (item.ref !== undefined && item.ref !== null) normalized.ref = String(item.ref).trim();
36
+ if (item.summary !== undefined && item.summary !== null) normalized.summary = String(item.summary).trim();
37
+ return normalized;
38
+ });
39
+ }
40
+
41
+ function evidenceForTransition(nextStatus, note, evidence = []) {
42
+ const normalized = normalizeEvidence(evidence);
43
+ if (note && !normalized.length) {
44
+ const kind = nextStatus === "validating" ? "test" : nextStatus === "learning" ? "insight" : nextStatus === "blocked" || nextStatus === "failed" ? "risk" : "note";
45
+ normalized.push({ kind, summary: String(note).trim() });
46
+ }
47
+ return normalized;
48
+ }
49
+
50
+ function makeRunEvent({
51
+ runId,
52
+ sequence,
53
+ type = "transition",
54
+ occurredAt = instant(),
55
+ timestampPrecision = "instant",
56
+ actor = "agent",
57
+ from,
58
+ to,
59
+ reason,
60
+ evidence = []
61
+ }) {
62
+ return {
63
+ schema: RUN_LEDGER_VERSION,
64
+ id: eventId(runId, sequence),
65
+ sequence,
66
+ type,
67
+ occurred_at: occurredAt,
68
+ timestamp_precision: timestampPrecision,
69
+ actor,
70
+ from: from === undefined ? null : from,
71
+ to,
72
+ reason,
73
+ evidence: normalizeEvidence(evidence)
74
+ };
75
+ }
76
+
77
+ function renderRunEvent(event) {
78
+ return `### ${event.id}\n\n\`\`\`json\n${JSON.stringify(event, null, 2)}\n\`\`\``;
79
+ }
80
+
81
+ function eventsSection(events) {
82
+ return `## Events\n\n${events.map(renderRunEvent).join("\n\n")}`;
83
+ }
84
+
85
+ function runTitle(body, fallback) {
86
+ return ((body || "").match(/^# ([^\r\n]+)$/m) || [])[1] || fallback;
87
+ }
88
+
89
+ function createRunBody(record, {
90
+ title = `Run for ${record.task}`,
91
+ approvalId = null,
92
+ occurredAt = instant(),
93
+ actor = "owner",
94
+ reason = "Explicit approval accepted.",
95
+ evidence = null
96
+ } = {}) {
97
+ const eventEvidence = evidence || [{
98
+ kind: "approval",
99
+ ...(approvalId ? { ref: `approval:${approvalId}` } : {}),
100
+ summary: reason
101
+ }];
102
+ const event = makeRunEvent({
103
+ runId: record.id,
104
+ sequence: 1,
105
+ occurredAt,
106
+ actor,
107
+ from: "created",
108
+ to: "executing",
109
+ reason,
110
+ evidence: eventEvidence
111
+ });
112
+ return { body: `# ${title}\n\n${eventsSection([event])}`, event };
113
+ }
114
+
115
+ function parseRunLedger(body) {
116
+ const events = [];
117
+ const errors = [];
118
+ const headings = [...String(body || "").matchAll(/^### (RUN-\d{3,}-EVT-\d{3,})[ \t]*$/gm)];
119
+ const blocks = [...String(body || "").matchAll(/^### (RUN-\d{3,}-EVT-\d{3,})[ \t]*\r?\n[ \t]*\r?\n```json[ \t]*\r?\n([\s\S]*?)\r?\n```[ \t]*$/gm)];
120
+ if (headings.length !== blocks.length) errors.push("one or more Run event blocks are malformed");
121
+ for (const block of blocks) {
122
+ try {
123
+ const event = JSON.parse(block[2]);
124
+ if (!event || typeof event !== "object" || Array.isArray(event)) throw new Error("event payload must be an object");
125
+ if (event.id !== block[1]) errors.push(`${block[1]} payload id does not match its heading`);
126
+ events.push(event);
127
+ } catch (error) {
128
+ errors.push(`${block[1]} contains invalid JSON: ${error.message}`);
129
+ }
130
+ }
131
+ return { events, errors };
132
+ }
133
+
134
+ function validateEvidence(event) {
135
+ const errors = [];
136
+ if (!Array.isArray(event.evidence)) return [`${event.id}.evidence must be an array`];
137
+ for (let index = 0; index < event.evidence.length; index++) {
138
+ const item = event.evidence[index];
139
+ const label = `${event.id}.evidence[${index}]`;
140
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
141
+ errors.push(`${label} must be an object`);
142
+ continue;
143
+ }
144
+ if (!RUN_EVIDENCE_KINDS.includes(item.kind)) errors.push(`${label}.kind is invalid: ${item.kind || "missing"}`);
145
+ if (!(typeof item.ref === "string" && item.ref.trim()) && !(typeof item.summary === "string" && item.summary.trim())) {
146
+ errors.push(`${label} requires ref or summary`);
147
+ }
148
+ }
149
+ return errors;
150
+ }
151
+
152
+ function validateRunLedger(record, body) {
153
+ const parsed = parseRunLedger(body);
154
+ const errors = [...parsed.errors];
155
+ const events = parsed.events;
156
+ if (record.ledger !== RUN_LEDGER_VERSION) errors.push(`ledger must be ${RUN_LEDGER_VERSION}`);
157
+ if (!events.length) errors.push("Run ledger has no events");
158
+ const ids = new Set();
159
+ let previous = null;
160
+ let previousTime = null;
161
+ for (let index = 0; index < events.length; index++) {
162
+ const event = events[index];
163
+ const expectedSequence = index + 1;
164
+ const match = EVENT_ID.exec(event.id || "");
165
+ if (!match || match[1] !== record.id) errors.push(`invalid event id for ${record.id}: ${event.id || "missing"}`);
166
+ if (ids.has(event.id)) errors.push(`duplicate Run event id: ${event.id}`);
167
+ ids.add(event.id);
168
+ if (event.schema !== RUN_LEDGER_VERSION) errors.push(`${event.id}.schema must be ${RUN_LEDGER_VERSION}`);
169
+ if (event.sequence !== expectedSequence || (match && Number(match[2]) !== expectedSequence)) {
170
+ errors.push(`${event.id}.sequence must be contiguous from 1; expected ${expectedSequence}`);
171
+ }
172
+ if (!RUN_EVENT_TYPES.includes(event.type)) errors.push(`${event.id}.type is invalid: ${event.type || "missing"}`);
173
+ if (!RFC3339.test(event.occurred_at || "") || !Number.isFinite(Date.parse(event.occurred_at))) {
174
+ errors.push(`${event.id}.occurred_at must be a real RFC3339 timestamp`);
175
+ } else {
176
+ const currentTime = Date.parse(event.occurred_at);
177
+ if (previousTime !== null && currentTime < previousTime) errors.push(`${event.id}.occurred_at precedes the previous event`);
178
+ previousTime = currentTime;
179
+ }
180
+ if (!["instant", "date"].includes(event.timestamp_precision)) errors.push(`${event.id}.timestamp_precision must be instant or date`);
181
+ if (typeof event.actor !== "string" || !event.actor.trim()) errors.push(`${event.id}.actor is required`);
182
+ if (typeof event.reason !== "string" || !event.reason.trim()) errors.push(`${event.id}.reason is required`);
183
+ if (containsSecret(JSON.stringify(event))) errors.push(`${event.id} contains secret-like content`);
184
+ errors.push(...validateEvidence(event));
185
+
186
+ if (index === 0 && event.type === "snapshot") {
187
+ if (event.from !== null) errors.push(`${event.id} snapshot.from must be null`);
188
+ if (!event.evidence.some((item) => item && ["migration", "legacy"].includes(item.kind))) {
189
+ errors.push(`${event.id} snapshot requires migration or legacy evidence`);
190
+ }
191
+ } else {
192
+ const expectedFrom = index === 0 ? "created" : previous.to;
193
+ if (event.type !== "transition") errors.push(`${event.id} snapshot is allowed only as the first event`);
194
+ if (event.from !== expectedFrom) errors.push(`${event.id}.from must be ${expectedFrom}`);
195
+ if (event.from === "created") {
196
+ if (event.to !== "executing") errors.push(`${event.id} initial transition must enter executing`);
197
+ } else {
198
+ const allowed = (ARTIFACT_TRANSITIONS.run[event.from] || []);
199
+ if (!allowed.includes(event.to)) errors.push(`${event.id} contains invalid Run transition: ${event.from} -> ${event.to}`);
200
+ }
201
+ }
202
+ if (!["snapshot"].includes(event.type) && !event.evidence.length) errors.push(`${event.id} transition requires evidence`);
203
+ previous = event;
204
+ }
205
+ if (previous && previous.to !== record.status) errors.push(`Run status ${record.status} disagrees with final event state ${previous.to}`);
206
+ if (previous && record.updated !== String(previous.occurred_at || "").slice(0, 10)) {
207
+ errors.push(`Run updated date ${record.updated} disagrees with final event timestamp`);
208
+ }
209
+ if (record.status === "completed" && events[0] && events[0].type !== "snapshot") {
210
+ for (const required of ["validating", "learning", "completed"]) {
211
+ const event = events.find((item) => item.type === "transition" && item.to === required);
212
+ if (!event || !event.evidence.length) errors.push(`completed Run requires evidenced ${required} transition`);
213
+ }
214
+ }
215
+ return { events, errors };
216
+ }
217
+
218
+ function appendRunEvent(record, body, nextRecord, {
219
+ note,
220
+ evidence = [],
221
+ actor = "agent",
222
+ occurredAt = instant()
223
+ } = {}) {
224
+ const current = validateRunLedger(record, body);
225
+ if (current.errors.length) throw new Error(`Invalid Run ledger: ${current.errors.join("; ")}`);
226
+ const normalizedEvidence = evidenceForTransition(nextRecord.status, note, evidence);
227
+ if (!note && !normalizedEvidence.length) throw new Error(`Run transition to ${nextRecord.status} requires a reason or structured evidence.`);
228
+ const reason = String(note || normalizedEvidence.map((item) => item.summary || item.ref).filter(Boolean).join("; ")).trim();
229
+ const event = makeRunEvent({
230
+ runId: record.id,
231
+ sequence: current.events.length + 1,
232
+ occurredAt,
233
+ actor,
234
+ from: record.status,
235
+ to: nextRecord.status,
236
+ reason,
237
+ evidence: normalizedEvidence
238
+ });
239
+ const nextBody = `${String(body).trimEnd()}\n\n${renderRunEvent(event)}\n`;
240
+ const validated = validateRunLedger(nextRecord, nextBody);
241
+ if (validated.errors.length) throw new Error(`Run ledger transition failed validation: ${validated.errors.join("; ")}`);
242
+ return { body: nextBody, event, events: validated.events };
243
+ }
244
+
245
+ function legacyTransitionEvents(record, body, legacyHash) {
246
+ const raw = [];
247
+ const log = String(body || "").match(/^## Transition Log[ \t]*\r?\n([\s\S]*?)(?=^## |(?![\s\S]))/m);
248
+ if (log) {
249
+ const initial = log[1].match(/^-\s+(\d{4}-\d{2}-\d{2}):\s*([a-z]+)\s*(?:→|->)\s*([a-z]+)\s*(?:—|-)\s*(.+)$/m);
250
+ if (initial) raw.push({ date: initial[1], from: initial[2], to: initial[3], note: initial[4].trim() });
251
+ }
252
+ for (const match of String(body || "").matchAll(/^## Transition (\d{4}-\d{2}-\d{2}) · ([a-z]+)-to-([a-z]+)[ \t]*\r?\n\r?\n-\s+([^\r\n]+)$/gm)) {
253
+ raw.push({ date: match[1], from: match[2], to: match[3], note: match[4].trim() });
254
+ }
255
+ if (!raw.length) return [];
256
+ let previous = "created";
257
+ for (const item of raw) {
258
+ if (item.from !== previous) return [];
259
+ const allowed = item.from === "created" ? ["executing"] : (ARTIFACT_TRANSITIONS.run[item.from] || []);
260
+ if (!allowed.includes(item.to)) return [];
261
+ previous = item.to;
262
+ }
263
+ if (previous !== record.status) return [];
264
+ return raw.map((item, index) => makeRunEvent({
265
+ runId: record.id,
266
+ sequence: index + 1,
267
+ occurredAt: dateInstant(item.date),
268
+ timestampPrecision: "date",
269
+ actor: "migration",
270
+ from: item.from,
271
+ to: item.to,
272
+ reason: item.note,
273
+ evidence: [
274
+ { kind: index === 0 ? "approval" : "note", summary: item.note },
275
+ { kind: "legacy", ref: `sha256:${legacyHash}`, summary: "Recovered from the byte-exact legacy Run record." }
276
+ ]
277
+ }));
278
+ }
279
+
280
+ function migrateLegacyRun(record, body, { legacyHash, backupRef }) {
281
+ let events = legacyTransitionEvents(record, body, legacyHash);
282
+ let mode = "transitions";
283
+ if (!events.length) {
284
+ mode = "snapshot";
285
+ events = [makeRunEvent({
286
+ runId: record.id,
287
+ sequence: 1,
288
+ type: "snapshot",
289
+ occurredAt: dateInstant(record.updated),
290
+ timestampPrecision: "date",
291
+ actor: "migration",
292
+ from: null,
293
+ to: record.status,
294
+ reason: "Imported the recorded Run state without inventing missing transitions.",
295
+ evidence: [{
296
+ kind: "legacy",
297
+ ref: `sha256:${legacyHash}`,
298
+ summary: `Byte-exact source preserved at ${backupRef}.`
299
+ }]
300
+ })];
301
+ }
302
+ const title = runTitle(body, `Run for ${record.task}`);
303
+ const provenance = `## Ledger Migration\n\n- Mode: ${mode}\n- Legacy SHA-256: \`${legacyHash}\`\n- Byte-exact backup: \`${backupRef}\``;
304
+ const nextRecord = { ...record, ledger: RUN_LEDGER_VERSION };
305
+ const nextBody = `# ${title}\n\n${eventsSection(events)}\n\n${provenance}\n`;
306
+ const validation = validateRunLedger(nextRecord, nextBody);
307
+ if (validation.errors.length) throw new Error(`${record.id} ledger migration failed: ${validation.errors.join("; ")}`);
308
+ return { record: nextRecord, body: nextBody, events, mode };
309
+ }
310
+
311
+ module.exports = {
312
+ appendRunEvent,
313
+ createRunBody,
314
+ dateInstant,
315
+ eventId,
316
+ eventsSection,
317
+ evidenceForTransition,
318
+ instant,
319
+ makeRunEvent,
320
+ migrateLegacyRun,
321
+ parseRunLedger,
322
+ renderRunEvent,
323
+ validateRunLedger
324
+ };
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+
3
+ const SECRET_PATTERNS = Object.freeze([
4
+ /\bsk-[A-Za-z0-9_-]{16,}\b/,
5
+ /\bAKIA[0-9A-Z]{16}\b/,
6
+ /\bgh[pousr]_[A-Za-z0-9]{30,}\b/,
7
+ /\bBearer\s+[A-Za-z0-9._~+/-]{16,}=*/i,
8
+ /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/,
9
+ /\b(?:password|passwd|secret|api[_-]?key|access[_-]?token|private[_-]?key)\s*[:=]\s*[^\s]+/i,
10
+ /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/
11
+ ]);
12
+
13
+ function containsSecret(value) {
14
+ const text = String(value || "");
15
+ return SECRET_PATTERNS.some((pattern) => pattern.test(text));
16
+ }
17
+
18
+ function assertNoSecret(values, message = "Secret-like content is forbidden outside the local vault.") {
19
+ const list = Array.isArray(values) ? values : [values];
20
+ if (list.some(containsSecret)) throw new Error(message);
21
+ }
22
+
23
+ module.exports = { SECRET_PATTERNS, assertNoSecret, containsSecret };