on_the_money 0.7.0 → 0.7.2

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/CHANGELOG.md CHANGED
@@ -4,6 +4,28 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.7.2] — 2026-07-20
8
+
9
+ ### Changed
10
+
11
+ - **HTML-106 promoted from warn to error tier.** The production trial closed with zero false positives across both consumers and one true positive (a `the()` write projecting into a slot that doesn't exist — dead state the deletion test exists to catch). Runs that previously reported-and-passed now fail on unconsumed global keys; consume the key or delete the write. The warn-tier machinery remains for future rules. (#136)
12
+ - **HTML-107 pairs by CSS reference, not name coupling** (landed post-0.7.1 as #144): the state key revealing a span may be named anything — a reveal rule necessarily references the span it reveals, so the reference is the proof. Kills the false-positive class that condemned functional cross-named wiring. (#143)
13
+
14
+ ## [0.7.1] — 2026-07-19
15
+
16
+ Port feedback, same day: the first downstream migrations onto the 0.7.0 batteries surfaced three regressions. Issues #132–#135, #137; PRs #138–#142.
17
+
18
+ ### Fixed
19
+
20
+ - **`otm/no-raw-websocket` flags only the browser global.** Scope-resolved: an imported or locally declared `WebSocket` (a Node `ws`-package client) passes — the hand-rolled-lifecycle slop vector the rule targets doesn't exist there. (#132)
21
+ - **`.ejs` joins otm-lint's markup universe** for cross-file rules (reveal spans, slots, actions); per-file rules stay `.html`-only so template syntax never trips HTML-004. HTML-107's css→span direction is silent when zero reveal spans were found anywhere — an empty universe is blindness, not dead wiring. Kills 46 false errors on server-templated projects. (#133)
22
+ - **src-first entry — the core singleton unforked.** `exports "."` (and `main`/`module`) resolve to `src/core/index.js`, the same graph every battery subpath imports; previously `.` pointed at the dist bundle, so importing the entry plus any battery loaded two `The` singletons and `boot()` configured one while batteries read the other. The redundant `./src` alias is gone; `dist/` demotes to a CDN/script-tag artifact. Reverses the 0.2.0 dist-entry decision — deliberately, no legacy. (#134)
23
+ - Test-runner IPC flake (`Unable to deserialize cloned data`) starved at the source: the CLI's console chatter is mocked in its tests. (#30 lineage)
24
+
25
+ ### Changed
26
+
27
+ - Blog example modernized to 0.7.0 idioms: `$.cloneEach` + `data-bind` replace the manual clone loop and per-clone `setAttribute`. (#135)
28
+
7
29
  ## [0.7.0] — 2026-07-19
8
30
 
9
31
  The boundary release. OTM's mandate extends to the browser side of live data — batteries for the platform's gaps, contracts instead of server code, and the lint stack grown into a drift wall — under a ratified constitution: a battery ships only if it wraps a platform *absence*, rides an existing standard or published machine-readable convention, lands with lint + doctest on day one, lives in a subpath the core never pays for, and carries an explicit NOT-list. Issues #84, #86–#89, #115–#120; PRs #121–#131.
package/README.md CHANGED
@@ -763,10 +763,10 @@ npx otm-lint --check ./src
763
763
  | **HTML-103** | `data-i18n-{var}` attr has no matching `{var}` placeholder in the dictionary template | The token is silently dropped at runtime. Either remove the unused attr or add `{var}` to the template. |
764
764
  | **HTML-104** | A global `the("key", ...)` write collides with a `data-text="key"` slot inside a `<template>` | Global writes walk the whole document and clobber every cloned instance. Rename the template slot key or the global key. |
765
765
  | **HTML-105** | `data-action` values and `on()` `[data-action="..."]` selector literals disagree | Both directions: a `data-action` no handler references is a dead control; a selector no element carries is an orphan handler. A generic `[data-action]` dispatch selector in JS waives the dead-control direction; `data-otm-dynamic` opts an element out. |
766
- | **HTML-106** *(warn)* | A globally written state key nothing consumes | The deletion test as lint: no CSS attribute selector, `[data-text]` slot, `"data-key"` JS string literal (e.g. MutationObserver `attributeFilter`), or `the("key")` read touches it — dead state or a missing CSS rule. Warn-level: reported, never fails the run. |
767
- | **HTML-107** | A `data-K-key="V"` reveal span with no `[data-K="V"]` state-CSS rule, or a CSS `[data-K-key="V"]` reference with no span | Either half missing means the message silently never shows (or the rule is dead wiring). Add the missing half or delete the orphan; `data-otm-dynamic` opts a span out of the span→CSS direction. |
766
+ | **HTML-106** | A globally written state key nothing consumes | The deletion test as lint: no CSS attribute selector, `[data-text]` slot, `"data-key"` JS string literal (e.g. MutationObserver `attributeFilter`), or `the("key")` read touches it — dead state or a missing CSS rule. Promoted from warn to error after a zero-false-positive production trial. |
767
+ | **HTML-107** | A `data-K-key="V"` reveal span no state-CSS rule references, or a CSS `[data-K-key="V"]` reference no element carries | Pairing is by CSS reference the state key revealing a span may be named anything (`body[data-form-error="V"] [data-error-key="V"]` is fine). Either half missing means the message silently never shows (or the rule is dead wiring); `data-otm-dynamic` opts a span out of the span→CSS direction. |
768
768
 
769
- `otm-lint` walks `.html`, `.js`, `.css`, and locale `.json` files. HTML files get the per-file rules above (template content included); `.js` files contribute `$.clone` references, `the()` write/read keys, and `data-action` selectors; `.css` files contribute attribute-selector consumption for HTML-106; locale dicts get loaded per HTML file's `<meta name="i18n">` for HTML-102/103. Default excludes: `node_modules`, `dist`, `.git`, dotdirs. Exit code is nonzero when error-level violations are found.
769
+ `otm-lint` walks `.html`, `.ejs` (cross-file rules only — per-file rules stay `.html`), `.js`, `.css`, and locale `.json` files. HTML files get the per-file rules above (template content included); `.js` files contribute `$.clone` references, `the()` write/read keys, and `data-action` selectors; `.css` files contribute attribute-selector consumption for HTML-106; locale dicts get loaded per HTML file's `<meta name="i18n">` for HTML-102/103. Default excludes: `node_modules`, `dist`, `.git`, dotdirs. Exit code is nonzero when error-level violations are found.
770
770
 
771
771
  ### Lint-rule scope
772
772
 
@@ -849,9 +849,11 @@ src/
849
849
  test/
850
850
  └── integration.test.js # cross-module integration suite
851
851
  dist/
852
- └── on_the_money.min.js # built ESM bundle (esbuild)
852
+ └── on_the_money.min.js # CDN/script-tag artifact only — npm resolves "." to src
853
853
  ```
854
854
 
855
+ **One module graph.** The npm entry (`.`) and every battery subpath resolve into the same `src/` graph, so `The`'s statics (`dictionary`, `locale`, `prefix`, `persistKeys`) are one singleton everywhere. `dist/on_the_money.min.js` exists solely for `<script type="module">` hotlinking — importing it *alongside* npm subpaths would fork the core, which is exactly the bug this arrangement removes.
856
+
855
857
  Unit tests are co-located with source: `src/core/On.test.js`, etc.
856
858
 
857
859
  ## Commands
package/package.json CHANGED
@@ -1,16 +1,15 @@
1
1
  {
2
2
  "name": "on_the_money",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "Opinionated, attribute-driven, standards-oriented modern framework. Native browser APIs only. See README.md for the authoring context.",
5
5
  "type": "module",
6
- "main": "dist/on_the_money.min.js",
7
- "module": "dist/on_the_money.min.js",
6
+ "main": "src/core/index.js",
7
+ "module": "src/core/index.js",
8
8
  "bin": {
9
9
  "otm-lint": "./src/linter/main.js"
10
10
  },
11
11
  "exports": {
12
- ".": "./dist/on_the_money.min.js",
13
- "./src": "./src/core/index.js",
12
+ ".": "./src/core/index.js",
14
13
  "./live": "./src/live/index.js",
15
14
  "./clipboard": "./src/clipboard/index.js",
16
15
  "./test": "./src/test/index.js",
@@ -293,9 +293,14 @@ const noRawWebsocket = {
293
293
  create(context) {
294
294
  return {
295
295
  NewExpression(node) {
296
- if (node.callee?.name === "WebSocket") {
297
- context.report({ node, messageId: "useLive" });
298
- }
296
+ if (node.callee?.name !== "WebSocket") return;
297
+ // Only the browser GLOBAL is banned. An imported or locally
298
+ // declared WebSocket (a Node ws-package client) resolves to a
299
+ // definition and is outside live()'s reach — let it pass.
300
+ const scope = context.sourceCode.getScope(node);
301
+ const ref = scope.references.find((r) => r.identifier === node.callee);
302
+ if (ref?.resolved?.defs.length) return;
303
+ context.report({ node, messageId: "useLive" });
299
304
  },
300
305
  };
301
306
  },
@@ -371,7 +371,6 @@ export default class Linter {
371
371
  { line: writer.line, column: 1 },
372
372
  "HTML-106",
373
373
  `State key "${key}" is written via the() but never consumed by a CSS attribute selector, a [data-text] slot, or a JS data-attribute reference — dead state or a missing CSS rule (the deletion test).`,
374
- "warn",
375
374
  );
376
375
  }
377
376
 
@@ -405,13 +404,18 @@ export default class Linter {
405
404
  }
406
405
 
407
406
  // Inline <style> blocks ride along in the raw HTML sources.
407
+ // Pairing is by CSS *reference*, never by name coupling: the state key
408
+ // that reveals a span may be named anything (body[data-form-error="V"]
409
+ // revealing [data-error-key="V"] is fully functional) — a reveal rule
410
+ // necessarily references the span it reveals, so the ref IS the proof.
408
411
  const styleTexts = [...cssSources, ...htmlSources];
409
- const stateRules = new Set();
410
412
  const spanRefs = [];
413
+ const refPairs = new Set();
411
414
  for (const { file, source } of styleTexts) {
412
415
  for (const m of source.matchAll(
413
416
  /\[data-([a-z0-9-]+?)-key=["']?([\w-]+)["']?\]/g,
414
417
  )) {
418
+ refPairs.add(`${m[1]} ${m[2]}`);
415
419
  spanRefs.push({
416
420
  group: m[1],
417
421
  value: m[2],
@@ -419,25 +423,23 @@ export default class Linter {
419
423
  line: Linter.#lineOf(source, m.index),
420
424
  });
421
425
  }
422
- for (const m of source.matchAll(
423
- /\[data-([a-z0-9-]+)=["']?([\w-]+)["']?\]/g,
424
- )) {
425
- if (!m[1].endsWith("-key")) stateRules.add(`${m[1]} ${m[2]}`);
426
- }
427
426
  }
428
427
 
429
428
  for (const span of revealSpans) {
430
- if (stateRules.has(`${span.group} ${span.value}`)) continue;
429
+ if (refPairs.has(`${span.group} ${span.value}`)) continue;
431
430
  Linter.#addViolation(
432
431
  violations,
433
432
  span.file,
434
433
  { line: span.line, column: span.column },
435
434
  "HTML-107",
436
- `Reveal span data-${span.group}-key="${span.value}" has no state-CSS rule matching [data-${span.group}="${span.value}"] — the message can never show. Add the reveal rule, or delete the span, or mark it data-otm-dynamic.`,
435
+ `Reveal span data-${span.group}-key="${span.value}" is referenced by no state-CSS rule the message can never show. Add a reveal rule selecting [data-${span.group}-key="${span.value}"], or delete the span, or mark it data-otm-dynamic.`,
437
436
  );
438
437
  }
439
438
 
440
- for (const ref of spanRefs) {
439
+ // An empty span universe is evidence of blindness (markup living in
440
+ // templates the scan can't see), not of dead wiring — the css→span
441
+ // direction only fires when at least one reveal span was found.
442
+ for (const ref of spanPairs.size > 0 ? spanRefs : []) {
441
443
  if (spanPairs.has(`${ref.group} ${ref.value}`)) continue;
442
444
  Linter.#addViolation(
443
445
  violations,
package/src/linter/cli.js CHANGED
@@ -62,8 +62,19 @@ export default class Cli {
62
62
  }
63
63
 
64
64
  static async scan(dir) {
65
- const allFiles = await Cli.getFiles(dir, [".html", ".js", ".json", ".css"]);
66
- const htmlPaths = allFiles.filter((f) => f.endsWith(".html"));
65
+ const allFiles = await Cli.getFiles(dir, [
66
+ ".html",
67
+ ".ejs",
68
+ ".js",
69
+ ".json",
70
+ ".css",
71
+ ]);
72
+ // .ejs joins the markup universe for cross-file rules (reveal spans,
73
+ // slots, actions); per-file rules stay .html-only — Linter.check
74
+ // skips non-.html, so template syntax never false-positives HTML-004.
75
+ const htmlPaths = allFiles.filter(
76
+ (f) => f.endsWith(".html") || f.endsWith(".ejs"),
77
+ );
67
78
  const jsPaths = allFiles.filter((f) => f.endsWith(".js"));
68
79
  const cssPaths = allFiles.filter((f) => f.endsWith(".css"));
69
80
  let totalViolations = 0;