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

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 (109) 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/dist/decisions/git-head-trust.d.ts +12 -0
  68. package/dist/decisions/git-head-trust.d.ts.map +1 -1
  69. package/dist/decisions/git-head-trust.js +52 -0
  70. package/dist/decisions/git-head-trust.js.map +1 -1
  71. package/dist/decisions/legacy-waiver-migration.d.ts +6 -1
  72. package/dist/decisions/legacy-waiver-migration.d.ts.map +1 -1
  73. package/dist/decisions/legacy-waiver-migration.js +8 -2
  74. package/dist/decisions/legacy-waiver-migration.js.map +1 -1
  75. package/dist/harness/gates/in-run-decision-check.d.ts.map +1 -1
  76. package/dist/harness/gates/in-run-decision-check.js +19 -11
  77. package/dist/harness/gates/in-run-decision-check.js.map +1 -1
  78. package/dist/harness/gates/migrated-waiver-import-bounds.d.ts +13 -0
  79. package/dist/harness/gates/migrated-waiver-import-bounds.d.ts.map +1 -0
  80. package/dist/harness/gates/migrated-waiver-import-bounds.js +141 -0
  81. package/dist/harness/gates/migrated-waiver-import-bounds.js.map +1 -0
  82. package/dist/harness/gates/migrated-waiver-review.d.ts +14 -4
  83. package/dist/harness/gates/migrated-waiver-review.d.ts.map +1 -1
  84. package/dist/harness/gates/migrated-waiver-review.js +78 -80
  85. package/dist/harness/gates/migrated-waiver-review.js.map +1 -1
  86. package/package.json +3 -3
  87. package/src/cli/commands/contract.ts +35 -4
  88. package/src/contract/accept-opaque-steps.ts +114 -0
  89. package/src/contract/approve-contract.ts +10 -3
  90. package/src/contract/build-contract-verdict.ts +2 -2
  91. package/src/contract/build-trace-conformance.ts +8 -3
  92. package/src/contract/classify-contract-change.ts +3 -3
  93. package/src/contract/classify-trace-conformance.ts +76 -22
  94. package/src/contract/contract-schema.ts +31 -2
  95. package/src/contract/contracts-lock-file.ts +1 -0
  96. package/src/contract/project-contract-blockers.ts +4 -2
  97. package/src/contract/record-contract-approval.ts +41 -1
  98. package/src/contract/render-free-gherkin.ts +5 -0
  99. package/src/contract/trace/collect-trace-evidence.ts +66 -29
  100. package/src/contract/trace/read-file-once.ts +95 -0
  101. package/src/contract/trace/select-final-attempt.ts +3 -3
  102. package/src/contract/trace/trace-file-reader.ts +17 -6
  103. package/src/contract/trace/trace-run-record.ts +3 -3
  104. package/src/contract/verify-approval-decisions.ts +9 -5
  105. package/src/decisions/git-head-trust.ts +44 -0
  106. package/src/decisions/legacy-waiver-migration.ts +9 -2
  107. package/src/harness/gates/in-run-decision-check.ts +16 -12
  108. package/src/harness/gates/migrated-waiver-import-bounds.ts +103 -0
  109. package/src/harness/gates/migrated-waiver-review.ts +90 -37
@@ -0,0 +1,141 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.migratedWaiverRemedy = void 0;
37
+ exports.migratedWaiverImportBounds = migratedWaiverImportBounds;
38
+ /**
39
+ * Could a migrated waiver's legacy record have been the source of the import that wrote it? (#925)
40
+ *
41
+ * The one-time import (legacy-waiver-migration.ts) reads a record git HEAD ALREADY holds, and only
42
+ * before 3.3 takes over. So a genuine record was committed strictly before each of:
43
+ * 1. the decision file's first appearance — its first commit, else its creation on disk;
44
+ * 2. the unit's import marker `.sungen/migrations/<slug>.waivers-imported.json` — the commit that
45
+ * added the one HEAD holds (an agent can delete or recreate the file, not that commit), else its
46
+ * creation on disk; once it exists the import never reads that unit's legacy file again;
47
+ * 3. the project's adoption of 3.3 — the first commit that added any 3.3-only file: an import marker
48
+ * or other `.sungen/migrations/` file, a decision file (`qa/**\/decisions/*.yaml`) or a contract lock
49
+ * (`qa/**\/contracts.lock.yaml`). Only the 3.3 CLI writes them, and only committed ones count: a
50
+ * legacy waiver is one written before the project's first reviewed 3.3 change. Mixed versions pay
51
+ * for it: a waiver a pre-3.3 CLI writes after that commit must be re-confirmed on a terminal.
52
+ * A record failing any bound claims a migration that never happened: the decision is in-run.
53
+ *
54
+ * What stays open (the limit of any time-based check, as for ordinary decisions): a record forged and
55
+ * committed before the project's first 3.3 commit — indistinguishable from a real legacy waiver, as on
56
+ * a project adopting 3.3 for the very first time — and commit times forged with GIT_COMMITTER_DATE.
57
+ */
58
+ const fs = __importStar(require("fs"));
59
+ const path = __importStar(require("path"));
60
+ const git_head_trust_1 = require("../../decisions/git-head-trust");
61
+ const human_confirmation_1 = require("../../decisions/human-confirmation");
62
+ const legacy_waiver_migration_1 = require("../../decisions/legacy-waiver-migration");
63
+ /** Files only the 3.3 CLI writes, as pathspecs relative to the project. */
64
+ const ADOPTION_PATHSPECS = ['.sungen/migrations', ':(glob)qa/**/decisions/*.yaml', ':(glob)qa/**/contracts.lock.yaml'];
65
+ /**
66
+ * What a human does about a migrated waiver its legacy record cannot vouch for: record it anew on a
67
+ * terminal (a `cli` decision that supersedes it — the superseded one is no longer judged), or accept it
68
+ * for one delivery.
69
+ */
70
+ const migratedWaiverRemedy = (scope, ob) => `re-confirm it on a terminal: ${(0, human_confirmation_1.formatCommand)(['sungen', 'journey', '--screen', scope.split('/').pop() ?? scope, '--waive', ob, '--reason', '…'])}, `
71
+ + 'then commit the new decision; or accept it once with `sungen delivery --accept-in-run`';
72
+ exports.migratedWaiverRemedy = migratedWaiverRemedy;
73
+ const validMs = (iso) => (iso && !Number.isNaN(Date.parse(iso)) ? Date.parse(iso) : null);
74
+ const createdMs = (abs) => {
75
+ try {
76
+ const s = fs.statSync(abs);
77
+ return s.birthtimeMs || s.mtimeMs;
78
+ }
79
+ catch {
80
+ return null;
81
+ }
82
+ };
83
+ /** One run's bounds: each decision's first appearance, each unit's marker and the project's adoption, read once. */
84
+ function migratedWaiverImportBounds(projectRoot) {
85
+ const relOf = (abs) => path.relative(projectRoot, abs).split(path.sep).join('/');
86
+ const firstSeen = new Map();
87
+ const markers = new Map();
88
+ let adoption;
89
+ /** When a decision file first appeared: its first commit, else its creation (or last change) on disk. */
90
+ const decisionMs = (e) => {
91
+ if (!firstSeen.has(e.path)) {
92
+ const added = (0, git_head_trust_1.gitOutput)(projectRoot, ['log', '--diff-filter=A', '--format=%cI', '--', relOf(e.path)])?.trim().split('\n').filter(Boolean).pop();
93
+ firstSeen.set(e.path, validMs(added) ?? createdMs(e.path) ?? Number.NEGATIVE_INFINITY);
94
+ }
95
+ return firstSeen.get(e.path);
96
+ };
97
+ const markerMs = (slug) => {
98
+ if (!markers.has(slug)) {
99
+ const abs = (0, legacy_waiver_migration_1.importMarkerPath)(projectRoot, slug);
100
+ const added = (0, git_head_trust_1.committedText)(projectRoot, abs) !== null
101
+ ? validMs((0, git_head_trust_1.gitOutput)(projectRoot, ['log', '-1', '--diff-filter=A', '--format=%cI', '--', relOf(abs)])?.trim()) : null;
102
+ markers.set(slug, added ?? createdMs(abs));
103
+ }
104
+ return markers.get(slug);
105
+ };
106
+ const adoptionMs = () => {
107
+ if (adoption === undefined) {
108
+ const added = ((0, git_head_trust_1.gitOutput)(projectRoot, ['log', '--diff-filter=A', '--format=%cI', '--', ...ADOPTION_PATHSPECS]) ?? '')
109
+ .split('\n').map((l) => validMs(l.trim())).filter((t) => t !== null);
110
+ adoption = added.length ? Math.min(...added) : null;
111
+ }
112
+ return adoption;
113
+ };
114
+ /** The first bound a record committed at `legacy` (ms) crosses, or undefined. */
115
+ const boundCrossed = (slug, e, legacy) => {
116
+ const decision = decisionMs(e);
117
+ if (legacy > decision)
118
+ return 'after this decision first appeared';
119
+ if (legacy === decision)
120
+ return 'when this decision first appeared, not before it — an import only reads a record already committed';
121
+ const marker = markerMs(slug);
122
+ if (marker !== null && legacy >= marker) {
123
+ return `after this unit's waivers were imported (${relOf((0, legacy_waiver_migration_1.importMarkerPath)(projectRoot, slug))}, ${new Date(marker).toISOString()}) `
124
+ + '— from then on the import never reads the legacy file';
125
+ }
126
+ const adopted = adoptionMs();
127
+ if (adopted !== null && legacy >= adopted) {
128
+ return `after this project adopted sungen 3.3 (${new Date(adopted).toISOString()}: the first commit adding a .sungen/migrations/ file, `
129
+ + 'a decision or a contract lock) — a legacy waiver is one written before that';
130
+ }
131
+ return undefined;
132
+ };
133
+ return {
134
+ /** Why a record committed at `at` (state file of `slug`) cannot have been the source of `e`'s import; undefined when it can. */
135
+ notImported(slug, e, at) {
136
+ const why = boundCrossed(slug, e, Date.parse(at));
137
+ return why && `migrated: its legacy record was committed at ${at}, ${why}; ${(0, exports.migratedWaiverRemedy)(e.scope, String(e.payload?.obligation))}`;
138
+ },
139
+ };
140
+ }
141
+ //# sourceMappingURL=migrated-waiver-import-bounds.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrated-waiver-import-bounds.js","sourceRoot":"","sources":["../../../src/harness/gates/migrated-waiver-import-bounds.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,16 @@
1
1
  import type { ReadDecision } from '../../decisions/read-decisions';
2
- /** When a migrated waiver's legacy record was committed, or null when nothing in git HEAD vouches for it. */
3
- export declare function migratedWaiverReviewedAt(projectRoot: string, scope: string, e: ReadDecision): string | null;
4
- /** When a decision file first appeared: its first commit, else its creation (or last change) on disk. */
5
- export declare function decisionFirstSeenMs(projectRoot: string, rel: string, abs: string): number;
2
+ /** A migrated waiver HEAD's legacy file holds: when it was reviewed, or why that cannot be said. */
3
+ export interface MigratedReview {
4
+ /** When its legacy record was committed; null when that commit is out of reach (see `unvouched`). */
5
+ at: string | null;
6
+ /** Set with `at`: why the record cannot have been the import's source — the decision is in-run. */
7
+ notImported?: string;
8
+ /** Set when `at` is null: why the record vouches for nothing — the decision is judged as any other. */
9
+ unvouched?: string;
10
+ }
11
+ /** One run's reads for migrated waivers: each state file's history, decision's first commit and import marker, read once. */
12
+ export declare function migratedWaiverReview(projectRoot: string): {
13
+ /** How a migrated waiver was reviewed; null when it is not one HEAD's legacy file holds (judged as any other). */
14
+ reviewOf(scope: string, e: ReadDecision): MigratedReview | null;
15
+ };
6
16
  //# sourceMappingURL=migrated-waiver-review.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"migrated-waiver-review.d.ts","sourceRoot":"","sources":["../../../src/harness/gates/migrated-waiver-review.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AA6BnE,6GAA6G;AAC7G,wBAAgB,wBAAwB,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,EAAE,YAAY,GAAG,MAAM,GAAG,IAAI,CAY3G;AAED,yGAAyG;AACzG,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAIzF"}
1
+ {"version":3,"file":"migrated-waiver-review.d.ts","sourceRoot":"","sources":["../../../src/harness/gates/migrated-waiver-review.ts"],"names":[],"mappings":"AA2BA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAYnE,oGAAoG;AACpG,MAAM,WAAW,cAAc;IAC7B,qGAAqG;IACrG,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAClB,mGAAmG;IACnG,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uGAAuG;IACvG,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA4CD,6HAA6H;AAC7H,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM;IAWpD,kHAAkH;oBAClG,MAAM,KAAK,YAAY,GAAG,cAAc,GAAG,IAAI;EAgBlE"}
@@ -1,40 +1,6 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
2
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.migratedWaiverReviewedAt = migratedWaiverReviewedAt;
37
- exports.decisionFirstSeenMs = decisionFirstSeenMs;
3
+ exports.migratedWaiverReview = migratedWaiverReview;
38
4
  /**
39
5
  * When a MIGRATED waiver was reviewed (DOS-13.5, phase 09) — for `decision.in-run`.
40
6
  *
@@ -44,18 +10,28 @@ exports.decisionFirstSeenMs = decisionFirstSeenMs;
44
10
  *
45
11
  * vouched HEAD's state file holds the record (same reason, `at`, auditHashAtWaive) — reviewedAt is
46
12
  * the commit time of the oldest commit in the unbroken run, back from HEAD, that holds it
47
- * with a run start in-run when reviewedAt is after it (the same test as any committed decision)
48
- * without one in-run when reviewedAt is after the decision file first appeared (its first
49
- * commit, else its creation time): a genuine import always follows the commit it read
13
+ * imported the record could have been the import's source (#925) — always checked, run start or not:
14
+ * committed strictly before the decision file, the unit's import marker and the project's
15
+ * adoption of 3.3 first appeared (migrated-waiver-import-bounds.ts). Failing one, the decision
16
+ * claims a migration that never happened → in-run, even at `gate --phase deliver`, which has
17
+ * no run start.
18
+ * with a run start also in-run when reviewedAt is after it (the same test as any committed decision)
50
19
  * Anything else — no git, no such record in HEAD, a record that differs — is not vouched for: the
51
- * decision is judged as any other (`not in git HEAD`, or its own commit time).
20
+ * decision is judged as any other (`not in git HEAD`, or its own commit time). Only a rewritten history
21
+ * (a squash or rebase that folds the record's commit into the import's) can fail a genuine import; the
22
+ * finding says which bound it crossed.
23
+ *
24
+ * Nor is a record whose introducing commit is out of reach — judged as any other, with `unvouched`
25
+ * saying why, never vouched for by a later commit taken for its introduction: the walk reads at most
26
+ * MAX_COMMITS commits of a state file and the record is held by the commit before them too (#924), or
27
+ * the record reaches a shallow clone's boundary commit. One review reads each state file's history
28
+ * once (one `git log`, one `git cat-file --batch`), however many waivers share it.
52
29
  */
53
- const fs = __importStar(require("fs"));
54
- const path = __importStar(require("path"));
55
30
  const git_head_trust_1 = require("../../decisions/git-head-trust");
56
31
  const waiver_decisions_1 = require("../../decisions/waiver-decisions");
57
32
  const unit_paths_1 = require("../unit-paths");
58
- /** Commits of one state file the walk reads at most (each is one `git show`). */
33
+ const migrated_waiver_import_bounds_1 = require("./migrated-waiver-import-bounds");
34
+ /** Commits of one state file the walk reads at most. */
59
35
  const MAX_COMMITS = 200;
60
36
  const parse = (text) => {
61
37
  if (text === null)
@@ -71,46 +47,68 @@ function holds(state, e, ob) {
71
47
  const w = state?.waivers?.[ob];
72
48
  return (0, waiver_decisions_1.isLegacyWaiver)(w) && w.at === e.valid_from && (0, waiver_decisions_1.sameWaiver)(w, (0, waiver_decisions_1.asLegacyWaiver)(e));
73
49
  }
74
- /** The commit time of the oldest commit, in the unbroken run back from HEAD, whose state file holds the record. */
75
- function introducedAt(projectRoot, rel, e, ob) {
76
- const log = (0, git_head_trust_1.gitOutput)(projectRoot, ['log', `--max-count=${MAX_COMMITS}`, '--format=%H %cI', '--', rel]) ?? '';
77
- let oldest = null;
78
- for (const line of log.split('\n').filter(Boolean)) {
79
- const [sha, at] = line.split(' ');
80
- if (!holds(parse((0, git_head_trust_1.gitOutput)(projectRoot, ['show', `${sha}:./${rel}`])), e, ob))
81
- break;
82
- oldest = at;
83
- }
84
- return oldest && !Number.isNaN(Date.parse(oldest)) ? oldest : null;
85
- }
86
- /** When a migrated waiver's legacy record was committed, or null when nothing in git HEAD vouches for it. */
87
- function migratedWaiverReviewedAt(projectRoot, scope, e) {
88
- const ob = e.payload?.obligation;
89
- if (e.kind !== 'waiver' || e.source?.where !== 'migrated' || typeof ob !== 'string')
90
- return null;
91
- // The journey keys its state by the unit name it was given: `<name>`, or the catalog id for api units.
92
- const slugs = new Set([scope.split('/').pop() ?? '', (0, unit_paths_1.reportSlug)(scope.replace(/^(screens|flows)\//, ''))]);
93
- for (const slug of slugs) {
94
- const file = path.join(projectRoot, '.sungen', 'journey', `${slug}.state.json`);
95
- if (!holds(parse((0, git_head_trust_1.committedText)(projectRoot, file)), e, ob))
96
- continue;
97
- const at = introducedAt(projectRoot, path.relative(projectRoot, file).split(path.sep).join('/'), e, ob);
98
- if (at)
99
- return at;
100
- }
101
- return null;
50
+ function readHistory(projectRoot, rel) {
51
+ const log = ((0, git_head_trust_1.gitOutput)(projectRoot, ['log', `--max-count=${MAX_COMMITS + 1}`, '--format=%H %cI %P', '--', rel]) ?? '')
52
+ .split('\n').filter(Boolean).map((line) => line.trim().split(' '));
53
+ const texts = (0, git_head_trust_1.gitFileTexts)(projectRoot, [`HEAD:./${rel}`, ...log.map(([sha]) => `${sha}:./${rel}`)]);
54
+ return {
55
+ rel,
56
+ head: parse(texts?.[0] ?? null),
57
+ commits: log.map(([, at, ...parents], i) => ({ at, state: parse(texts?.[i + 1] ?? null), root: parents.length === 0 })),
58
+ };
102
59
  }
103
- /** When a decision file first appeared: its first commit, else its creation (or last change) on disk. */
104
- function decisionFirstSeenMs(projectRoot, rel, abs) {
105
- const added = (0, git_head_trust_1.gitOutput)(projectRoot, ['log', '--diff-filter=A', '--format=%cI', '--', rel])?.trim().split('\n').filter(Boolean).pop();
106
- if (added && !Number.isNaN(Date.parse(added)))
107
- return Date.parse(added);
108
- try {
109
- const s = fs.statSync(abs);
110
- return s.birthtimeMs || s.mtimeMs;
60
+ /**
61
+ * The commit time of the oldest commit, in the unbroken run back from HEAD, whose state file holds the
62
+ * record. The walk reads MAX_COMMITS commits; the next one only says whether the last of them introduced it.
63
+ * In a shallow clone a commit with no parent shown is the boundary: every file seems to appear there, so a
64
+ * record reaching it has no known commit time.
65
+ */
66
+ function introducedAt(h, e, ob, scope, shallow) {
67
+ let run = 0;
68
+ while (run < h.commits.length && holds(h.commits[run].state, e, ob))
69
+ run++;
70
+ const unvouched = (why) => ({ at: null, unvouched: `migrated, but its legacy record cannot vouch for it: ${why}` });
71
+ if (run > MAX_COMMITS) {
72
+ return unvouched(`${h.rel} changed in more than ${MAX_COMMITS} commits, and the commit that introduced the record is older than all of them`);
111
73
  }
112
- catch {
113
- return Number.NEGATIVE_INFINITY;
74
+ const oldest = run ? h.commits[run - 1] : null;
75
+ if (oldest?.root && shallow()) {
76
+ return unvouched(`the git history is shallow, so the record's real commit time is unknown — fetch the full history `
77
+ + `(\`git fetch --unshallow\`; \`fetch-depth: 0\` in CI), or ${(0, migrated_waiver_import_bounds_1.migratedWaiverRemedy)(scope, ob)}`);
114
78
  }
79
+ return oldest && !Number.isNaN(Date.parse(oldest.at)) ? { at: oldest.at } : null;
80
+ }
81
+ /** One run's reads for migrated waivers: each state file's history, decision's first commit and import marker, read once. */
82
+ function migratedWaiverReview(projectRoot) {
83
+ const histories = new Map();
84
+ const bounds = (0, migrated_waiver_import_bounds_1.migratedWaiverImportBounds)(projectRoot);
85
+ let isShallow;
86
+ const shallow = () => (isShallow ?? (isShallow = (0, git_head_trust_1.isShallowRepository)(projectRoot)));
87
+ const historyOf = (rel) => {
88
+ if (!histories.has(rel))
89
+ histories.set(rel, readHistory(projectRoot, rel));
90
+ return histories.get(rel);
91
+ };
92
+ return {
93
+ /** How a migrated waiver was reviewed; null when it is not one HEAD's legacy file holds (judged as any other). */
94
+ reviewOf(scope, e) {
95
+ const ob = e.payload?.obligation;
96
+ if (e.kind !== 'waiver' || e.source?.where !== 'migrated' || typeof ob !== 'string')
97
+ return null;
98
+ // The journey keys its state by the unit name it was given: `<name>`, or the catalog id for api units.
99
+ const slugs = new Set([scope.split('/').pop() ?? '', (0, unit_paths_1.reportSlug)(scope.replace(/^(screens|flows)\//, ''))]);
100
+ let unvouched = null;
101
+ for (const slug of slugs) {
102
+ const h = historyOf(`.sungen/journey/${slug}.state.json`);
103
+ if (!holds(h.head, e, ob))
104
+ continue;
105
+ const review = introducedAt(h, e, ob, scope, shallow);
106
+ if (review?.at)
107
+ return { at: review.at, notImported: bounds.notImported(slug, e, review.at) };
108
+ unvouched = unvouched ?? review;
109
+ }
110
+ return unvouched;
111
+ },
112
+ };
115
113
  }
116
114
  //# sourceMappingURL=migrated-waiver-review.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"migrated-waiver-review.js","sourceRoot":"","sources":["../../../src/harness/gates/migrated-waiver-review.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"migrated-waiver-review.js","sourceRoot":"","sources":["../../../src/harness/gates/migrated-waiver-review.ts"],"names":[],"mappings":";;AA4FA,oDA4BC;AAxHD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,mEAA8F;AAE9F,uEAA8F;AAC9F,8CAA2C;AAC3C,mFAAmG;AAEnG,wDAAwD;AACxD,MAAM,WAAW,GAAG,GAAG,CAAC;AAgBxB,MAAM,KAAK,GAAG,CAAC,IAAmB,EAAS,EAAE;IAC3C,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAC/B,IAAI,CAAC;QAAC,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC;IAAC,CAAC;IAAC,MAAM,CAAC;QAAC,OAAO,IAAI,CAAC;IAAC,CAAC;AAC3E,CAAC,CAAC;AAEF,SAAS,KAAK,CAAC,KAAY,EAAE,CAAe,EAAE,EAAU;IACtD,MAAM,CAAC,GAAG,KAAK,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;IAC/B,OAAO,IAAA,iCAAc,EAAC,CAAC,CAAC,IAAI,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,UAAU,IAAI,IAAA,6BAAU,EAAC,CAAC,EAAE,IAAA,iCAAc,EAAC,CAAC,CAAC,CAAC,CAAC;AACxF,CAAC;AAED,SAAS,WAAW,CAAC,WAAmB,EAAE,GAAW;IACnD,MAAM,GAAG,GAAG,CAAC,IAAA,0BAAS,EAAC,WAAW,EAAE,CAAC,KAAK,EAAE,eAAe,WAAW,GAAG,CAAC,EAAE,EAAE,oBAAoB,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;SACnH,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;IACrE,MAAM,KAAK,GAAG,IAAA,6BAAY,EAAC,WAAW,EAAE,CAAC,UAAU,GAAG,EAAE,EAAE,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,MAAM,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC;IACrG,OAAO;QACL,GAAG;QACH,IAAI,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;QAC/B,OAAO,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC,CAAC;KACxH,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,SAAS,YAAY,CAAC,CAAe,EAAE,CAAe,EAAE,EAAU,EAAE,KAAa,EAAE,OAAsB;IACvG,IAAI,GAAG,GAAG,CAAC,CAAC;IACZ,OAAO,GAAG,GAAG,CAAC,CAAC,OAAO,CAAC,MAAM,IAAI,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC;QAAE,GAAG,EAAE,CAAC;IAC3E,MAAM,SAAS,GAAG,CAAC,GAAW,EAAkB,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,wDAAwD,GAAG,EAAE,EAAE,CAAC,CAAC;IAC5I,IAAI,GAAG,GAAG,WAAW,EAAE,CAAC;QACtB,OAAO,SAAS,CAAC,GAAG,CAAC,CAAC,GAAG,yBAAyB,WAAW,+EAA+E,CAAC,CAAC;IAChJ,CAAC;IACD,MAAM,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAC/C,IAAI,MAAM,EAAE,IAAI,IAAI,OAAO,EAAE,EAAE,CAAC;QAC9B,OAAO,SAAS,CAAC,mGAAmG;cAChH,6DAA6D,IAAA,oDAAoB,EAAC,KAAK,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC;IACtG,CAAC;IACD,OAAO,MAAM,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AACnF,CAAC;AAED,6HAA6H;AAC7H,SAAgB,oBAAoB,CAAC,WAAmB;IACtD,MAAM,SAAS,GAAG,IAAI,GAAG,EAAwB,CAAC;IAClD,MAAM,MAAM,GAAG,IAAA,0DAA0B,EAAC,WAAW,CAAC,CAAC;IACvD,IAAI,SAA8B,CAAC;IACnC,MAAM,OAAO,GAAG,GAAY,EAAE,CAAC,CAAC,SAAS,KAAT,SAAS,GAAK,IAAA,oCAAmB,EAAC,WAAW,CAAC,EAAC,CAAC;IAChF,MAAM,SAAS,GAAG,CAAC,GAAW,EAAgB,EAAE;QAC9C,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,SAAS,CAAC,GAAG,CAAC,GAAG,EAAE,WAAW,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC,CAAC;QAC3E,OAAO,SAAS,CAAC,GAAG,CAAC,GAAG,CAAE,CAAC;IAC7B,CAAC,CAAC;IAEF,OAAO;QACL,kHAAkH;QAClH,QAAQ,CAAC,KAAa,EAAE,CAAe;YACrC,MAAM,EAAE,GAAG,CAAC,CAAC,OAAO,EAAE,UAAU,CAAC;YACjC,IAAI,CAAC,CAAC,IAAI,KAAK,QAAQ,IAAI,CAAC,CAAC,MAAM,EAAE,KAAK,KAAK,UAAU,IAAI,OAAO,EAAE,KAAK,QAAQ;gBAAE,OAAO,IAAI,CAAC;YACjG,uGAAuG;YACvG,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,IAAA,uBAAU,EAAC,KAAK,CAAC,OAAO,CAAC,oBAAoB,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;YAC3G,IAAI,SAAS,GAA0B,IAAI,CAAC;YAC5C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;gBACzB,MAAM,CAAC,GAAG,SAAS,CAAC,mBAAmB,IAAI,aAAa,CAAC,CAAC;gBAC1D,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC;oBAAE,SAAS;gBACpC,MAAM,MAAM,GAAG,YAAY,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;gBACtD,IAAI,MAAM,EAAE,EAAE;oBAAE,OAAO,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,WAAW,EAAE,MAAM,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC,EAAE,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC;gBAC9F,SAAS,GAAG,SAAS,IAAI,MAAM,CAAC;YAClC,CAAC;YACD,OAAO,SAAS,CAAC;QACnB,CAAC;KACF,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sun-asterisk/sungen",
3
- "version": "3.3.0-alpha.2",
3
+ "version": "3.3.0-alpha.4",
4
4
  "description": "Deterministic E2E Test Compiler - Gherkin + Selectors → Playwright tests",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -40,8 +40,8 @@
40
40
  "@babel/types": "^7.28.5",
41
41
  "@cucumber/gherkin": "^37.0.0",
42
42
  "@cucumber/messages": "^31.0.0",
43
- "@sungen/driver-data-factory": "3.3.0-alpha.2",
44
- "@sungen/driver-ui": "3.3.0-alpha.2",
43
+ "@sungen/driver-data-factory": "3.3.0-alpha.4",
44
+ "@sungen/driver-ui": "3.3.0-alpha.4",
45
45
  "chalk": "^5.6.2",
46
46
  "commander": "^14.0.2",
47
47
  "dotenv": "^17.2.3",
@@ -4,6 +4,9 @@
4
4
  * approve --screen <u> [--scenario <id>] [--reason <text>]
5
5
  * a human types a confirmation on a TTY; writes contracts.lock.yaml + a contract-approval
6
6
  * decision. No TTY → refused with the exact command (exit 1).
7
+ * approve --screen <u> --scenario <id> --accept-opaque [--reason <text>]
8
+ * the same human act for the reviewed opaque trace steps of one approved free / impl:code
9
+ * scenario (#923): bound to each step as it reads now (accept-opaque-steps.ts).
7
10
  * check --screen <u> [--trace] [--json]
8
11
  * rebuilds the verdict from .feature + lock + git HEAD + recorded traces (never from a report),
9
12
  * writes .sungen/reports/<slug>-contract.json as output (secret-scanned first). Exit 2 on a hard
@@ -17,6 +20,7 @@ import * as fs from 'fs';
17
20
  import * as path from 'path';
18
21
  import { approveContract } from '../../contract/approve-contract';
19
22
  import { buildContractVerdict, ContractVerdict } from '../../contract/build-contract-verdict';
23
+ import { LOW_IDENTITY_FINDING } from '../../contract/classify-trace-conformance';
20
24
  import { contractReportPath, writeContractReport } from '../../contract/contract-report-file';
21
25
  import { HARD_CLASSES, REVIEW_CLASSES } from '../../contract/contract-schema';
22
26
  import { processIO } from '../../decisions/human-confirmation';
@@ -33,7 +37,13 @@ const MARK: Record<string, string> = {
33
37
  const CHECK_TRACE_BUDGET_MS = 120_000;
34
38
  const UNPROVEN_HINT = " → free / impl:code tests need their own config (outputDir 'test-results-free', trace: 'on', the sungen run-record reporter), then a fresh run (see `sungen contract derive`).";
35
39
 
36
- function printVerdict(v: ContractVerdict, reportFile: string): void {
40
+ /** The command that recompiles a unit's spec — `sungen generate --screen|--flow|--api <unit>`. */
41
+ function generateCommand(unit: { name: string; dir: string }): string {
42
+ const kind = path.relative(path.join(process.cwd(), 'qa'), unit.dir).split(path.sep)[0];
43
+ return `sungen generate --${kind === 'flows' ? 'flow' : kind === 'api' ? 'api' : 'screen'} ${unit.name}`;
44
+ }
45
+
46
+ function printVerdict(v: ContractVerdict, reportFile: string, generate: string): void {
37
47
  const L = console.log;
38
48
  const head = v.verdict === 'pass' ? '✓ PASS' : v.verdict === 'review'
39
49
  ? '⚖ REVIEW — changed, not weaker. Exit 0 here, but review blocks `sungen delivery` until QA re-approves' : '✗ FAIL';
@@ -64,7 +74,19 @@ function printVerdict(v: ContractVerdict, reportFile: string): void {
64
74
  : onlyUnproven ? " → Re-run the free / impl:code tests with trace: 'on', then check again."
65
75
  : ' → Restore the approved checks in the .feature — or, if the change is intended, QA approves it on a terminal:');
66
76
  }
67
- if (v.verdict !== 'pass' && !v.uncommitted.length && !onlyUnproven) L(` sungen contract approve --screen ${v.unit}`);
77
+ // An opaque step is not cleared by re-approving (#923): QA reviews it, then accepts it as it reads now.
78
+ const acceptable = blocking.filter((s) => s.class === 'opaque' && s.trace?.opaque?.some((o) => !o.accepted)
79
+ && !s.findings.some((f) => f.detail.startsWith(LOW_IDENTITY_FINDING)));
80
+ if (v.verdict !== 'pass' && !v.uncommitted.length && !onlyUnproven && acceptable.length < blocking.length) L(` sungen contract approve --screen ${v.unit}`);
81
+ for (const s of acceptable) {
82
+ L(` → ${s.id}: QA reviews each opaque step above (verbatim, file:line); to accept them as they read now, on a terminal:`);
83
+ L(` sungen contract approve --screen ${v.unit} --scenario ${s.id} --accept-opaque`);
84
+ }
85
+ // A weakened compiled scenario also leaves its spec out of step (#943): name the command that fixes the drift.
86
+ // (A free / impl:code scenario carries a trace summary; its test is hand-written, never regenerated.)
87
+ if (v.verdict === 'fail' && !v.uncommitted.length && v.scenarios.some((s) => !s.trace && HARD_CLASSES.has(s.class))) {
88
+ L(` → After fixing the scenario, run \`${generate}\`: until the compiled spec is regenerated, \`sungen script-check\` reports drift.`);
89
+ }
68
90
  L(` Report: ${path.relative(process.cwd(), reportFile)}`);
69
91
  L('');
70
92
  }
@@ -85,11 +107,20 @@ export function registerContractCommand(program: Command): void {
85
107
  .option('-s, --screen <name>', 'Screen / flow / api unit name')
86
108
  .option('--scenario <id>', 'Approve one scenario only (its ID, e.g. VP-LOGIN-001)')
87
109
  .option('--reason <text>', 'Why (recorded in the contract-approval decision)')
110
+ .option('--accept-opaque', 'With --scenario: accept the reviewed opaque trace step(s) of one approved free / impl:code scenario')
88
111
  .action(async (options) => {
89
112
  try {
90
113
  const unit = unitOrThrow(options.screen);
91
- const r = await approveContract({ projectRoot: process.cwd(), unit: unit.name, scenario: options.scenario, reason: options.reason, io: processIO() });
114
+ const r = await approveContract({
115
+ projectRoot: process.cwd(), unit: unit.name, scenario: options.scenario, reason: options.reason, acceptOpaque: options.acceptOpaque === true, io: processIO(),
116
+ });
92
117
  if (r.status === 'nothing-to-approve') { console.log(`\n= Nothing to approve: every chosen scenario of ${unit.name} already matches its approved lock entry.\n`); return; }
118
+ if (r.status === 'nothing-to-accept') { console.log(`\n= Nothing to accept: every opaque step of ${options.scenario} is already accepted as it reads now.\n`); return; }
119
+ if (r.status === 'accepted') {
120
+ console.log(`\n✓ Accepted ${r.accepted?.length ?? 0} opaque step(s) of ${options.scenario} → ${path.relative(process.cwd(), r.lockPath)} (decision ${r.decisionId})`);
121
+ console.log(' Commit the lock and the decision through review, then re-run the free test with its trace: a trace older than the lock proves nothing.\n');
122
+ return;
123
+ }
93
124
  console.log(`\n✓ Approved ${r.approved.length} scenario(s)${r.removed.length ? `, removed ${r.removed.length}` : ''} → ${path.relative(process.cwd(), r.lockPath)} (decision ${r.decisionId})`);
94
125
  console.log(' Commit the lock and the decision through review: until they are in git HEAD, `sungen contract check` reports contract.lock-uncommitted.\n');
95
126
  } catch (error) {
@@ -116,7 +147,7 @@ export function registerContractCommand(program: Command): void {
116
147
  if (hits.length) throw new Error(`Refused to write the contract report — likely secret(s): ${hits.map((h) => `${h.where} ${h.reason}`).join('; ')}`);
117
148
  const file = writeContractReport(process.cwd(), verdict);
118
149
  if (options.json) console.log(JSON.stringify(verdict, null, 2));
119
- else printVerdict(verdict, file);
150
+ else printVerdict(verdict, file, generateCommand(unit));
120
151
  process.exit(verdict.exitCode);
121
152
  } catch (error) {
122
153
  // Unreadable = not run (DOS-14.0): an earlier report must not outlive the inputs it judged.
@@ -0,0 +1,114 @@
1
+ /**
2
+ * `sungen contract approve --screen <u> --scenario <id> --accept-opaque` — a QA accepts the reviewed
3
+ * opaque steps of ONE approved free / impl:code scenario (#923). An opaque step (a check on a JS value,
4
+ * an API request, an evaluate) is never compared by code, so without this an approved test that has one
5
+ * stays in review for good: re-approving changes nothing and contract.opaque-step allows no override.
6
+ * In order, like `approve` (approve-contract.ts), so a human never confirms something then refused:
7
+ *
8
+ * 1. the scenario must be approved AS IT READS NOW — its lock entry human-approved, intact, at the
9
+ * current hash, and vouched for by its decision — else: approve it first
10
+ * 2. its verdict is rebuilt (buildContractVerdict, as `contract check`): the opaque steps must be the
11
+ * ONLY thing holding it; anything else (changed, unproven, inconclusive, a `low` identity…) refuses
12
+ * with its own fix. Every opaque step already accepted → nothing to accept, no prompt
13
+ * 3. each opaque step is shown verbatim with its file:line; on a TTY the human types the scenario id
14
+ * and a one-time code — without one: refused with the exact command (an agent never runs this)
15
+ * 4. everything is read AGAIN: if the trace, the feature or the lock moved meanwhile → refused
16
+ * 5. a contract-approval decision records the scenario at its hash WITH the accepted steps (step + sha)
17
+ * and the lock entry points at it (record-contract-approval.ts)
18
+ *
19
+ * The acceptance is bound to each step's sha (opaqueStepSha): a change to an accepted step, or a new
20
+ * approval of the scenario, brings it back to review.
21
+ *
22
+ * Trust boundary: the TTY confirmation guards this command, not the files. A lock entry AND a matching
23
+ * decision forged together and committed through review are trusted like any approval — git review
24
+ * is that boundary, here as for `approve`.
25
+ */
26
+ import { gitUserName } from '../decisions/decision-source';
27
+ import { formatCommand, requireTypedConfirmation } from '../decisions/human-confirmation';
28
+ import type { ApproveInput, ApproveResult } from './approve-contract';
29
+ import { buildContractVerdict } from './build-contract-verdict';
30
+ import { LOW_IDENTITY_FINDING } from './classify-trace-conformance';
31
+ import { canonicalJson } from './contract-hash';
32
+ import { ContractScenario, isHumanApproved, OpaqueStep } from './contract-schema';
33
+ import { resolveContractUnit } from './contract-units';
34
+ import { lockPathFor, ReadLock, readLockFile } from './contracts-lock-file';
35
+ import { loadContractScenarios } from './normalize-feature-steps';
36
+ import { recordOpaqueAcceptance } from './record-contract-approval';
37
+ import { unverifiedApprovals } from './verify-approval-decisions';
38
+
39
+ /** Accepting reads the traces with `contract check`'s generous deadline, never the guard's ~3 s. */
40
+ const ACCEPT_TRACE_BUDGET_MS = 120_000;
41
+ /** Findings that never hold a scenario back may stand beside the opaque steps. */
42
+ const HARMLESS = new Set(['strengthened', 'nondeterministic']);
43
+
44
+ interface Plan { unitDir: string; unit: string; scenario: ContractScenario; lock: ReadLock; decisionId: string; opaque: OpaqueStep[]; fingerprint: string }
45
+
46
+ const isLow = (detail: string): boolean => detail.startsWith(LOW_IDENTITY_FINDING);
47
+
48
+ function plan(i: ApproveInput, id: string): Plan {
49
+ const unit = resolveContractUnit(i.projectRoot, i.unit);
50
+ const lock = readLockFile(lockPathFor(unit.dir));
51
+ const matches = loadContractScenarios(unit.dir, unit.name).filter((s) => s.id === id);
52
+ if (!matches.length) throw new Error(`--scenario ${id} names no scenario of ${i.unit}`);
53
+ if (matches.length > 1) throw new Error(`Refused: scenario id ${id} is used by ${matches.length} scenarios — an acceptance must name exactly one. Give each its own ID.`);
54
+ const sc = matches[0];
55
+ if (sc.implementation === 'compiled') throw new Error(`Refused: ${id} is compiled — only a free / impl:code test has trace steps to accept.`);
56
+ const entry = lock?.scenarios[id];
57
+ if (!lock || !isHumanApproved(entry) || lock.tampered.includes(id) || entry.hash !== sc.hash) {
58
+ throw new Error(`Refused: ${id} is not approved as it reads now — approve it first (\`sungen contract approve --screen ${i.unit} --scenario ${id}\`), re-run it with a trace, then accept its opaque steps.`);
59
+ }
60
+ // The entry an acceptance re-points must be QA's own: its decision (on disk) vouches for it as it is.
61
+ const [unvouched] = unverifiedApprovals(i.projectRoot, unit.dir, { schema: lock.schema, scenarios: { [id]: entry } }, new Set(), { root: i.projectRoot, timeoutMs: 0, blobs: null });
62
+ if (unvouched) throw new Error(`Refused: the lock entry of ${id} is not vouched for by its decision (${unvouched.detail}) — restore the lock from git, or approve the scenario again.`);
63
+
64
+ const v = buildContractVerdict(i.projectRoot, unit, { traceBudgetMs: ACCEPT_TRACE_BUDGET_MS });
65
+ const bad = v.uncommitted.find((u) => u.id === id && (u.change === 'tampered' || u.change === 'unverified'));
66
+ if (bad) throw new Error(`Refused: the lock entry of ${id} is ${bad.change} (${bad.detail}) — restore it from git before accepting anything.`);
67
+ const r = v.scenarios.find((s) => s.id === id);
68
+ const blocking = (r?.findings ?? []).filter((f) => !HARMLESS.has(f.class) && !(f.class === 'opaque' && !isLow(f.detail)));
69
+ if (blocking.length) {
70
+ const fix = blocking.some((f) => isLow(f.detail))
71
+ ? `run \`sungen contract check --screen ${i.unit} --trace\` (a real browser resolves the identity) first`
72
+ : `settle that first — \`sungen contract check --screen ${i.unit}\` names the fix`;
73
+ throw new Error(`Refused: accepting clears only opaque steps, and ${id} is also held by: ${blocking.map((f) => `${f.class}${f.step ? ` (step ${f.step})` : ''} — ${f.detail}`).join('; ')}. ${fix[0].toUpperCase()}${fix.slice(1)}.`);
74
+ }
75
+ const opaque = r?.trace?.opaque ?? [];
76
+ if (!opaque.length) throw new Error(`Refused: the trace of ${id} has no opaque step — nothing to accept (${r?.class ?? 'not judged'}).`);
77
+ return { unitDir: unit.dir, unit: unit.name, scenario: sc, lock, decisionId: entry.decisionId, opaque, fingerprint: canonicalJson({ lock, hash: sc.hash, opaque }) };
78
+ }
79
+
80
+ const describe = (o: OpaqueStep): string =>
81
+ `${o.accepted ? '=' : '◐'} step ${o.step} opaque ${o.kind === 'assert' ? 'check' : 'step'} "${o.verbatim}"${o.where ? ` at ${o.where}` : ''}`
82
+ + `${o.replaces ? ` — replaces approved ${o.replaces}` : ''}${o.accepted ? ' (accepted before)' : ''}`;
83
+
84
+ export async function acceptOpaqueSteps(i: ApproveInput): Promise<ApproveResult> {
85
+ const id = i.scenario;
86
+ if (!id) throw new Error('--accept-opaque needs --scenario <id>: an acceptance names the one scenario whose opaque steps QA reviewed');
87
+ const before = plan(i, id);
88
+ const lockPath = lockPathFor(before.unitDir);
89
+ if (before.opaque.every((o) => o.accepted)) return { status: 'nothing-to-accept', lockPath, approved: [], removed: [] };
90
+
91
+ const pending = before.opaque.filter((o) => !o.accepted);
92
+ await requireTypedConfirmation({
93
+ what: `accept ${pending.length} reviewed opaque step(s) of ${id} (${i.unit}) — steps sungen cannot compare, so they are never checked by code`,
94
+ phrase: id,
95
+ command: formatCommand(['sungen', 'contract', 'approve', '--screen', i.unit, '--scenario', id, '--accept-opaque', ...(i.reason ? ['--reason', i.reason] : [])]),
96
+ summary: [
97
+ ...before.opaque.map(describe),
98
+ `bound to each step as it reads now: a change to it, or a new approval of ${id}, brings it back to review`,
99
+ `${id} stays approved at hash ${before.scenario.hash}; its lock entry moves from decision ${before.decisionId} to the new one`,
100
+ `recorded by: ${gitUserName(i.projectRoot)}`,
101
+ ],
102
+ io: i.io,
103
+ });
104
+
105
+ const after = plan(i, id);
106
+ if (after.fingerprint !== before.fingerprint) {
107
+ throw new Error('Refused: the trace, the feature or the lock changed while you were confirming — nothing was written. Run the command again and review the new summary.');
108
+ }
109
+ const { decisionId } = recordOpaqueAcceptance({
110
+ projectRoot: i.projectRoot, unitDir: after.unitDir, unit: after.unit, scenario: after.scenario, reason: i.reason, lock: after.lock,
111
+ accepted: after.opaque.map((o) => ({ step: o.step, sha: o.sha })),
112
+ });
113
+ return { status: 'accepted', lockPath, decisionId, approved: [id], removed: [], accepted: after.opaque };
114
+ }