@am_shork/attest 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.
@@ -31,16 +31,11 @@ import { hasRecordedRed, recordedOutcome } from './red-record.js';
31
31
  export function statusRows(addedIds, plan, firstRun) {
32
32
  // Index the scenarios once, the way `validator.ts`'s `detectPotentialDrift`
33
33
  // does and for the reason recorded there: filtering the whole plan per
34
- // requirement is O(requirements x scenarios). The plan here is the *merged*
35
- // one, so the inner term is the whole repository's scenario count while the
36
- // outer is only what this change adds.
34
+ // requirement is O(requirements x scenarios), and the plan here is the
35
+ // *merged* one, so the inner term is the whole repository's scenario count.
37
36
  //
38
- // The cost is invisible today 20 added ids against 2000 scenarios is 40k
39
- // comparisons, well under a millisecond so this is filed as the
40
- // inconsistency it is rather than as a slow path. Two functions over one
41
- // shape held two beliefs about whether it is worth indexing; they hold one
42
- // now. Insertion order is plan order, which is file then line, so the
43
- // scenarios a row carries are still in the order their author reads them.
37
+ // Insertion order is plan order, which is file then line, so the scenarios a
38
+ // row carries stay in the order their author reads them.
44
39
  const byReqId = new Map();
45
40
  for (const s of plan.scenarios) {
46
41
  const group = byReqId.get(s.reqId);
@@ -16,32 +16,18 @@ export declare function inline(text: string): string;
16
16
  /**
17
17
  * Every C0 control except the newline, plus DEL and the C1 range, as a space.
18
18
  *
19
- * A space rather than deletion: removing the byte would silently splice
20
- * `atte` + `st` into a word that was never in the file, and a diagnostic that
21
- * quietly rewrites what it quotes is its own kind of wrong.
19
+ * A space rather than deletion, and the newline exempt by not being in the
20
+ * class: both are design §9.1, stated there because they hold for every artifact
21
+ * Attest writes rather than only for this one.
22
22
  *
23
- * The class is written out rather than computed per character. The previous
24
- * spelling spread the string into a per-code-point array, mapped and rejoined
25
- * three allocations proportional to the input, where the engine's own scan needs
26
- * none when nothing matches, which is the case every real registry is. Measured
27
- * over 120,000 characters: **27x** faster on plain ASCII, **54x** with
28
- * newlines, **84x** on CJK. A payload that is *entirely* control bytes is a
29
- * wash (0.9x), because then there is nothing to fast-path and both spellings
30
- * build a new string; both are linear either way, so this is a constant factor
31
- * rather than a second ATX-59.
23
+ * The one thing §9.1 does not reach: surrogates `D800`–`DFFF` fall outside every
24
+ * range below, so a pair is never touched and never split the case that makes
25
+ * a per-code-point rewrite look necessary when it is not.
32
26
  *
33
- * It is worth the change because the count scales with the registry while the
34
- * length scales with whatever the registry chose: `render`'s `sanitised` calls
27
+ * Written out rather than computed per character so the scan allocates nothing
28
+ * when nothing matches, which is what every real registry is; `render` calls
35
29
  * this once per statement, rationale, param key, param value and out-of-scope
36
- * entry, on the same reachable `render --check` path ATX-59 came off.
37
- *
38
- * **Byte-identical to the spelling it replaces**, which is the only thing that
39
- * mattered: checked exhaustively over every code unit in the BMP, and over
40
- * 200,000 randomised strings mixing control bytes, CJK, astral characters and
41
- * lone surrogates. Surrogates are the case the spread existed to get right —
42
- * `D800`–`DFFF` fall outside every range below, so neither spelling touches a
43
- * pair or splits one — and newlines survive here as they always did, by not
44
- * being in the class.
30
+ * entry, on the `render --check` path ATX-59 came off. Measured in `[0.7.0]`.
45
31
  */
46
32
  export declare function control(text: string): string;
47
33
  //# sourceMappingURL=terminal.d.ts.map
@@ -41,32 +41,18 @@ export function inline(text) {
41
41
  /**
42
42
  * Every C0 control except the newline, plus DEL and the C1 range, as a space.
43
43
  *
44
- * A space rather than deletion: removing the byte would silently splice
45
- * `atte` + `st` into a word that was never in the file, and a diagnostic that
46
- * quietly rewrites what it quotes is its own kind of wrong.
44
+ * A space rather than deletion, and the newline exempt by not being in the
45
+ * class: both are design §9.1, stated there because they hold for every artifact
46
+ * Attest writes rather than only for this one.
47
47
  *
48
- * The class is written out rather than computed per character. The previous
49
- * spelling spread the string into a per-code-point array, mapped and rejoined
50
- * three allocations proportional to the input, where the engine's own scan needs
51
- * none when nothing matches, which is the case every real registry is. Measured
52
- * over 120,000 characters: **27x** faster on plain ASCII, **54x** with
53
- * newlines, **84x** on CJK. A payload that is *entirely* control bytes is a
54
- * wash (0.9x), because then there is nothing to fast-path and both spellings
55
- * build a new string; both are linear either way, so this is a constant factor
56
- * rather than a second ATX-59.
48
+ * The one thing §9.1 does not reach: surrogates `D800`–`DFFF` fall outside every
49
+ * range below, so a pair is never touched and never split the case that makes
50
+ * a per-code-point rewrite look necessary when it is not.
57
51
  *
58
- * It is worth the change because the count scales with the registry while the
59
- * length scales with whatever the registry chose: `render`'s `sanitised` calls
52
+ * Written out rather than computed per character so the scan allocates nothing
53
+ * when nothing matches, which is what every real registry is; `render` calls
60
54
  * this once per statement, rationale, param key, param value and out-of-scope
61
- * entry, on the same reachable `render --check` path ATX-59 came off.
62
- *
63
- * **Byte-identical to the spelling it replaces**, which is the only thing that
64
- * mattered: checked exhaustively over every code unit in the BMP, and over
65
- * 200,000 randomised strings mixing control bytes, CJK, astral characters and
66
- * lone surrogates. Surrogates are the case the spread existed to get right —
67
- * `D800`–`DFFF` fall outside every range below, so neither spelling touches a
68
- * pair or splits one — and newlines survive here as they always did, by not
69
- * being in the class.
55
+ * entry, on the `render --check` path ATX-59 came off. Measured in `[0.7.0]`.
70
56
  */
71
57
  export function control(text) {
72
58
  // eslint-disable-next-line no-control-regex -- matching control characters is the whole function.
@@ -1,4 +1,4 @@
1
- export type { Requirement, Registry } from './schema.js';
1
+ export type { Requirement, Registry, ParamValue } from './schema.js';
2
2
  export type { IssueCode } from './docs.js';
3
3
  import type { IssueCode } from './docs.js';
4
4
  /** A scenario extracted statically from a spec file (design §5.2). */
@@ -15,6 +15,7 @@ export declare function uncoveredIssues(registry: Registry, plan: AttestPlan): I
15
15
  * - orphan-test: a scenario covers an unknown requirement id
16
16
  * - uncovered-requirement: a requirement has no scenario
17
17
  * - unbound-param: a statement placeholder has no matching param
18
+ * - non-scalar-interpolation: a statement placeholder names a structured param
18
19
  *
19
20
  * and one WARNING:
20
21
  * - rationale-placeholder: a `{name}` in a rationale, which is never interpolated
@@ -30,6 +30,7 @@ export function uncoveredIssues(registry, plan) {
30
30
  * - orphan-test: a scenario covers an unknown requirement id
31
31
  * - uncovered-requirement: a requirement has no scenario
32
32
  * - unbound-param: a statement placeholder has no matching param
33
+ * - non-scalar-interpolation: a statement placeholder names a structured param
33
34
  *
34
35
  * and one WARNING:
35
36
  * - rationale-placeholder: a `{name}` in a rationale, which is never interpolated
@@ -78,6 +79,14 @@ export function validateStructure(registry, plan, registryIncomplete = false) {
78
79
  // `Object.hasOwn`, never `in`: `'toString' in {}` is true, so an `in` probe
79
80
  // silently accepts placeholders no author declared — and render.ts would then
80
81
  // interpolate the prototype method into the document.
82
+ //
83
+ // non-scalar-interpolation: the placeholder resolves, but to a value that has
84
+ // no reading as a sentence. This is what the schema's old scalar-or-list union
85
+ // was actually defending — `[object Object]` in the rendered requirement
86
+ // document — and it is stated here because that is where the damage is. A
87
+ // param the statement never names can be any shape it likes: a kind -> payload
88
+ // table the scenario reads is not a rendering problem, and refusing it in the
89
+ // schema refused the drift-prone value along with the display bug.
81
90
  for (const [id, req] of Object.entries(registry)) {
82
91
  for (const m of req.statement.matchAll(/\{(\w+)\}/g)) {
83
92
  const name = m[1];
@@ -88,6 +97,16 @@ export function validateStructure(registry, plan, registryIncomplete = false) {
88
97
  reqId: id,
89
98
  message: `Requirement "${id}" uses {${name}} in its statement, but params does not define it.`,
90
99
  });
100
+ continue;
101
+ }
102
+ if (!interpolatable(req.params[name])) {
103
+ issues.push({
104
+ level: 'ERROR',
105
+ code: 'non-scalar-interpolation',
106
+ reqId: id,
107
+ message: `Requirement "${id}" uses {${name}} in its statement, but that param is ${shapeOf(req.params[name])}. ` +
108
+ `A statement placeholder must be a scalar or a list of scalars.`,
109
+ });
91
110
  }
92
111
  }
93
112
  }
@@ -156,4 +175,22 @@ export function detectPotentialDrift(registry, plan, refs) {
156
175
  }
157
176
  return issues;
158
177
  }
178
+ /**
179
+ * Whether a param has a reading as a run of words inside a sentence.
180
+ *
181
+ * A scalar does; a list of scalars does, as a comma-separated series. Anything
182
+ * deeper does not — `render` would have to choose a punctuation for structure,
183
+ * and every choice is a way for the document to say something the registry did
184
+ * not. `null` is a scalar here: it renders as `null`, which is the value.
185
+ */
186
+ function interpolatable(value) {
187
+ const isScalar = (v) => v === null || typeof v !== 'object';
188
+ return Array.isArray(value) ? value.every(isScalar) : isScalar(value);
189
+ }
190
+ /** How to name the offending shape in the diagnostic, in the author's terms. */
191
+ function shapeOf(value) {
192
+ if (Array.isArray(value))
193
+ return 'a list with a structured element';
194
+ return 'an object';
195
+ }
159
196
  //# sourceMappingURL=validator.js.map
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "@am_shork/attest",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "TDD-native spec framework: tests are the source of truth for verification, ID-bound requirements the source of truth for intent.",
5
5
  "type": "module",
6
+ "packageManager": "pnpm@10.28.0",
6
7
  "engines": {
7
8
  "node": ">=20.19"
8
9
  },
@@ -36,6 +37,24 @@
36
37
  "publishConfig": {
37
38
  "access": "public"
38
39
  },
40
+ "scripts": {
41
+ "clean": "node -e \"require('fs').rmSync('dist', {recursive: true, force: true})\"",
42
+ "build": "pnpm run clean && tsc -p tsconfig.json",
43
+ "typecheck": "tsc -p tsconfig.json --noEmit",
44
+ "typecheck:all": "tsc -p tsconfig.typecheck.json",
45
+ "test": "vitest run",
46
+ "test:watch": "vitest",
47
+ "test:consumer": "pnpm run build && vitest run --config vitest.consumer.config.ts",
48
+ "lint": "eslint .",
49
+ "prepack": "pnpm run build",
50
+ "prepublishOnly": "pnpm run test:consumer",
51
+ "attest": "node bin/attest.js",
52
+ "check:self": "node bin/attest.js check self",
53
+ "verify:self": "node bin/attest.js verify self",
54
+ "cover:self": "node bin/attest.js cover self",
55
+ "render:self": "node bin/attest.js render self --out self/requirements/SPEC.md",
56
+ "render:self:check": "node bin/attest.js render self --out self/requirements/SPEC.md --check"
57
+ },
39
58
  "keywords": [
40
59
  "tdd",
41
60
  "spec",
@@ -62,20 +81,13 @@
62
81
  "vite": "^8.1.5",
63
82
  "vitest": "^4.1.10"
64
83
  },
65
- "scripts": {
66
- "clean": "node -e \"require('fs').rmSync('dist', {recursive: true, force: true})\"",
67
- "build": "pnpm run clean && tsc -p tsconfig.json",
68
- "typecheck": "tsc -p tsconfig.json --noEmit",
69
- "typecheck:all": "tsc -p tsconfig.typecheck.json",
70
- "test": "vitest run",
71
- "test:watch": "vitest",
72
- "test:consumer": "pnpm run build && vitest run --config vitest.consumer.config.ts",
73
- "lint": "eslint .",
74
- "attest": "node bin/attest.js",
75
- "check:self": "node bin/attest.js check self",
76
- "verify:self": "node bin/attest.js verify self",
77
- "cover:self": "node bin/attest.js cover self",
78
- "render:self": "node bin/attest.js render self --out self/requirements/SPEC.md",
79
- "render:self:check": "node bin/attest.js render self --out self/requirements/SPEC.md --check"
84
+ "pnpm": {
85
+ "onlyBuiltDependencies": [
86
+ "esbuild"
87
+ ],
88
+ "overrides": {
89
+ "brace-expansion@<5.0.9": ">=5.0.9",
90
+ "js-yaml@<4.3.1": ">=4.3.1"
91
+ }
80
92
  }
81
- }
93
+ }