create-pmos 0.13.0 → 0.15.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/README.md +39 -0
- package/package.json +1 -1
- package/payload/KIT-VERSION +2 -2
- package/workspace/focus.test.js +650 -0
- package/workspace/handoff.test.js +443 -0
- package/workspace/lib/console-state.js +48 -1
- package/workspace/lib/drive-guard.js +98 -0
- package/workspace/lib/drive-plan.js +97 -0
- package/workspace/lib/engine/workflow-state.js +13 -3
- package/workspace/lib/focus.js +967 -0
- package/workspace/lib/handoff.js +280 -0
- package/workspace/lib/layout.js +68 -46
- package/workspace/lib/mobile-nav.js +127 -0
- package/workspace/lib/profiles.js +285 -0
- package/workspace/lib/repo-scan.js +469 -0
- package/workspace/package.json +4 -2
- package/workspace/profiles.test.js +362 -0
- package/workspace/public/app.js +2318 -208
- package/workspace/public/index.html +158 -28
- package/workspace/public/styles.css +865 -51
- package/workspace/repo-scan.test.js +327 -0
- package/workspace/server.js +914 -23
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* Unit suite for lib/handoff.js — the handoff links model.
|
|
3
|
+
*
|
|
4
|
+
* No framework, no mocks. Records are REAL markdown files on disk, parsed by the REAL
|
|
5
|
+
* `parseLeadingFrontmatter` (lib/file-mode-parse.js) — the same function the server and the kit's
|
|
6
|
+
* metrics.py agree on. A test double for the parser would agree with whatever this file assumed,
|
|
7
|
+
* and the frontmatter format's sharp edge (section A5) is precisely a thing a double would hide.
|
|
8
|
+
*
|
|
9
|
+
* The suite is organised around the one invariant the module exists to hold:
|
|
10
|
+
*
|
|
11
|
+
* A · the five record fields, through the real parser — and the format's truncation trap
|
|
12
|
+
* B · every link is NOT-CONNECTED, per row, naming its own system
|
|
13
|
+
* C · absence and emptiness are stated, never implied
|
|
14
|
+
* D · a malformed declaration is a problem with its value verbatim — never dropped, never rendered
|
|
15
|
+
* E · the URL admission table: clickable only as an absolute http(s) URL
|
|
16
|
+
* F · STRUCTURAL IMPOSSIBILITY — the section that matters
|
|
17
|
+
* G · the real repository, as a corpus check
|
|
18
|
+
*
|
|
19
|
+
* Section F is the point of this file. Anyone can write a test showing that today's code did not
|
|
20
|
+
* fabricate a story count; that test passes again the day after someone adds one somewhere else.
|
|
21
|
+
* F asserts that the module CANNOT: it imports nothing, so no reading can enter it; it returns no
|
|
22
|
+
* number anywhere, so a count has no field to live in; its key sets are compared for equality, so a
|
|
23
|
+
* `synced_at` cannot be added quietly; its result is frozen, so `connected` cannot be flipped
|
|
24
|
+
* downstream; and every string it emits is either copied verbatim out of the record file or is one
|
|
25
|
+
* of ten enumerated fixed sentences.
|
|
26
|
+
*
|
|
27
|
+
* Exits non-zero if any check fails.
|
|
28
|
+
*/
|
|
29
|
+
'use strict';
|
|
30
|
+
const fs = require('fs');
|
|
31
|
+
const os = require('os');
|
|
32
|
+
const path = require('path');
|
|
33
|
+
const assert = require('assert');
|
|
34
|
+
|
|
35
|
+
const H = require('./lib/handoff');
|
|
36
|
+
const { parseLeadingFrontmatter } = require('./lib/file-mode-parse');
|
|
37
|
+
|
|
38
|
+
const results = [];
|
|
39
|
+
const ok = (name, cond, extra = '') => { results.push({ name, cond }); console.log(`${cond ? '✓' : '✗'} ${name}${extra ? ' — ' + extra : ''}`); };
|
|
40
|
+
|
|
41
|
+
const TMP = fs.mkdtempSync(path.join(os.tmpdir(), 'pmos-handoff-'));
|
|
42
|
+
/** An initiative record file with whatever frontmatter the case needs. */
|
|
43
|
+
function record(fm, body = 'Body prose, which must never be able to fake a declaration.\n') {
|
|
44
|
+
return `---\n${Object.entries(fm).map(([k, v]) => `${k}: ${v}`).join('\n')}\n---\n\n${body}`;
|
|
45
|
+
}
|
|
46
|
+
function writeRecord(name, fm, body) {
|
|
47
|
+
const full = path.join(TMP, name);
|
|
48
|
+
fs.writeFileSync(full, record(fm, body));
|
|
49
|
+
return full;
|
|
50
|
+
}
|
|
51
|
+
/** The whole path a real record takes: file → real parser → model. */
|
|
52
|
+
const modelOf = (file) => H.readLinks(parseLeadingFrontmatter(fs.readFileSync(file, 'utf8')));
|
|
53
|
+
const linkById = (m, id) => m.links.find((l) => l.id === id) || null;
|
|
54
|
+
const problemById = (m, id) => m.problems.find((p) => p.id === id) || null;
|
|
55
|
+
|
|
56
|
+
/** Every string anywhere in a value — the basis of the whitelist and the fabrication checks. */
|
|
57
|
+
function collectStrings(v, out = []) {
|
|
58
|
+
if (typeof v === 'string') out.push(v);
|
|
59
|
+
else if (Array.isArray(v)) v.forEach((x) => collectStrings(x, out));
|
|
60
|
+
else if (v && typeof v === 'object') Object.keys(v).forEach((k) => collectStrings(v[k], out));
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
/** Every non-string primitive anywhere — used to prove there is not one number in the model. */
|
|
64
|
+
function collectPrimitives(v, out = []) {
|
|
65
|
+
if (v === null || typeof v !== 'object') out.push(v);
|
|
66
|
+
else if (Array.isArray(v)) v.forEach((x) => collectPrimitives(x, out));
|
|
67
|
+
else Object.keys(v).forEach((k) => collectPrimitives(v[k], out));
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
const uniqSorted = (a) => [...new Set(a)].sort();
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
// ── A · the five record fields, through the real parser (R1) ────────────────────────────────
|
|
74
|
+
const allFile = writeRecord('all-five.md', {
|
|
75
|
+
type: 'Initiative', title: '"crm import wizard"', timestamp: '2026-07-20',
|
|
76
|
+
id: 'crm-import-wizard', lane: 'feature', parent_kr: 'q3-2026:o1:kr2',
|
|
77
|
+
flow: 'adlc-handoff',
|
|
78
|
+
handoff_to: 'atlas app team — the ATL board',
|
|
79
|
+
handoff_epic: 'ATL-482',
|
|
80
|
+
handoff_page: 'https://example.atlassian.net/wiki/spaces/PM/crm-import-wizard-prd',
|
|
81
|
+
handoff_at: '2026-07-20',
|
|
82
|
+
});
|
|
83
|
+
const allText = fs.readFileSync(allFile, 'utf8');
|
|
84
|
+
const all = modelOf(allFile);
|
|
85
|
+
|
|
86
|
+
ok('A1: the five R1 keys round-trip through the REAL parser into the model',
|
|
87
|
+
all.handoff === true && all.flow === 'adlc-handoff' && all.to === 'atlas app team — the ATL board'
|
|
88
|
+
&& all.at === '2026-07-20' && all.links.length === 2,
|
|
89
|
+
JSON.stringify({ flow: all.flow, at: all.at, links: all.links.length }));
|
|
90
|
+
|
|
91
|
+
ok('A2: every declared value is carried VERBATIM — nothing is normalised, prettified or re-cased',
|
|
92
|
+
linkById(all, 'handoff_epic').declared === 'ATL-482'
|
|
93
|
+
&& linkById(all, 'handoff_page').declared === 'https://example.atlassian.net/wiki/spaces/PM/crm-import-wizard-prd',
|
|
94
|
+
linkById(all, 'handoff_epic').declared);
|
|
95
|
+
|
|
96
|
+
ok('A3: `systems` names both systems, from which keys are declared — not from any reading',
|
|
97
|
+
JSON.stringify(all.systems) === JSON.stringify(['Jira', 'Confluence']), JSON.stringify(all.systems));
|
|
98
|
+
|
|
99
|
+
// Link order is fixed by the module, not by the record, so the section cannot re-order itself when
|
|
100
|
+
// a PM happens to type the page key above the epic key.
|
|
101
|
+
const flippedFile = writeRecord('flipped.md', {
|
|
102
|
+
type: 'Initiative', title: '"flipped"', timestamp: '2026-07-20', id: 'flipped',
|
|
103
|
+
handoff_page: 'https://example.atlassian.net/wiki/x', handoff_epic: 'ATL-9',
|
|
104
|
+
});
|
|
105
|
+
ok('A4: link order is the module\'s, not the record\'s — frontmatter order cannot reshuffle the section',
|
|
106
|
+
JSON.stringify(modelOf(flippedFile).links.map((l) => l.id)) === JSON.stringify(['handoff_epic', 'handoff_page']),
|
|
107
|
+
JSON.stringify(modelOf(flippedFile).links.map((l) => l.id)));
|
|
108
|
+
|
|
109
|
+
/* A5 — the truncation trap (PRD R2). This is here so that a later author who reaches for
|
|
110
|
+
* `links:` as a YAML list finds a FAILING TEST explaining what it costs, rather than discovering
|
|
111
|
+
* it through an initiative that silently reads as unanchored. The list does not partially parse:
|
|
112
|
+
* the `links:` line ends the frontmatter block, and every key below it — including `lane` and
|
|
113
|
+
* `parent_kr`, the two the Initiative Gate rejects work for lacking — is dropped without a word. */
|
|
114
|
+
const listFile = path.join(TMP, 'yaml-list.md');
|
|
115
|
+
fs.writeFileSync(listFile,
|
|
116
|
+
'---\ntype: Initiative\ntitle: "listy"\ntimestamp: 2026-07-29\nid: listy\n'
|
|
117
|
+
+ 'links:\n - ATL-482\n - https://example.atlassian.net/wiki/x\n'
|
|
118
|
+
+ 'lane: feature\nparent_kr: q3-2026:o1:kr2\n---\n\nBody.\n');
|
|
119
|
+
const truncated = parseLeadingFrontmatter(fs.readFileSync(listFile, 'utf8'));
|
|
120
|
+
ok('A5: a YAML list TERMINATES the frontmatter block — `lane` and `parent_kr` below it vanish, '
|
|
121
|
+
+ 'which is why the handoff fields are flat and single-line and must stay that way',
|
|
122
|
+
truncated.id === 'listy' && !('links' in truncated) && !('lane' in truncated) && !('parent_kr' in truncated),
|
|
123
|
+
JSON.stringify(Object.keys(truncated)));
|
|
124
|
+
|
|
125
|
+
// ── B · every link is NOT-CONNECTED, per row, naming its own system (R4) ────────────────────
|
|
126
|
+
const epic = linkById(all, 'handoff_epic');
|
|
127
|
+
const page = linkById(all, 'handoff_page');
|
|
128
|
+
|
|
129
|
+
ok('B1: the epic row carries the state, the clause and the full sentence — on the ROW, not once per section',
|
|
130
|
+
epic.state === 'not-connected' && epic.why === 'no Jira credential is configured'
|
|
131
|
+
&& epic.state_text === 'not connected — no Jira credential is configured; this is what the record declares, not a reading',
|
|
132
|
+
epic.state_text);
|
|
133
|
+
|
|
134
|
+
ok('B2: a Confluence row says Confluence — the state names the system this row points at, not a generic one',
|
|
135
|
+
page.state === 'not-connected' && page.why === 'no Confluence credential is configured'
|
|
136
|
+
&& page.state_text.includes('no Confluence credential is configured') && !page.state_text.includes('Jira'),
|
|
137
|
+
page.state_text);
|
|
138
|
+
|
|
139
|
+
ok('B3: the state is text, so it can be a row\'s accessible name — never colour, never a tooltip alone',
|
|
140
|
+
all.links.every((l) => typeof l.state_text === 'string' && l.state_text.length > 60
|
|
141
|
+
&& typeof l.why === 'string' && l.why.length > 20),
|
|
142
|
+
JSON.stringify(all.links.map((l) => l.state_text.length)));
|
|
143
|
+
|
|
144
|
+
ok('B4: `kind` is a marker for WHICH system, and is the only field that could ever be a badge',
|
|
145
|
+
epic.kind === 'jira' && page.kind === 'confluence', `${epic.kind} / ${page.kind}`);
|
|
146
|
+
|
|
147
|
+
ok('B5: `id` is the RECORD KEY, so a renderer keying rows on it never prints it as the epic',
|
|
148
|
+
epic.id === 'handoff_epic' && page.id === 'handoff_page' && epic.id !== epic.declared,
|
|
149
|
+
`${epic.id} vs ${epic.declared}`);
|
|
150
|
+
|
|
151
|
+
// ── C · absence and emptiness are stated, never implied ─────────────────────────────────────
|
|
152
|
+
const absent = H.readLinks(undefined);
|
|
153
|
+
ok('C1: no argument at all is a successful read of nothing — calm, not an error, not a claim',
|
|
154
|
+
absent.handoff === false && absent.links.length === 0 && absent.problems.length === 0
|
|
155
|
+
&& absent.empty === false && absent.flow === null && absent.to === null && absent.at === null,
|
|
156
|
+
JSON.stringify(absent));
|
|
157
|
+
|
|
158
|
+
ok('C2: null is answered identically — an initiative with no record is not a fault',
|
|
159
|
+
JSON.stringify(H.readLinks(null)) === JSON.stringify(absent));
|
|
160
|
+
|
|
161
|
+
const plainFile = writeRecord('plain.md', {
|
|
162
|
+
type: 'Initiative', title: '"plain"', timestamp: '2026-07-29', id: 'plain',
|
|
163
|
+
lane: 'feature', parent_kr: 'q3-2026:o1:kr2',
|
|
164
|
+
});
|
|
165
|
+
const plain = modelOf(plainFile);
|
|
166
|
+
ok('C3: a record that was never handed off claims nothing — the panel is unchanged for it',
|
|
167
|
+
plain.handoff === false && plain.empty === false && plain.links.length === 0
|
|
168
|
+
&& plain.problems.length === 0 && plain.systems.length === 0,
|
|
169
|
+
JSON.stringify({ handoff: plain.handoff, empty: plain.empty }));
|
|
170
|
+
|
|
171
|
+
const flowOnlyFile = writeRecord('flow-only.md', {
|
|
172
|
+
type: 'Initiative', title: '"flow only"', timestamp: '2026-07-29', id: 'flow-only', flow: 'adlc-handoff',
|
|
173
|
+
});
|
|
174
|
+
const flowOnly = modelOf(flowOnlyFile);
|
|
175
|
+
ok('C4: `flow:` declared with no references is a VERIFIED EMPTY (R6) — a stated absence, not a hidden section',
|
|
176
|
+
flowOnly.handoff === true && flowOnly.empty === true && flowOnly.flow === 'adlc-handoff'
|
|
177
|
+
&& flowOnly.links.length === 0 && flowOnly.problems.length === 0,
|
|
178
|
+
JSON.stringify({ handoff: flowOnly.handoff, empty: flowOnly.empty }));
|
|
179
|
+
|
|
180
|
+
const toOnlyFile = writeRecord('to-only.md', {
|
|
181
|
+
type: 'Initiative', title: '"to only"', timestamp: '2026-07-29', id: 'to-only', handoff_to: 'atlas app team',
|
|
182
|
+
});
|
|
183
|
+
ok('C5: a recipient with no references is the same verified empty — the section still renders',
|
|
184
|
+
modelOf(toOnlyFile).empty === true && modelOf(toOnlyFile).to === 'atlas app team');
|
|
185
|
+
|
|
186
|
+
ok('C6: a declared reference is NOT empty — `empty` means nothing to show, never nothing to say',
|
|
187
|
+
all.empty === false, String(all.empty));
|
|
188
|
+
|
|
189
|
+
// ── D · a malformed declaration is red, with its value verbatim (R7) ────────────────────────
|
|
190
|
+
const badFile = writeRecord('malformed.md', {
|
|
191
|
+
type: 'Initiative', title: '"malformed"', timestamp: '2026-07-29', id: 'malformed',
|
|
192
|
+
flow: '../../../etc/passwd',
|
|
193
|
+
handoff_epic: 'ATL-482 import wizard epic 3 of 7 done',
|
|
194
|
+
handoff_at: 'soonish',
|
|
195
|
+
});
|
|
196
|
+
const bad = modelOf(badFile);
|
|
197
|
+
|
|
198
|
+
ok('D1: `handoff_at: soonish` is a problem with the offending value VERBATIM, and never reaches `at`',
|
|
199
|
+
bad.at === null && !!problemById(bad, 'handoff_at') && problemById(bad, 'handoff_at').declared === 'soonish'
|
|
200
|
+
&& problemById(bad, 'handoff_at').why.includes('will not guess'),
|
|
201
|
+
JSON.stringify(problemById(bad, 'handoff_at')));
|
|
202
|
+
|
|
203
|
+
ok('D2: an epic carrying whitespace-separated prose is a problem, NOT a link — junk is never dressed as a reference',
|
|
204
|
+
bad.links.length === 0 && problemById(bad, 'handoff_epic').declared === 'ATL-482 import wizard epic 3 of 7 done',
|
|
205
|
+
JSON.stringify(bad.links.map((l) => l.declared)));
|
|
206
|
+
|
|
207
|
+
ok('D3: a `flow:` that is a path rather than a template name is refused — the "open the file" affordance stays honest',
|
|
208
|
+
!!problemById(bad, 'flow') && problemById(bad, 'flow').declared === '../../../etc/passwd' && bad.flow === null,
|
|
209
|
+
JSON.stringify(problemById(bad, 'flow')));
|
|
210
|
+
|
|
211
|
+
ok('D4: a malformed reference still names its system, so the ladder can still say what PMOS cannot read',
|
|
212
|
+
JSON.stringify(bad.systems) === JSON.stringify(['Jira']), JSON.stringify(bad.systems));
|
|
213
|
+
|
|
214
|
+
ok('D5: a record that is only malformed is NOT a verified empty — red wins over calm',
|
|
215
|
+
bad.handoff === true && bad.empty === false && bad.problems.length === 3,
|
|
216
|
+
JSON.stringify({ empty: bad.empty, problems: bad.problems.length }));
|
|
217
|
+
|
|
218
|
+
const emptyVal = H.readLinks({ handoff_epic: ' ', handoff_to: '' });
|
|
219
|
+
ok('D6: present-but-empty is reported as present-and-empty, not as absent',
|
|
220
|
+
emptyVal.problems.length === 2 && emptyVal.problems.every((p) => p.why.includes('present but empty'))
|
|
221
|
+
&& emptyVal.problems.every((p) => p.declared === null),
|
|
222
|
+
JSON.stringify(emptyVal.problems.map((p) => p.id)));
|
|
223
|
+
|
|
224
|
+
const nonString = H.readLinks({ handoff_at: 20260720, handoff_epic: ['ATL-1'], handoff_to: { team: 'atlas' } });
|
|
225
|
+
ok('D7: a value that was never text yields a problem naming the TYPE and a null value — nothing is invented to print',
|
|
226
|
+
nonString.problems.length === 3 && nonString.problems.every((p) => p.declared === null)
|
|
227
|
+
&& problemById(nonString, 'handoff_at').why.includes('a number')
|
|
228
|
+
&& problemById(nonString, 'handoff_epic').why.includes('an array'),
|
|
229
|
+
JSON.stringify(nonString.problems.map((p) => `${p.id}=${p.declared}`)));
|
|
230
|
+
|
|
231
|
+
const ctrl = H.readLinks({ handoff_epic: 'ATL-\u0007482' });
|
|
232
|
+
ok('D8: a control character is refused rather than rendered — what it would print is not what the record shows',
|
|
233
|
+
ctrl.links.length === 0 && !!problemById(ctrl, 'handoff_epic')
|
|
234
|
+
&& problemById(ctrl, 'handoff_epic').why.includes('control character'));
|
|
235
|
+
|
|
236
|
+
const wrongShape = H.readLinks(allText);
|
|
237
|
+
ok('D9: handed the record TEXT instead of its parsed frontmatter, the model states a fault — not a silent empty',
|
|
238
|
+
wrongShape.handoff === false && wrongShape.problems.length === 1
|
|
239
|
+
&& wrongShape.problems[0].id === 'record' && wrongShape.problems[0].why.includes('a string'),
|
|
240
|
+
wrongShape.problems[0].why.slice(0, 60));
|
|
241
|
+
|
|
242
|
+
ok('D10: a problem carries exactly PROBLEM_FIELDS — no room for a "last checked" beside the reason',
|
|
243
|
+
[bad, emptyVal, nonString, wrongShape].every((m) => m.problems.every(
|
|
244
|
+
(p) => JSON.stringify(Object.keys(p)) === JSON.stringify(H.PROBLEM_FIELDS))),
|
|
245
|
+
JSON.stringify(H.PROBLEM_FIELDS));
|
|
246
|
+
|
|
247
|
+
ok('D11: no problem message embeds the offending value — the value is printed once, by `declared`',
|
|
248
|
+
problemById(bad, 'handoff_at').why.indexOf('soonish') === -1
|
|
249
|
+
&& problemById(bad, 'flow').why.indexOf('passwd') === -1,
|
|
250
|
+
problemById(bad, 'handoff_at').why.slice(0, 50));
|
|
251
|
+
|
|
252
|
+
// ── E · the URL admission table (R5, PRD test 3) ────────────────────────────────────────────
|
|
253
|
+
const TABLE = [
|
|
254
|
+
['https://example.atlassian.net/browse/ATL-482', true, 'an absolute https URL'],
|
|
255
|
+
['http://jira.internal/browse/ATL-482', true, 'an absolute http URL'],
|
|
256
|
+
['HTTPS://EXAMPLE.ATLASSIAN.NET/browse/ATL-482', true, 'scheme case does not matter'],
|
|
257
|
+
['ATL-482', false, 'a bare reference'],
|
|
258
|
+
['jira://ATL-482', false, 'a non-http scheme'],
|
|
259
|
+
['//evil.example.com/browse/ATL-482', false, 'protocol-relative — inherits whatever scheme the page is on'],
|
|
260
|
+
['javascript:alert(1)', false, 'a script URL'],
|
|
261
|
+
['data:text/html,<h1>hi</h1>', false, 'a data URL'],
|
|
262
|
+
['/planning/prd/thing.md', false, 'a repo-relative path'],
|
|
263
|
+
['https://', false, 'a scheme with no host'],
|
|
264
|
+
];
|
|
265
|
+
const admitted = TABLE.map(([v, want]) => {
|
|
266
|
+
const l = H.readLinks({ handoff_epic: v }).links[0];
|
|
267
|
+
return !!(l && l.href) === want && (l ? (want ? l.href === v : l.href === null) : false);
|
|
268
|
+
});
|
|
269
|
+
ok('E1: the admission table holds — a value is clickable only as an absolute http(s) URL',
|
|
270
|
+
admitted.every(Boolean),
|
|
271
|
+
TABLE.filter((_, i) => !admitted[i]).map(([v]) => v).join(' | ') || 'all 10 rows as specified');
|
|
272
|
+
|
|
273
|
+
const dangerous = ['javascript:alert(1)', 'data:text/html,<h1>hi</h1>', '//evil.example.com'];
|
|
274
|
+
ok('E2: a `javascript:`/`data:`/protocol-relative value NEVER reaches an href — it survives only as `declared`',
|
|
275
|
+
dangerous.every((v) => {
|
|
276
|
+
const l = H.readLinks({ handoff_epic: v }).links[0];
|
|
277
|
+
return l.href === null && l.declared === v && collectStrings(l).filter((s) => s === v).length === 1;
|
|
278
|
+
}));
|
|
279
|
+
|
|
280
|
+
const inert = H.readLinks({ handoff_epic: 'ATL-482' }).links[0];
|
|
281
|
+
ok('E3: a refused value is inert WITH ITS REASON — never silently dropped, never silently dead',
|
|
282
|
+
inert.href === null && inert.inert_why === H.INERT_WHY && inert.declared === 'ATL-482'
|
|
283
|
+
&& inert.inert_why.includes('will not construct one it was not given'),
|
|
284
|
+
inert.inert_why);
|
|
285
|
+
|
|
286
|
+
ok('E4: an admitted href is byte-identical to the declared value — PMOS links what it was given, nothing else',
|
|
287
|
+
page.href === page.declared && page.inert_why === null, page.href);
|
|
288
|
+
|
|
289
|
+
const rawish = H.readLinks({ handoff_page: 'https://x.example/<script>&"' });
|
|
290
|
+
ok('E5: values are returned UNESCAPED and verbatim — escaping belongs at the interpolation site, and '
|
|
291
|
+
+ 'a model that pre-escaped could not be checked against the record file',
|
|
292
|
+
rawish.links[0].declared === 'https://x.example/<script>&"'
|
|
293
|
+
&& rawish.links[0].href === 'https://x.example/<script>&"',
|
|
294
|
+
rawish.links[0].declared);
|
|
295
|
+
|
|
296
|
+
// ── F · STRUCTURAL IMPOSSIBILITY ────────────────────────────────────────────────────────────
|
|
297
|
+
// A corpus wide enough that the properties below are claims about the module, not about one case.
|
|
298
|
+
const CORPUS = [
|
|
299
|
+
['absent', undefined], ['null', null], ['plain', parseLeadingFrontmatter(fs.readFileSync(plainFile, 'utf8'))],
|
|
300
|
+
['all five', parseLeadingFrontmatter(allText)], ['flow only', { flow: 'adlc-handoff' }],
|
|
301
|
+
['malformed', parseLeadingFrontmatter(fs.readFileSync(badFile, 'utf8'))],
|
|
302
|
+
['empty values', { handoff_epic: ' ', handoff_to: '' }],
|
|
303
|
+
['non-string values', { handoff_at: 20260720, handoff_epic: ['ATL-1'] }],
|
|
304
|
+
['wrong shape', allText], ['an array', ['handoff_epic']], ['a number', 7], ['a function', () => 1],
|
|
305
|
+
// The hostile record: a PM (or a well-meaning agent) writes into the record the very things PMOS
|
|
306
|
+
// cannot know. None of it may reach the model, because none of it is a field the model has.
|
|
307
|
+
['hostile', {
|
|
308
|
+
flow: 'adlc-handoff', handoff_epic: 'ATL-482', handoff_to: 'atlas app team', handoff_at: '2026-07-20',
|
|
309
|
+
connected: 'true', story_count: '7', stories_done: '3 of 7', synced_at: '12:38',
|
|
310
|
+
last_checked: '2026-07-29T12:38:00Z', jira_status: 'In Progress', progress: '43%',
|
|
311
|
+
}],
|
|
312
|
+
...TABLE.map(([v]) => [`url:${v}`, { handoff_epic: v }]),
|
|
313
|
+
];
|
|
314
|
+
const MODELS = CORPUS.map(([name, input]) => [name, H.readLinks(input)]);
|
|
315
|
+
|
|
316
|
+
const src = fs.readFileSync(path.join(__dirname, 'lib', 'handoff.js'), 'utf8');
|
|
317
|
+
ok('F1: the module imports NOTHING — with no `require(` there is no fs, no socket and no clock, '
|
|
318
|
+
+ 'so there is no path by which a reading of an external system could enter it',
|
|
319
|
+
!/\brequire\s*\(/.test(src) && !/\bimport\s*\(/.test(src) && !/^\s*import\s/m.test(src),
|
|
320
|
+
(src.match(/\brequire\s*\([^)]*\)/g) || []).join(', ') || 'no imports of any kind');
|
|
321
|
+
|
|
322
|
+
const CLOCK_OR_IO = [/\bDate\b/, /\bMath\b/, /\bprocess\b/, /\bfetch\s*\(/, /\bsetTimeout\b/,
|
|
323
|
+
/\bsetInterval\b/, /\bXMLHttpRequest\b/, /\bWebSocket\b/, /\bglobalThis\b/];
|
|
324
|
+
ok('F2: no clock and no I/O identifier appears in the source — a sync time cannot be formatted by '
|
|
325
|
+
+ 'code that cannot read the time',
|
|
326
|
+
CLOCK_OR_IO.every((re) => !re.test(src)),
|
|
327
|
+
CLOCK_OR_IO.filter((re) => re.test(src)).map(String).join(', ') || 'none present');
|
|
328
|
+
|
|
329
|
+
const numbers = MODELS.flatMap(([name, m]) => collectPrimitives(m).filter((p) => typeof p === 'number').map((p) => `${name}:${p}`));
|
|
330
|
+
ok('F3: there is not ONE number anywhere in the model, for any input — a story count, an issue '
|
|
331
|
+
+ 'tally and a progress percentage have no field they could occupy',
|
|
332
|
+
numbers.length === 0, numbers.join(', ') || `${MODELS.length} inputs, 0 numeric values`);
|
|
333
|
+
|
|
334
|
+
const exportedOutputs = [H.whyFor('Jira'), H.stateTextFor('Confluence'), H.NOT_CONNECTED, H.INERT_WHY,
|
|
335
|
+
H.HANDOFF_KEYS, H.REFERENCE_KEYS, H.LINK_FIELDS, H.PROBLEM_FIELDS, H.RESULT_FIELDS];
|
|
336
|
+
ok('F4: no export returns a number about anything, external or otherwise',
|
|
337
|
+
exportedOutputs.every((v) => collectPrimitives(v).every((p) => typeof p !== 'number')),
|
|
338
|
+
Object.keys(H).join(', '));
|
|
339
|
+
|
|
340
|
+
const linkKeys = MODELS.flatMap(([name, m]) => m.links.map((l) => [name, JSON.stringify(Object.keys(l))]));
|
|
341
|
+
ok('F5: every link carries EXACTLY LINK_FIELDS, in order — a `story_count` or a `synced_at` cannot '
|
|
342
|
+
+ 'be added to a row without failing here',
|
|
343
|
+
linkKeys.length > 0 && linkKeys.every(([, k]) => k === JSON.stringify(H.LINK_FIELDS)),
|
|
344
|
+
`${linkKeys.length} links checked against ${JSON.stringify(H.LINK_FIELDS)}`);
|
|
345
|
+
|
|
346
|
+
ok('F6: every result carries EXACTLY RESULT_FIELDS, in order — the same lock on the model itself',
|
|
347
|
+
MODELS.every(([, m]) => JSON.stringify(Object.keys(m)) === JSON.stringify(H.RESULT_FIELDS)),
|
|
348
|
+
JSON.stringify(H.RESULT_FIELDS));
|
|
349
|
+
|
|
350
|
+
ok('F7: every link in every model is `not-connected` — there is no second state and no input reaches one',
|
|
351
|
+
MODELS.every(([, m]) => m.links.every((l) => l.state === H.NOT_CONNECTED && l.state === 'not-connected')),
|
|
352
|
+
`${MODELS.reduce((n, [, m]) => n + m.links.length, 0)} links, one state`);
|
|
353
|
+
|
|
354
|
+
ok('F8: `connected` is false for every input — including a record that flatly declares it true',
|
|
355
|
+
MODELS.every(([, m]) => m.connected === false),
|
|
356
|
+
`${MODELS.length} inputs`);
|
|
357
|
+
|
|
358
|
+
// Frozen, so a consumer that would rather the answer were different cannot make it so after the
|
|
359
|
+
// fact. In strict mode the assignment throws; the assertion is on the value either way.
|
|
360
|
+
const frozen = MODELS.find(([n]) => n === 'all five')[1];
|
|
361
|
+
let threw = 0;
|
|
362
|
+
try { frozen.connected = true; } catch { threw++; }
|
|
363
|
+
try { frozen.synced_at = '12:38'; } catch { threw++; }
|
|
364
|
+
try { frozen.links[0].story_count = '3 of 7'; } catch { threw++; }
|
|
365
|
+
try { frozen.links.push({ kind: 'jira' }); } catch { threw++; }
|
|
366
|
+
ok('F9: the model is deep-frozen — `connected` cannot be flipped and no fabricated field can be '
|
|
367
|
+
+ 'bolted on downstream',
|
|
368
|
+
threw === 4 && frozen.connected === false && !('synced_at' in frozen)
|
|
369
|
+
&& !('story_count' in frozen.links[0]) && frozen.links.length === 2,
|
|
370
|
+
`${threw}/4 mutations refused`);
|
|
371
|
+
|
|
372
|
+
/* F10 — THE ANTI-FABRICATION WHITELIST (PRD test 8), and the check the rest of this file exists
|
|
373
|
+
* to support. Not a denylist of "3 of 7" and "synced": those ban today's fabrication and license
|
|
374
|
+
* tomorrow's. Every string the model emits for a real record is partitioned against THE RECORD
|
|
375
|
+
* FILE ITSELF. Whatever is not in the file must be one of the fixed sentences enumerated below —
|
|
376
|
+
* compared for EQUALITY, so a value that is computed, estimated, tallied or formatted relative to
|
|
377
|
+
* now lands in that pile and fails immediately, whatever it is and however plausible it looks. */
|
|
378
|
+
const FIXED_VOCABULARY = [
|
|
379
|
+
'Jira', 'Confluence', 'jira', 'confluence', 'not-connected',
|
|
380
|
+
'no Jira credential is configured',
|
|
381
|
+
'no Confluence credential is configured',
|
|
382
|
+
'not connected — no Jira credential is configured; this is what the record declares, not a reading',
|
|
383
|
+
'not connected — no Confluence credential is configured; this is what the record declares, not a reading',
|
|
384
|
+
'PMOS was given a reference, not a URL, and will not construct one it was not given',
|
|
385
|
+
];
|
|
386
|
+
const notInFile = uniqSorted(collectStrings(all).filter((s) => allText.indexOf(s) === -1));
|
|
387
|
+
ok('F10: every string the model emits is either VERBATIM from the record file or one of ten fixed '
|
|
388
|
+
+ 'sentences — a value nobody typed has nowhere to hide',
|
|
389
|
+
JSON.stringify(notInFile) === JSON.stringify(uniqSorted(FIXED_VOCABULARY)),
|
|
390
|
+
notInFile.filter((s) => FIXED_VOCABULARY.indexOf(s) === -1).map((s) => JSON.stringify(s)).join(', ')
|
|
391
|
+
|| `${collectStrings(all).length} strings, all accounted for`);
|
|
392
|
+
|
|
393
|
+
// F10 is only worth anything if it can fail. This proves the partition has teeth before it is
|
|
394
|
+
// trusted: a fabricated value put through the same computation is caught by it.
|
|
395
|
+
const fabricated = { links: [{ declared: 'ATL-482', summary: '3 of 7 stories done', synced: 'synced 12:38' }] };
|
|
396
|
+
ok('F11: the F10 partition demonstrably CATCHES a fabrication — the check has teeth, not just a pass',
|
|
397
|
+
collectStrings(fabricated).filter((s) => allText.indexOf(s) === -1).sort().join('|') === '3 of 7 stories done|synced 12:38');
|
|
398
|
+
|
|
399
|
+
const hostile = MODELS.find(([n]) => n === 'hostile')[1];
|
|
400
|
+
const hostileStrings = collectStrings(hostile).join('');
|
|
401
|
+
ok('F12: the hostile record\'s fabricated keys and values reach the model NOWHERE — belt beside '
|
|
402
|
+
+ 'F10\'s braces, and the one denylist in this file, named as one',
|
|
403
|
+
['3 of 7', '12:38', '43%', 'In Progress', 'story_count', 'synced_at', 'last_checked']
|
|
404
|
+
.every((s) => hostileStrings.indexOf(s) === -1)
|
|
405
|
+
&& hostile.connected === false && hostile.links.length === 1,
|
|
406
|
+
JSON.stringify(hostile.links.map((l) => l.declared)));
|
|
407
|
+
|
|
408
|
+
ok('F13: the model is a pure function of its input — two calls are deep-equal, so nothing in it '
|
|
409
|
+
+ 'varies with when it was called',
|
|
410
|
+
(() => { try { assert.deepStrictEqual(H.readLinks(parseLeadingFrontmatter(allText)), all); return true; } catch { return false; } })());
|
|
411
|
+
|
|
412
|
+
ok('F14: no export name suggests an action or a reading PMOS cannot perform',
|
|
413
|
+
Object.keys(H).every((k) => !/sync|refresh|fetch|poll|count|progress|connect(?!ed)/i.test(k)),
|
|
414
|
+
Object.keys(H).join(', '));
|
|
415
|
+
|
|
416
|
+
// ── G · the real repository, as a corpus check ──────────────────────────────────────────────
|
|
417
|
+
// The fixtures above were written by this file and agree with it by construction. These records
|
|
418
|
+
// were written by people who had never heard of this module.
|
|
419
|
+
const initDir = path.join(__dirname, '..', 'planning', 'initiatives');
|
|
420
|
+
const realFiles = fs.readdirSync(initDir).filter((f) => f.endsWith('.md') && f !== 'index.md' && f !== 'log.md')
|
|
421
|
+
.map((f) => path.join(initDir, f));
|
|
422
|
+
const realModels = realFiles.map((f) => [path.basename(f), modelOf(f)]);
|
|
423
|
+
|
|
424
|
+
ok('G1: every real initiative record yields a well-shaped model — no throw, no missing field',
|
|
425
|
+
realModels.length > 50 && realModels.every(([, m]) => JSON.stringify(Object.keys(m)) === JSON.stringify(H.RESULT_FIELDS)),
|
|
426
|
+
`${realModels.length} records`);
|
|
427
|
+
|
|
428
|
+
ok('G2: not one of them declares a handoff today, so the panel is UNCHANGED for every initiative '
|
|
429
|
+
+ 'in this repository — the feature is additive or it is nothing',
|
|
430
|
+
realModels.every(([, m]) => m.handoff === false && m.links.length === 0 && m.problems.length === 0 && m.empty === false),
|
|
431
|
+
realModels.filter(([, m]) => m.handoff).map(([n]) => n).join(', ') || 'none handed off');
|
|
432
|
+
|
|
433
|
+
ok('G3: on the real corpus there is still no number, no connection and no unexpected field',
|
|
434
|
+
realModels.every(([, m]) => m.connected === false && collectPrimitives(m).every((p) => typeof p !== 'number')),
|
|
435
|
+
`${realModels.length} records, 0 numeric values`);
|
|
436
|
+
} catch (e) {
|
|
437
|
+
ok('no exceptions during suite', false, e && e.stack ? e.stack.split('\n').slice(0, 2).join(' @ ') : String(e));
|
|
438
|
+
} finally {
|
|
439
|
+
try { fs.rmSync(TMP, { recursive: true, force: true }); } catch {}
|
|
440
|
+
const failed = results.filter((r) => !r.cond);
|
|
441
|
+
console.log(`\n${results.length - failed.length}/${results.length} passed${failed.length ? ' — FAILURES: ' + failed.map((f) => f.name).join('; ') : ''}`);
|
|
442
|
+
process.exit(failed.length ? 1 : 0);
|
|
443
|
+
}
|
|
@@ -222,4 +222,51 @@ function workflow({ root }) {
|
|
|
222
222
|
|
|
223
223
|
// ENGINE_PATHS/loadEngine are exported for the packaging guard in workspace/test.js: the incident
|
|
224
224
|
// that made them a list was invisible to every unit test precisely because resolution was inline.
|
|
225
|
-
|
|
225
|
+
/* ONE initiative's ladder, derived on demand.
|
|
226
|
+
*
|
|
227
|
+
* WHY THIS IS SEPARATE FROM `workflow`. The board caps what it returns (`inFlight`) and reports the
|
|
228
|
+
* remainder as `hidden` — 12 shown against 93 hidden in this repository. That cap is right for a
|
|
229
|
+
* board, which is a list of what is in flight. It is wrong for a PANEL, which is asked about one
|
|
230
|
+
* initiative the PM picked by name: reading the capped list and finding nothing there means "not in
|
|
231
|
+
* the top twelve", and a panel that renders that as "no initiative record" states something false
|
|
232
|
+
* about 93 of 105 records. This derives the one that was asked for, so the answer is about the
|
|
233
|
+
* initiative rather than about its rank.
|
|
234
|
+
*
|
|
235
|
+
* It is safe on the request path for the same reason the board is not: this shells git once for the
|
|
236
|
+
* commit range and runs the engine ONCE, rather than 105 times. `workflow` still owns the board.
|
|
237
|
+
*/
|
|
238
|
+
function ladder({ root, id }) {
|
|
239
|
+
if (!root) return { ok: false, error: 'no workspace root resolved for the ladder' };
|
|
240
|
+
if (!id) return { ok: false, error: 'no initiative id given' };
|
|
241
|
+
const loaded = loadEngine();
|
|
242
|
+
if (!loaded.ok) return { ok: false, error: loaded.error };
|
|
243
|
+
const engine = loaded.engine;
|
|
244
|
+
// ABSENT and BROKEN are different facts and must not share an answer. The engine returns
|
|
245
|
+
// `ok:false` for BOTH "there is no record with this id" and "the record is there and unreadable",
|
|
246
|
+
// and collapsing them makes the panel paint a red "read failed" over an initiative that simply
|
|
247
|
+
// does not exist — overstating a missing file as a fault. The initiative list is the arbiter:
|
|
248
|
+
// an id the repository does not know is an empty read, and only an id it DOES know can fail one.
|
|
249
|
+
let known = null;
|
|
250
|
+
try { known = engine.listInitiatives(root); } catch { known = null; }
|
|
251
|
+
if (Array.isArray(known) && !known.includes(id)) return { ok: true, empty: true, row: null };
|
|
252
|
+
let range, row;
|
|
253
|
+
try { range = engine.commitRange(root); }
|
|
254
|
+
catch (e) { return { ok: false, error: 'could not resolve the commit range: ' + String((e && e.message) || 'error').slice(0, 120) }; }
|
|
255
|
+
try { row = engine.computeWorkflow(id, { root, range }); }
|
|
256
|
+
catch (e) { return { ok: false, error: `could not derive the ladder for ${id}: ` + String((e && e.message) || 'error').slice(0, 160) }; }
|
|
257
|
+
if (row && row.ok === false) return { ok: false, error: row.error || `the record for ${id} could not be read` };
|
|
258
|
+
const bi = gitInfo.branchInfo(root);
|
|
259
|
+
return {
|
|
260
|
+
ok: true,
|
|
261
|
+
empty: !row, // no such record — distinct from a read that failed
|
|
262
|
+
row: row || null,
|
|
263
|
+
scope: {
|
|
264
|
+
branch: bi.ok ? (bi.current || 'a detached HEAD') : null,
|
|
265
|
+
branch_error: bi.ok ? null : (bi.error || 'branch could not be read'),
|
|
266
|
+
commits: range.ok ? range.shas.length : null,
|
|
267
|
+
empty_range: range.ok ? range.shas.length === 0 : null,
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
module.exports = { metrics, radar, workflow, ladder, computeMetricsFromRuns, loadJson, loadJsonl, ENGINE_PATHS, loadEngine };
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* drive-guard — the driver's ONLY write path (pmos-autonomous-drive-build, R9).
|
|
3
|
+
*
|
|
4
|
+
* THE PROBLEM THIS EXISTS FOR, stated plainly because it is easy to under-rate:
|
|
5
|
+
* nothing in PMOS checks an identity. `/api/gate` sets `pm: true` unconditionally
|
|
6
|
+
* (workspace/server.js). Every gate the corpus calls "the PM's" — D16's contract approval, D12's
|
|
7
|
+
* acceptance, D60's disposition — is, mechanically, A FILE WITH THE RIGHT WORDS IN IT. An
|
|
8
|
+
* autonomous writer can satisfy all of them by writing. If it can, the PM's gate structure is
|
|
9
|
+
* decorative and the whole three-tier eval gate is theatre.
|
|
10
|
+
*
|
|
11
|
+
* The harness already knows how to make a declaration falsifiable and did not apply it here: O7
|
|
12
|
+
* validates `priors` ids against the corpus, so a fabricated citation FAILS — while O5's
|
|
13
|
+
* `PM_approved` is a bare boolean, so a fabricated approval PASSES. This module is the smallest
|
|
14
|
+
* honest answer available to code: the driver cannot write those records because the only writer it
|
|
15
|
+
* has REFUSES them.
|
|
16
|
+
*
|
|
17
|
+
* WHY REFUSAL AND NOT OMISSION. A driver that simply never calls the acceptance writer is one
|
|
18
|
+
* prompt away from calling it. The rubric's veto dimension asks for structural incapability and
|
|
19
|
+
* says so: "a code path that merely omits them fails this dimension". So the guard is a chokepoint
|
|
20
|
+
* that throws, and the tests ATTEMPT each forbidden write and assert the throw. A rule the driver
|
|
21
|
+
* is trusted to follow is exactly what leg A showed cannot be trusted.
|
|
22
|
+
*
|
|
23
|
+
* SCOPE, honestly. This binds the DRIVER, not the repo: a human, another agent, or a careless
|
|
24
|
+
* `fs.writeFileSync` elsewhere is unaffected. It is a seatbelt on one vehicle, not a law of
|
|
25
|
+
* physics — and that limit is the argument for making O5's boolean falsifiable at the engine, which
|
|
26
|
+
* is a separate initiative this one does not pretend to have done.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const path = require('path');
|
|
30
|
+
|
|
31
|
+
class ForbiddenWrite extends Error {
|
|
32
|
+
constructor(what, detail) {
|
|
33
|
+
super(`drive-guard: refused to write ${what} — ${detail}`);
|
|
34
|
+
this.name = 'ForbiddenWrite';
|
|
35
|
+
this.what = what;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Ledgers that record a human decision. The driver may never append to these. */
|
|
40
|
+
const FORBIDDEN_FILES = Object.freeze(['acceptances.jsonl']);
|
|
41
|
+
|
|
42
|
+
/** Disposition tokens (D60). Case-insensitive; a status carrying one is a PM act. */
|
|
43
|
+
const DISPOSITION_RE = /\b(PROMOTED|ITERATE[D]?|KILLED)\b/i;
|
|
44
|
+
|
|
45
|
+
/** True when `value` contains a truthy PM_approved anywhere in its object graph. */
|
|
46
|
+
function carriesApproval(value, depth = 0) {
|
|
47
|
+
if (depth > 8 || value === null || typeof value !== 'object') return false;
|
|
48
|
+
for (const [k, v] of Object.entries(value)) {
|
|
49
|
+
if (k === 'PM_approved' && v === true) return true;
|
|
50
|
+
if (k === 'pre_run_contract' && v && typeof v === 'object' && v.PM_approved === true) return true;
|
|
51
|
+
if (carriesApproval(v, depth + 1)) return true;
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The chokepoint. Every driver write goes through here or does not happen.
|
|
58
|
+
* @param {{file:string, payload?:any, text?:string}} req
|
|
59
|
+
* @throws {ForbiddenWrite}
|
|
60
|
+
*/
|
|
61
|
+
function assertWritable(req) {
|
|
62
|
+
const file = (req && req.file) || '';
|
|
63
|
+
const base = path.basename(file);
|
|
64
|
+
|
|
65
|
+
if (FORBIDDEN_FILES.includes(base)) {
|
|
66
|
+
throw new ForbiddenWrite('an acceptance', 'the Acceptance Gate is the PM\'s and non-delegable (D12)');
|
|
67
|
+
}
|
|
68
|
+
if (req && req.payload !== undefined && carriesApproval(req.payload)) {
|
|
69
|
+
throw new ForbiddenWrite('pre_run_contract.PM_approved',
|
|
70
|
+
'the contract is the PM\'s to approve before any build (D16, AGENTS.md §4)');
|
|
71
|
+
}
|
|
72
|
+
if (req && typeof req.text === 'string' && /PM_approved\s*[:=]\s*true/i.test(req.text)) {
|
|
73
|
+
throw new ForbiddenWrite('pre_run_contract.PM_approved',
|
|
74
|
+
'the contract is the PM\'s to approve before any build (D16) — caught in raw text');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// A disposition is a PM act (D60) wherever it lands: the record's `status:` is where it is
|
|
78
|
+
// conventionally written, so both the structured and the raw-text shapes are refused.
|
|
79
|
+
const status = req && req.payload && typeof req.payload === 'object' ? req.payload.status : null;
|
|
80
|
+
if (typeof status === 'string' && DISPOSITION_RE.test(status)) {
|
|
81
|
+
throw new ForbiddenWrite('a disposition', 'promote/iterate/kill is the PM\'s (D60 step 7)');
|
|
82
|
+
}
|
|
83
|
+
if (req && typeof req.text === 'string' && /^status:.*$/im.test(req.text)
|
|
84
|
+
&& DISPOSITION_RE.test(req.text.match(/^status:.*$/im)[0])) {
|
|
85
|
+
throw new ForbiddenWrite('a disposition', 'promote/iterate/kill is the PM\'s (D60 step 7)');
|
|
86
|
+
}
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Wrap a writer so it cannot be called with a forbidden payload. The driver is given ONLY this. */
|
|
91
|
+
function guardedWriter(write) {
|
|
92
|
+
return function guarded(req) {
|
|
93
|
+
assertWritable(req);
|
|
94
|
+
return write(req);
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = { assertWritable, guardedWriter, carriesApproval, ForbiddenWrite, FORBIDDEN_FILES };
|