create-pmos 0.20.0 → 0.20.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-pmos",
3
- "version": "0.20.0",
3
+ "version": "0.20.1",
4
4
  "description": "Scaffold the PMOS portable kit — run the PMOS product-management workflow (initiative → lane-fit spec → contract → build → three gates, with work-type lanes for features, research, maintenance, incidents, and content) entirely inside your own repo, with all state as git-versioned files. `create-pmos workspace` also installs + launches the local Terminal Workspace GUI. No database, no network, no telemetry.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -1,3 +1,3 @@
1
- kernel-source: pmos@ec75724
1
+ kernel-source: pmos@1d54a27
2
2
  assembled: 2026-07-31
3
3
  mode: vendor
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.20.0",
3
+ "version": "0.20.1",
4
4
  "private": true,
5
5
  "description": "PMOS Terminal Workspace \u2014 local companion hosting real PTYs beside the library and gates (D62)",
6
6
  "license": "MIT",
@@ -40,6 +40,12 @@
40
40
  rather than only in a browser. pm/mobile.js reads it from the global; the module graph does not
41
41
  import it, because a second copy of the reducer is exactly the drift it was extracted to stop. -->
42
42
  <script src="/lib/mobile-nav.js"></script>
43
+ <!-- ONE classifier, both sides (workspace-reader-preview). What the reader may do with a file —
44
+ markdown, escaped text, an HTML page, an image — is decided by this module, which server.js
45
+ requires for its read endpoints and the doc tab reads here as window.PreviewKind. A second
46
+ opinion about what an .html file is would mean the endpoint and the pane disagreeing about the
47
+ same document. Classic script (a UMD), like the three above. -->
48
+ <script src="/lib/preview-kind.js"></script>
43
49
  <link rel="preload" href="/vendor/ibm-plex-mono-500.woff2" as="font" type="font/woff2" crossorigin />
44
50
  </head>
45
51
  <body>
@@ -1,8 +1,13 @@
1
1
  /* pm/tab-doc.js — the document tab body: view · raw · edit · diff, with save, revert and a dirty flag.
2
2
  *
3
3
  * A port of the prototype's doc pane (template lines 869–927, derivation 2020–2045). The four modes
4
- * are the whole design: `view` renders the markdown, `raw` shows the file tinted by line kind,
4
+ * are the whole design: `view` renders the document, `raw` shows the file tinted by line kind,
5
5
  * `edit` is the buffer, and `diff` is a real LCS line diff of the buffer against what is on disk.
6
+ *
7
+ * `view` renders the markdown — EXCEPT for an HTML file, which is a page, not prose. Handing one to
8
+ * the markdown parser prints its own markup back at the PM: the document read as its source code.
9
+ * So an `.html` document renders, in `view`, as the page it is; `raw` is the one click that still
10
+ * shows the code. See `htmlFrame` for why rendering it is not a new privilege.
6
11
  * The diff is COMPUTED, never narrated — a "2 sections differ" that nothing read is the watermelon
7
12
  * this repo has a word for, and the same rule that governs artifact sync governs this pane.
8
13
  *
@@ -31,6 +36,7 @@ const iv = (v) => el('span', null, v === null || v === undefined ? '' : String(v
31
36
  const disk = new Map(); // path -> text read from this machine
32
37
  const pending = new Set(); // paths whose read is in flight
33
38
  const saveErr = new Map(); // path -> why the last save was refused
39
+ const unread = new Set(); // paths whose read FAILED — what `disk` holds for them is a stub
34
40
 
35
41
  let dataMod = null;
36
42
  import('./data.js').then((m) => { dataMod = m; }).catch(() => { dataMod = null; });
@@ -77,9 +83,12 @@ function requestDoc(tab) {
77
83
  : fetch('/api/doc?ws=' + encodeURIComponent(wsId()) + '&path=' + encodeURIComponent(path))
78
84
  .then((r) => (r.ok ? r.text() : Promise.reject(new Error('HTTP ' + r.status))));
79
85
  Promise.resolve(load).then(
80
- (text) => done(typeof text === 'string' ? text : String(text == null ? '' : text)),
81
- (e) => done(stub(tab, 'This file could not be read: ' + (e && e.message ? e.message : e)
82
- + '\n\nNothing was rendered from it. That is the failure being stated rather than an empty page shown.')),
86
+ (text) => { unread.delete(path); done(typeof text === 'string' ? text : String(text == null ? '' : text)); },
87
+ (e) => {
88
+ unread.add(path);
89
+ done(stub(tab, 'This file could not be read: ' + (e && e.message ? e.message : e)
90
+ + '\n\nNothing was rendered from it. That is the failure being stated rather than an empty page shown.'));
91
+ },
83
92
  );
84
93
  }
85
94
 
@@ -180,6 +189,71 @@ function rawBody(text) {
180
189
  }));
181
190
  }
182
191
 
192
+ /* ── the HTML page body ────────────────────────────────────────────────────────────────────────
193
+ *
194
+ * `previewKind` is the single classifier both sides of this app already trust (server.js requires
195
+ * it; index.html loads it as a classic script), so "what counts as HTML" is decided in exactly one
196
+ * place and cannot drift from the read endpoints. If it somehow did not load, `isHtmlDoc` answers
197
+ * false and the pane behaves exactly as it did before — a missing classifier degrades to the old
198
+ * view, never to an unclassified render.
199
+ */
200
+ const isHtmlDoc = (p) => {
201
+ const PK = typeof window !== 'undefined' ? window.PreviewKind : null;
202
+ if (!PK || typeof PK.previewKind !== 'function') return false;
203
+ return PK.previewKind(String(p || '')) === 'html';
204
+ };
205
+
206
+ /* A STUB IS NOT THE DOCUMENT, AND MUST NOT BE RENDERED AS ONE. When a read is in flight or has
207
+ * failed, `docText` hands back this pane's own prose — "reading …", or the failure with its reason.
208
+ * Rendering that inside a frame would bury the one sentence the PM needs behind an iframe boundary,
209
+ * and an .html file that could not be read would look like an empty page: the exact "never a blank
210
+ * page" rule this pane already keeps for every other kind. So a page is drawn only from text that
211
+ * genuinely came from disk (or from an edit saved in this window). A reference URI never renders —
212
+ * nothing was read from it to render. */
213
+ function rendersAsPage(tab) {
214
+ const p = tab.path;
215
+ if (!isHtmlDoc(p) || isReference(p)) return false;
216
+ if (state.savedDocs[p] != null) return true;
217
+ if (FIXTURE) return true;
218
+ return disk.has(p) && !unread.has(p);
219
+ }
220
+
221
+ /* Rendering an HTML document adds no privilege over showing it. The frame is fully sandboxed —
222
+ * `sandbox=""` grants nothing, so scripts and same-origin access are both denied — and the injected
223
+ * CSP is `default-src 'none'`, which blocks every network fetch the page could attempt. A poisoned
224
+ * explainer therefore can neither execute nor beacon out, which is the same no-remote-fetch-on-view
225
+ * invariant the markdown image rule holds (R4). Only inline styles, data: images and data: fonts
226
+ * resolve — enough for a page to look like itself, not enough to reach anything. */
227
+ const HTML_CSP = '<meta http-equiv="Content-Security-Policy" content="'
228
+ + "default-src 'none'; img-src data:; style-src 'unsafe-inline'; font-src data:" + '">';
229
+
230
+ const frames = new Map(); // tab id -> { text, node } — the frame currently showing that text
231
+
232
+ /* THE FRAME IS NEVER RE-POINTED, IT IS REPLACED. Writing a new `srcdoc` onto a frame that is
233
+ * already in the document does navigate it — but the replacement document does NOT get the meta
234
+ * CSP applied to its subresource loads, so the remote <link>s in the new page are fetched. That
235
+ * is not a theory: the panel-4 suite opens `web/index.html`, whose remote font stylesheet went out
236
+ * exactly this way, and the containment assertion caught it. Setting `srcdoc` BEFORE the frame is
237
+ * inserted is enforced correctly, so a text change builds a new frame and the old one is dropped.
238
+ *
239
+ * The cache is therefore keyed on the TEXT, not just the tab: a repaint that changed nothing hands
240
+ * back the identical node — which is what `setChildren` needs to leave a live frame alone — while a
241
+ * changed document gets a frame built from scratch. The cost is that saving an open HTML file
242
+ * returns it to the top; the alternative is a page that renders contained only until it is edited. */
243
+ function htmlFrame(tab, text) {
244
+ const cur = frames.get(tab.id);
245
+ if (cur && cur.text === text) return cur.node;
246
+ const node = el('iframe', {
247
+ style: 'flex:1;min-height:0;width:100%;border:0;background:var(--background)',
248
+ sandbox: '', // no allow-scripts, no allow-same-origin
249
+ referrerpolicy: 'no-referrer',
250
+ title: String(tab.path || 'document'),
251
+ srcdoc: HTML_CSP + text, // set HERE, on a node not yet in the document — see above
252
+ });
253
+ frames.set(tab.id, { text, node });
254
+ return node;
255
+ }
256
+
183
257
  function diffBody(raw, text, dirty) {
184
258
  const rows = dirty ? FX.diffLines(raw, text) : [FX.NO_DIFF_ROW];
185
259
  return el('div', {
@@ -212,6 +286,7 @@ function editBox(tab) {
212
286
  /** Release what a closed doc tab held. Called by panel-tabs.js when the tab goes away. */
213
287
  export function disposeDoc(id) {
214
288
  boxes.delete(id);
289
+ frames.delete(id);
215
290
  }
216
291
 
217
292
  /* ── mount ─────────────────────────────────────────────────────────────────────────────────── */
@@ -270,7 +345,7 @@ export function mountDoc(host, tab) {
270
345
  * genuinely differs, which is what keeps the caret still mid-sentence. */
271
346
  if (body.defaultValue !== text) body.defaultValue = text;
272
347
  if (body.value !== text) body.value = text;
273
- } else body = viewBody(text);
348
+ } else body = rendersAsPage(tab) ? htmlFrame(tab, text) : viewBody(text);
274
349
 
275
350
  setChildren(host, [modeBar, body]);
276
351
  return host;