@sun-asterisk/sungen 3.3.0-alpha.2 → 3.3.0-alpha.3

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.
Files changed (85) hide show
  1. package/dist/cli/commands/contract.d.ts +3 -0
  2. package/dist/cli/commands/contract.d.ts.map +1 -1
  3. package/dist/cli/commands/contract.js +34 -4
  4. package/dist/cli/commands/contract.js.map +1 -1
  5. package/dist/contract/accept-opaque-steps.d.ts +3 -0
  6. package/dist/contract/accept-opaque-steps.d.ts.map +1 -0
  7. package/dist/contract/accept-opaque-steps.js +114 -0
  8. package/dist/contract/accept-opaque-steps.js.map +1 -0
  9. package/dist/contract/approve-contract.d.ts +8 -1
  10. package/dist/contract/approve-contract.d.ts.map +1 -1
  11. package/dist/contract/approve-contract.js +6 -0
  12. package/dist/contract/approve-contract.js.map +1 -1
  13. package/dist/contract/build-contract-verdict.js +2 -2
  14. package/dist/contract/build-contract-verdict.js.map +1 -1
  15. package/dist/contract/build-trace-conformance.d.ts +5 -1
  16. package/dist/contract/build-trace-conformance.d.ts.map +1 -1
  17. package/dist/contract/build-trace-conformance.js +9 -4
  18. package/dist/contract/build-trace-conformance.js.map +1 -1
  19. package/dist/contract/classify-contract-change.d.ts +3 -2
  20. package/dist/contract/classify-contract-change.d.ts.map +1 -1
  21. package/dist/contract/classify-contract-change.js.map +1 -1
  22. package/dist/contract/classify-trace-conformance.d.ts +18 -3
  23. package/dist/contract/classify-trace-conformance.d.ts.map +1 -1
  24. package/dist/contract/classify-trace-conformance.js +64 -17
  25. package/dist/contract/classify-trace-conformance.js.map +1 -1
  26. package/dist/contract/contract-schema.d.ts +21 -0
  27. package/dist/contract/contract-schema.d.ts.map +1 -1
  28. package/dist/contract/contract-schema.js +25 -3
  29. package/dist/contract/contract-schema.js.map +1 -1
  30. package/dist/contract/contracts-lock-file.d.ts.map +1 -1
  31. package/dist/contract/contracts-lock-file.js +1 -0
  32. package/dist/contract/contracts-lock-file.js.map +1 -1
  33. package/dist/contract/project-contract-blockers.d.ts.map +1 -1
  34. package/dist/contract/project-contract-blockers.js +4 -2
  35. package/dist/contract/project-contract-blockers.js.map +1 -1
  36. package/dist/contract/record-contract-approval.d.ts +16 -1
  37. package/dist/contract/record-contract-approval.d.ts.map +1 -1
  38. package/dist/contract/record-contract-approval.js +29 -1
  39. package/dist/contract/record-contract-approval.js.map +1 -1
  40. package/dist/contract/render-free-gherkin.d.ts +2 -0
  41. package/dist/contract/render-free-gherkin.d.ts.map +1 -1
  42. package/dist/contract/render-free-gherkin.js +6 -0
  43. package/dist/contract/render-free-gherkin.js.map +1 -1
  44. package/dist/contract/trace/collect-trace-evidence.d.ts +8 -3
  45. package/dist/contract/trace/collect-trace-evidence.d.ts.map +1 -1
  46. package/dist/contract/trace/collect-trace-evidence.js +65 -34
  47. package/dist/contract/trace/collect-trace-evidence.js.map +1 -1
  48. package/dist/contract/trace/read-file-once.d.ts +20 -0
  49. package/dist/contract/trace/read-file-once.d.ts.map +1 -0
  50. package/dist/contract/trace/read-file-once.js +135 -0
  51. package/dist/contract/trace/read-file-once.js.map +1 -0
  52. package/dist/contract/trace/select-final-attempt.d.ts +2 -2
  53. package/dist/contract/trace/select-final-attempt.d.ts.map +1 -1
  54. package/dist/contract/trace/select-final-attempt.js +3 -3
  55. package/dist/contract/trace/select-final-attempt.js.map +1 -1
  56. package/dist/contract/trace/trace-file-reader.d.ts +7 -1
  57. package/dist/contract/trace/trace-file-reader.d.ts.map +1 -1
  58. package/dist/contract/trace/trace-file-reader.js +17 -41
  59. package/dist/contract/trace/trace-file-reader.js.map +1 -1
  60. package/dist/contract/trace/trace-run-record.d.ts +2 -2
  61. package/dist/contract/trace/trace-run-record.d.ts.map +1 -1
  62. package/dist/contract/trace/trace-run-record.js +3 -3
  63. package/dist/contract/trace/trace-run-record.js.map +1 -1
  64. package/dist/contract/verify-approval-decisions.d.ts.map +1 -1
  65. package/dist/contract/verify-approval-decisions.js +9 -4
  66. package/dist/contract/verify-approval-decisions.js.map +1 -1
  67. package/package.json +3 -3
  68. package/src/cli/commands/contract.ts +35 -4
  69. package/src/contract/accept-opaque-steps.ts +114 -0
  70. package/src/contract/approve-contract.ts +10 -3
  71. package/src/contract/build-contract-verdict.ts +2 -2
  72. package/src/contract/build-trace-conformance.ts +8 -3
  73. package/src/contract/classify-contract-change.ts +3 -3
  74. package/src/contract/classify-trace-conformance.ts +76 -22
  75. package/src/contract/contract-schema.ts +31 -2
  76. package/src/contract/contracts-lock-file.ts +1 -0
  77. package/src/contract/project-contract-blockers.ts +4 -2
  78. package/src/contract/record-contract-approval.ts +41 -1
  79. package/src/contract/render-free-gherkin.ts +5 -0
  80. package/src/contract/trace/collect-trace-evidence.ts +66 -29
  81. package/src/contract/trace/read-file-once.ts +95 -0
  82. package/src/contract/trace/select-final-attempt.ts +3 -3
  83. package/src/contract/trace/trace-file-reader.ts +17 -6
  84. package/src/contract/trace/trace-run-record.ts +3 -3
  85. package/src/contract/verify-approval-decisions.ts +9 -5
@@ -12,7 +12,7 @@
12
12
  * a part without one is refused. Every JSONL line must parse — a line that does not is never skipped.
13
13
  */
14
14
  import { createHash } from 'crypto';
15
- import * as fs from 'fs';
15
+ import { FileRead, readFileOnce } from './read-file-once';
16
16
  import { openZip, ZipArchive } from './trace-zip-reader';
17
17
  import { SUPPORTED_TRACE_VERSIONS, traceZipLimits, TraceRejected, TraceZipLimits } from './trace-zip-limits';
18
18
 
@@ -124,10 +124,21 @@ export function readTraceBuffer(buf: Buffer, label: string, limits: TraceZipLimi
124
124
  };
125
125
  }
126
126
 
127
- /** Read a trace from disk: a regular file, size-checked BEFORE it is read into memory. */
127
+ /**
128
+ * A trace opened once (read-file-once.ts) → parsed: the size was checked on the descriptor BEFORE it
129
+ * was read into memory. `missing` / `outside` are the caller's to report; here they throw.
130
+ */
131
+ export function traceFromRead(read: FileRead, label: string, limits: TraceZipLimits = traceZipLimits()): ParsedTrace {
132
+ switch (read.status) {
133
+ case 'read': return readTraceBuffer(read.bytes, read.real, limits);
134
+ case 'too-large': throw new TraceRejected('zip.too-large', `${read.size} B > ${limits.maxZipBytes}`);
135
+ case 'not-a-file': throw new TraceRejected('zip.malformed', `${label} is not a regular file`);
136
+ case 'missing': throw new TraceRejected('zip.malformed', `${label} does not exist`);
137
+ default: throw new TraceRejected('zip.malformed', `${label} was not read: ${read.why}`);
138
+ }
139
+ }
140
+
141
+ /** Read a trace from a trusted path (a fixture, a test): a regular file, opened once. */
128
142
  export function readTraceFile(file: string, limits: TraceZipLimits = traceZipLimits()): ParsedTrace {
129
- const st = fs.statSync(file);
130
- if (!st.isFile()) throw new TraceRejected('zip.malformed', `${file} is not a regular file`);
131
- if (st.size > limits.maxZipBytes) throw new TraceRejected('zip.too-large', `${st.size} B > ${limits.maxZipBytes}`);
132
- return readTraceBuffer(fs.readFileSync(file), file, limits);
143
+ return traceFromRead(readFileOnce(file, limits.maxZipBytes), file, limits);
133
144
  }
@@ -60,10 +60,10 @@ function attemptOf(raw: unknown, i: number): RunRecordAttempt {
60
60
  return { testId, file, title, retry: retry as number, status, startTime, duration, trace: trace as RunRecordAttempt['trace'] };
61
61
  }
62
62
 
63
- /** Read and check one record; throws an Error naming what is wrong. */
64
- export function readRunRecord(file: string): RunRecord {
63
+ /** Read and check one record (`text`: its bytes, already read once by the caller); throws an Error naming what is wrong. */
64
+ export function readRunRecord(file: string, text?: string): RunRecord {
65
65
  let raw: unknown;
66
- try { raw = JSON.parse(fs.readFileSync(file, 'utf8')); } catch (e) { throw new Error(`not readable JSON (${(e as Error).message.split('\n')[0]})`); }
66
+ try { raw = JSON.parse(text ?? fs.readFileSync(file, 'utf8')); } catch (e) { throw new Error(`not readable JSON (${(e as Error).message.split('\n')[0]})`); }
67
67
  if (!isObj(raw)) throw new Error('not an object');
68
68
  const extra = Object.keys(raw).find((k) => !TOP.has(k));
69
69
  if (extra) throw new Error(`unknown key "${extra}"`);
@@ -2,9 +2,10 @@
2
2
  * Every approved lock entry must be vouched for by its decision (DOS-13.2, DOS-14.4, review M1):
3
3
  * `qa/<kind>/<unit>/decisions/<decisionId>.yaml`, COMMITTED in git HEAD (outside git: on disk),
4
4
  * intact (schema, id = content, entry_hash = entry), of kind `contract-approval`, whose payload lists
5
- * this scenario at this hash, and whose `valid_from` is the entry's `approvedAt`. A lock line copied
6
- * from elsewhere, re-pointed at another decision, or backdated fails here — `unverified`, reported
7
- * under contract.lock-uncommitted (hard).
5
+ * this scenario at this hash — with exactly the opaque steps the entry says QA accepted (#923) — and
6
+ * whose `valid_from` is the entry's `approvedAt`. A lock line copied from elsewhere, re-pointed at
7
+ * another decision, backdated, or given an acceptance nobody recorded fails here — `unverified`,
8
+ * reported under contract.lock-uncommitted (hard).
8
9
  *
9
10
  * Only entries that are otherwise identical to HEAD are checked: an entry already added, changed or
10
11
  * tampered is a hard finding on its own.
@@ -14,6 +15,7 @@ import { computeDecisionId, computeEntryHash } from '../decisions/decision-ident
14
15
  import { DecisionEntry, storedEntryErrors } from '../decisions/decision-schema';
15
16
  import { safeYamlParse } from '../utils/safe-yaml-parse';
16
17
  import { HeadSnapshot, headFileText, posixRel, readTextOrNull } from './contract-git';
18
+ import { sameJson } from './contract-hash';
17
19
  import { ContractLock, isHumanApproved, LockEntry } from './contract-schema';
18
20
  import type { UncommittedEntry } from './lock-vs-git-head';
19
21
 
@@ -38,8 +40,10 @@ function loadDecision(projectRoot: string, unitDir: string, id: string, snap: He
38
40
 
39
41
  function mismatch(scenario: string, lockEntry: LockEntry, d: DecisionEntry): string | null {
40
42
  if (d.kind !== 'contract-approval') return `decision ${d.id} is a ${d.kind}, not a contract-approval`;
41
- const listed = Array.isArray(d.payload?.scenarios) ? d.payload!.scenarios as Array<{ id?: unknown; hash?: unknown }> : [];
42
- if (!listed.some((s) => s && s.id === scenario && s.hash === lockEntry.hash)) return `decision ${d.id} does not approve ${scenario} at hash ${lockEntry.hash}`;
43
+ const listed = Array.isArray(d.payload?.scenarios) ? d.payload!.scenarios as Array<{ id?: unknown; hash?: unknown; acceptedOpaque?: unknown }> : [];
44
+ const item = listed.find((s) => s && s.id === scenario && s.hash === lockEntry.hash);
45
+ if (!item) return `decision ${d.id} does not approve ${scenario} at hash ${lockEntry.hash}`;
46
+ if (!sameJson(item.acceptedOpaque ?? [], lockEntry.acceptedOpaque ?? [])) return `decision ${d.id} does not accept the opaque step(s) the lock entry of ${scenario} lists (acceptedOpaque)`;
43
47
  if (Date.parse(lockEntry.approvedAt) !== Date.parse(d.valid_from)) return `approvedAt ${lockEntry.approvedAt} is not decision ${d.id}'s valid_from ${d.valid_from}`;
44
48
  return null;
45
49
  }