@am_shork/attest 0.7.0 → 0.7.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.
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "@am_shork/attest",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
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
6
  "engines": {