@jhlagado/azm 0.2.12 → 0.2.14

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 (48) hide show
  1. package/dist/src/api-compile.d.ts +7 -1
  2. package/dist/src/api-compile.js +17 -5
  3. package/dist/src/api-register-contracts.js +69 -2
  4. package/dist/src/api-tooling.d.ts +1 -1
  5. package/dist/src/cli/artifact-files.d.ts +1 -0
  6. package/dist/src/cli/artifact-files.js +5 -0
  7. package/dist/src/cli/parse-args.d.ts +6 -1
  8. package/dist/src/cli/parse-args.js +59 -0
  9. package/dist/src/cli/run.js +2 -2
  10. package/dist/src/cli/usage.js +5 -0
  11. package/dist/src/cli/write-artifacts.d.ts +1 -1
  12. package/dist/src/cli/write-artifacts.js +15 -2
  13. package/dist/src/expansion/op-expansion.js +12 -1
  14. package/dist/src/index.d.ts +1 -1
  15. package/dist/src/outputs/types.d.ts +13 -1
  16. package/dist/src/register-contracts/analyze-helpers.d.ts +6 -1
  17. package/dist/src/register-contracts/analyze-helpers.js +67 -0
  18. package/dist/src/register-contracts/analyze.d.ts +8 -1
  19. package/dist/src/register-contracts/analyze.js +353 -16
  20. package/dist/src/register-contracts/interfaceContracts.js +45 -0
  21. package/dist/src/register-contracts/liveness.js +23 -0
  22. package/dist/src/register-contracts/policy.d.ts +2 -0
  23. package/dist/src/register-contracts/policy.js +54 -0
  24. package/dist/src/register-contracts/profiles.d.ts +5 -0
  25. package/dist/src/register-contracts/profiles.js +32 -2
  26. package/dist/src/register-contracts/programModel-boundaries.d.ts +5 -1
  27. package/dist/src/register-contracts/programModel-boundaries.js +20 -5
  28. package/dist/src/register-contracts/programModel-routines.js +37 -6
  29. package/dist/src/register-contracts/ratchet.d.ts +3 -0
  30. package/dist/src/register-contracts/ratchet.js +88 -0
  31. package/dist/src/register-contracts/report.d.ts +8 -1
  32. package/dist/src/register-contracts/report.js +174 -0
  33. package/dist/src/register-contracts/smartCommentParsing.js +22 -0
  34. package/dist/src/register-contracts/summaries.js +4 -0
  35. package/dist/src/register-contracts/summary-boundary.js +21 -3
  36. package/dist/src/register-contracts/summary.js +31 -3
  37. package/dist/src/register-contracts/tooling.d.ts +2 -1
  38. package/dist/src/register-contracts/tooling.js +2 -0
  39. package/dist/src/register-contracts/types.d.ts +159 -0
  40. package/dist/src/syntax/parse-line.js +3 -0
  41. package/docs/codebase/02-source-loading-and-parsing.md +10 -6
  42. package/docs/codebase/04-ops-and-register-contracts.md +58 -4
  43. package/docs/codebase/05-interfaces-and-output-artifacts.md +69 -6
  44. package/docs/codebase/06-verification-and-maintenance.md +10 -2
  45. package/docs/codebase/appendices/a-directory-file-reference.md +3 -1
  46. package/docs/codebase/appendices/b-compile-flow-reference.md +7 -5
  47. package/docs/codebase/appendices/c-public-surface-reference.md +19 -5
  48. package/package.json +1 -1
@@ -91,7 +91,7 @@ function applyStackPop(tokens, consumedProduced, intendedProduced, stack, state,
91
91
  }
92
92
  return;
93
93
  }
94
- if (popped.length !== units.length) {
94
+ if (popped.tokens.length !== units.length) {
95
95
  for (const unit of units) {
96
96
  tokens.set(unit, { origin: 'unknown' });
97
97
  consumedProduced.delete(unit);
@@ -100,11 +100,35 @@ function applyStackPop(tokens, consumedProduced, intendedProduced, stack, state,
100
100
  return;
101
101
  }
102
102
  units.forEach((unit, idx) => {
103
- tokens.set(unit, popped[idx] ?? { origin: 'unknown' });
103
+ tokens.set(unit, popped.tokens[idx] ?? { origin: 'unknown' });
104
104
  consumedProduced.delete(unit);
105
105
  intendedProduced.delete(unit);
106
106
  });
107
107
  }
108
+ function stackFrameUnits(frameUnit) {
109
+ if (frameUnit === 'AF')
110
+ return ['A', 'sign', 'zero', 'halfCarry', 'parity', 'carry'];
111
+ if (frameUnit === 'BC')
112
+ return ['B', 'C'];
113
+ if (frameUnit === 'DE')
114
+ return ['D', 'E'];
115
+ if (frameUnit === 'HL')
116
+ return ['H', 'L'];
117
+ if (frameUnit === 'IX')
118
+ return ['IXH', 'IXL'];
119
+ return ['IYH', 'IYL'];
120
+ }
121
+ function consumeKnownBoundaryStackFrame(stack, state, knownBoundary) {
122
+ for (const frameUnit of knownBoundary?.consumesStackFrame ?? []) {
123
+ const expected = stackFrameUnits(frameUnit);
124
+ const popped = stack.pop();
125
+ if (popped === undefined ||
126
+ popped.units.length !== expected.length ||
127
+ !popped.units.every((unit, index) => unit === expected[index])) {
128
+ state.stackBalanced = false;
129
+ }
130
+ }
131
+ }
108
132
  function applyUnknownStackUnits(tokens, consumedProduced, intendedProduced, units) {
109
133
  for (const unit of units) {
110
134
  tokens.set(unit, { origin: 'unknown' });
@@ -114,7 +138,10 @@ function applyUnknownStackUnits(tokens, consumedProduced, intendedProduced, unit
114
138
  }
115
139
  function applyStackEffect(tokens, consumedProduced, intendedProduced, stack, state, effect, expectedTerminalReturn, knownBoundary) {
116
140
  if (effect.stack.kind === 'push') {
117
- stack.push(effect.stack.units.map((unit) => readToken(tokens, unit)));
141
+ stack.push({
142
+ units: effect.stack.units,
143
+ tokens: effect.stack.units.map((unit) => readToken(tokens, unit)),
144
+ });
118
145
  return;
119
146
  }
120
147
  if (effect.stack.kind === 'pop') {
@@ -126,6 +153,7 @@ function applyStackEffect(tokens, consumedProduced, intendedProduced, stack, sta
126
153
  applyUnknownStackUnits(tokens, consumedProduced, intendedProduced, effect.stack.units);
127
154
  return;
128
155
  }
156
+ consumeKnownBoundaryStackFrame(stack, state, knownBoundary);
129
157
  if (expectedTerminalReturn && stack.length !== 0) {
130
158
  state.stackBalanced = false;
131
159
  }
@@ -1,6 +1,6 @@
1
1
  import type { Diagnostic } from '../model/diagnostic.js';
2
2
  import type { LoadedProgram } from '../tooling/api.js';
3
- import type { AnalyzeRegisterContractsOptions, RegisterContractsOutputCandidate, RegisterContractsUnit } from './types.js';
3
+ import type { AnalyzeRegisterContractsOptions, RegisterContractsFinding, RegisterContractsOutputCandidate, RegisterContractsUnit } from './types.js';
4
4
  export interface RegisterContractsTextEdit {
5
5
  file: string;
6
6
  line: number;
@@ -42,6 +42,7 @@ export interface AnalyzeRegisterContractsForToolsOptions extends Omit<AnalyzeReg
42
42
  export type AnalyzeRegisterCareForToolsOptions = AnalyzeRegisterContractsForToolsOptions;
43
43
  export interface AnalyzeRegisterContractsForToolsResult {
44
44
  diagnostics: Diagnostic[];
45
+ findings: RegisterContractsFinding[];
45
46
  outputCandidates: RegisterContractsOutputCandidate[];
46
47
  candidateDiagnostics: RegisterContractsCandidateDiagnostic[];
47
48
  codeActions: RegisterContractsCodeAction[];
@@ -46,12 +46,14 @@ export function analyzeRegisterContractsForTools(loaded, options) {
46
46
  ? baseResultOptions
47
47
  : { ...baseResultOptions, registerContractsProfile: profile });
48
48
  const outputCandidates = result.outputCandidates ?? [];
49
+ const findings = result.findings ?? [];
49
50
  const candidateDiagnostics = outputCandidates.map(diagnosticForOutputCandidate);
50
51
  const codeActions = outputCandidates
51
52
  .filter((candidate) => candidate.autoFixable === true)
52
53
  .map(codeActionForOutputCandidate);
53
54
  return {
54
55
  diagnostics: result.diagnostics,
56
+ findings,
55
57
  outputCandidates,
56
58
  candidateDiagnostics,
57
59
  codeActions,
@@ -1,8 +1,17 @@
1
1
  import type { Z80Instruction } from '../z80/instruction.js';
2
2
  export type RegisterContractsMode = 'off' | 'audit' | 'warn' | 'error' | 'strict';
3
+ export type RegisterContractsReportFormat = 'text' | 'json';
4
+ export type RegisterContractsInferenceFormat = 'json' | 'markdown';
5
+ export type RegisterContractsPolicyMode = 'off' | 'audit' | 'strict';
6
+ export interface RegisterContractsPolicy {
7
+ strict?: readonly string[];
8
+ audit?: readonly string[];
9
+ off?: readonly string[];
10
+ }
3
11
  /** @deprecated Use RegisterContractsMode. */
4
12
  export type RegisterCareMode = RegisterContractsMode;
5
13
  export type RegisterContractsUnit = 'A' | 'B' | 'C' | 'D' | 'E' | 'H' | 'L' | 'IXH' | 'IXL' | 'IYH' | 'IYL' | 'SPH' | 'SPL' | 'carry' | 'zero' | 'sign' | 'parity' | 'halfCarry';
14
+ export type RegisterContractsStackFrameUnit = 'AF' | 'BC' | 'DE' | 'HL' | 'IX' | 'IY';
6
15
  /** @deprecated Use RegisterContractsUnit. */
7
16
  export type RegisterCareUnit = RegisterContractsUnit;
8
17
  export type SmartComment = {
@@ -28,6 +37,10 @@ export type SmartComment = {
28
37
  kind: 'expectOut';
29
38
  carriers: RegisterContractsUnit[];
30
39
  name?: string;
40
+ } | {
41
+ kind: 'rcIgnoreNext';
42
+ findingKind: RegisterContractsFindingKind;
43
+ reason: string;
31
44
  };
32
45
  export interface LocatedSmartComment {
33
46
  file: string;
@@ -47,6 +60,9 @@ export interface RegisterContractsInstruction {
47
60
  file: string;
48
61
  line: number;
49
62
  column: number;
63
+ sourceUnit?: string;
64
+ sourceRelation?: 'entry' | 'include' | 'import';
65
+ sourceUnitRelation?: 'entry' | 'include' | 'import';
50
66
  labels: string[];
51
67
  constants?: ReadonlyMap<string, number>;
52
68
  }
@@ -60,6 +76,9 @@ export interface RegisterContractsRoutine {
60
76
  constants?: ReadonlyMap<string, number>;
61
77
  span: {
62
78
  file: string;
79
+ sourceUnit?: string;
80
+ sourceRelation?: 'entry' | 'include' | 'import';
81
+ sourceUnitRelation?: 'entry' | 'include' | 'import';
63
82
  start: {
64
83
  line: number;
65
84
  column: number;
@@ -78,6 +97,9 @@ export interface RegisterContractsDirectCall {
78
97
  file: string;
79
98
  line: number;
80
99
  column: number;
100
+ sourceUnit?: string;
101
+ sourceRelation?: 'entry' | 'include' | 'import';
102
+ sourceUnitRelation?: 'entry' | 'include' | 'import';
81
103
  }
82
104
  /** @deprecated Use RegisterContractsDirectCall. */
83
105
  export type RegisterCareDirectCall = RegisterContractsDirectCall;
@@ -140,12 +162,17 @@ export interface RoutineSummary {
140
162
  valueRelations: ValueRelation[];
141
163
  stackBalanced: boolean;
142
164
  hasUnknownStackEffect?: boolean;
165
+ consumesStackFrame?: RegisterContractsStackFrameUnit[];
143
166
  outputCandidates?: RegisterContractsUnit[];
144
167
  }
145
168
  export interface RegisterContractsOutputCandidate {
169
+ kind?: 'output_candidate';
146
170
  file: string;
147
171
  line: number;
148
172
  column: number;
173
+ sourceUnit?: string;
174
+ sourceRelation?: 'entry' | 'include' | 'import';
175
+ sourceUnitRelation?: 'entry' | 'include' | 'import';
149
176
  routine: string;
150
177
  carriers: RegisterContractsUnit[];
151
178
  autoFixable?: boolean;
@@ -154,35 +181,167 @@ export interface RegisterContractsOutputCandidate {
154
181
  /** @deprecated Use RegisterContractsOutputCandidate. */
155
182
  export type RegisterCareOutputCandidate = RegisterContractsOutputCandidate;
156
183
  export interface RegisterContractsConflict {
184
+ kind?: 'definite_contract_violation' | 'flag_lifetime_risk';
157
185
  file: string;
158
186
  line: number;
159
187
  column: number;
188
+ sourceUnit?: string;
189
+ sourceRelation?: 'entry' | 'include' | 'import';
190
+ sourceUnitRelation?: 'entry' | 'include' | 'import';
191
+ routine?: string;
160
192
  callTarget: string;
161
193
  carriers: RegisterContractsUnit[];
162
194
  message: string;
163
195
  }
164
196
  /** @deprecated Use RegisterContractsConflict. */
165
197
  export type RegisterCareConflict = RegisterContractsConflict;
198
+ export type RegisterContractsFindingKind = 'definite_contract_violation' | 'flag_lifetime_risk' | 'missing_callee_contract' | 'unknown_control_flow' | 'external_interface_unknown' | 'output_candidate';
199
+ interface RegisterContractsFindingBase {
200
+ kind: RegisterContractsFindingKind;
201
+ file: string;
202
+ line: number;
203
+ column: number;
204
+ sourceUnit?: string;
205
+ sourceRelation?: 'entry' | 'include' | 'import';
206
+ sourceUnitRelation?: 'entry' | 'include' | 'import';
207
+ message: string;
208
+ routine?: string;
209
+ carriers?: RegisterContractsUnit[];
210
+ }
211
+ export interface RegisterContractsConflictFinding extends RegisterContractsFindingBase {
212
+ kind: 'definite_contract_violation' | 'flag_lifetime_risk';
213
+ callTarget: string;
214
+ }
215
+ export interface RegisterContractsUnknownBoundaryFinding extends RegisterContractsFindingBase {
216
+ kind: 'missing_callee_contract' | 'external_interface_unknown';
217
+ callTarget: string;
218
+ subject: string;
219
+ }
220
+ export interface RegisterContractsStackFinding extends RegisterContractsFindingBase {
221
+ kind: 'unknown_control_flow';
222
+ routine: string;
223
+ stackBalanced: boolean;
224
+ hasUnknownStackEffect?: boolean;
225
+ }
226
+ export interface RegisterContractsOutputCandidateFinding extends RegisterContractsFindingBase {
227
+ kind: 'output_candidate';
228
+ routine: string;
229
+ autoFixable?: boolean;
230
+ }
231
+ export type RegisterContractsFinding = RegisterContractsConflictFinding | RegisterContractsUnknownBoundaryFinding | RegisterContractsStackFinding | RegisterContractsOutputCandidateFinding;
166
232
  export interface RegisterContractsReportModel {
167
233
  entryFile: string;
168
234
  mode: RegisterContractsMode;
169
235
  profile?: string;
170
236
  summaries: RoutineSummary[];
237
+ findings?: RegisterContractsFinding[];
238
+ suppressedFindings?: RegisterContractsSuppressedFinding[];
171
239
  conflicts: RegisterContractsConflict[];
172
240
  outputCandidates?: RegisterContractsOutputCandidate[];
173
241
  unknownCalls: string[];
242
+ ratchet?: RegisterContractsRatchetResult;
243
+ }
244
+ export interface RegisterContractsJsonLocation {
245
+ file: string;
246
+ line: number;
247
+ column: number;
248
+ sourceUnit?: string;
249
+ sourceRelation?: 'entry' | 'include' | 'import';
250
+ sourceUnitRelation?: 'entry' | 'include' | 'import';
251
+ }
252
+ export interface RegisterContractsJsonRemediation {
253
+ category: 'add_contract' | 'fix_call_or_contract' | 'review_control_flow' | 'review_output_contract';
254
+ hint: string;
255
+ }
256
+ export interface RegisterContractsJsonFinding {
257
+ kind: RegisterContractsFindingKind;
258
+ location: RegisterContractsJsonLocation;
259
+ message: string;
260
+ routine?: string;
261
+ callTarget?: string;
262
+ subject?: string;
263
+ carriers?: RegisterContractsUnit[];
264
+ stackBalanced?: boolean;
265
+ hasUnknownStackEffect?: boolean;
266
+ autoFixable?: boolean;
267
+ remediation: RegisterContractsJsonRemediation;
268
+ }
269
+ export interface RegisterContractsSuppression {
270
+ file: string;
271
+ line: number;
272
+ column: number;
273
+ findingKind: RegisterContractsFindingKind;
274
+ reason: string;
275
+ }
276
+ export interface RegisterContractsSuppressedFinding {
277
+ finding: RegisterContractsFinding;
278
+ suppression: RegisterContractsSuppression;
279
+ }
280
+ export interface RegisterContractsJsonReportModel {
281
+ format: 'azm-register-contracts-report';
282
+ version: 1;
283
+ entryFile: string;
284
+ mode: RegisterContractsMode;
285
+ profile?: string;
286
+ summaries: RoutineSummary[];
287
+ findings: RegisterContractsJsonFinding[];
288
+ suppressedFindings?: Array<{
289
+ finding: RegisterContractsJsonFinding;
290
+ suppression: RegisterContractsSuppression;
291
+ }>;
292
+ unknownCalls: string[];
293
+ ratchet?: RegisterContractsRatchetResult;
294
+ }
295
+ export interface RegisterContractsInferenceRoutine {
296
+ name: string;
297
+ in: RegisterContractsUnit[];
298
+ out: RegisterContractsUnit[];
299
+ clobbers: RegisterContractsUnit[];
300
+ preserves: RegisterContractsUnit[];
301
+ confidence: 'explicit' | 'inferred' | 'draft';
302
+ callerImpact: {
303
+ outputCandidateCount: number;
304
+ outputCandidateCarriers: RegisterContractsUnit[];
305
+ };
306
+ }
307
+ export interface RegisterContractsInferenceModel {
308
+ format: 'azm-register-contracts-inference';
309
+ version: 1;
310
+ routines: RegisterContractsInferenceRoutine[];
311
+ }
312
+ export interface RegisterContractsRatchetEntry {
313
+ identity: string;
314
+ finding: RegisterContractsJsonFinding;
315
+ }
316
+ export interface RegisterContractsRatchetResult {
317
+ baselineFile?: string;
318
+ newFindings: RegisterContractsRatchetEntry[];
319
+ removedFindings: RegisterContractsRatchetEntry[];
320
+ changedFindings: Array<{
321
+ identity: string;
322
+ baseline: RegisterContractsJsonFinding;
323
+ current: RegisterContractsJsonFinding;
324
+ }>;
174
325
  }
175
326
  export interface AnalyzeRegisterContractsOptions {
176
327
  mode: RegisterContractsMode;
328
+ policy?: RegisterContractsPolicy;
177
329
  emitReport: boolean;
330
+ reportFormat?: RegisterContractsReportFormat;
178
331
  emitInterface: boolean;
332
+ emitInference?: boolean;
333
+ inferenceFormat?: RegisterContractsInferenceFormat;
179
334
  emitAnnotations?: boolean;
180
335
  fixRegisterContracts?: boolean;
181
336
  registerContractsProfile?: 'mon3';
182
337
  interfaceContracts?: RoutineContract[];
183
338
  acceptedOutputCandidates?: ReadonlyMap<string, RegisterContractsUnit[]>;
339
+ baselineReport?: RegisterContractsJsonReportModel;
340
+ baselineFile?: string;
341
+ ratchet?: boolean;
184
342
  }
185
343
  export interface RegisterContractsAnnotationFile {
186
344
  readonly path: string;
187
345
  readonly text: string;
188
346
  }
347
+ export {};
@@ -67,6 +67,9 @@ function commentOnlyLine(line) {
67
67
  column: firstNonWhitespaceColumn(line.text),
68
68
  ...(line.sourceUnit !== undefined ? { sourceUnit: line.sourceUnit } : {}),
69
69
  ...(line.sourceRelation !== undefined ? { sourceRelation: line.sourceRelation } : {}),
70
+ ...(line.sourceUnitRelation !== undefined
71
+ ? { sourceUnitRelation: line.sourceUnitRelation }
72
+ : {}),
70
73
  },
71
74
  },
72
75
  ],
@@ -107,10 +107,13 @@ been identified.
107
107
 
108
108
  `src/source/logical-lines.ts` scans a `SourceFile` into `LogicalLine` objects. A
109
109
  logical line records the source name, line number and original text. Tooling
110
- loads can also attach `sourceUnit` and `sourceRelation`:
110
+ loads can also attach `sourceUnit`, `sourceRelation` and `sourceUnitRelation`:
111
111
 
112
112
  - `sourceUnit` is the owning file for the current tooling unit
113
- - `sourceRelation` is `entry`, `include` or `import`
113
+ - `sourceRelation` says how this physical file entered the current expansion
114
+ step: `entry`, `include` or `import`
115
+ - `sourceUnitRelation` says how the owning source unit entered the load:
116
+ `entry` or `import`
114
117
 
115
118
  This thin structure gives every later diagnostic a stable location and enough
116
119
  provenance for tooling features that need to reason about module ownership.
@@ -176,10 +179,11 @@ line is a label, instruction, directive, layout declaration or comment item.
176
179
  - comments
177
180
 
178
181
  Each item carries a source span where appropriate. Tooling spans now preserve
179
- optional `sourceUnit` and `sourceRelation` fields when the loader attached them.
180
- Assembly uses item kind to decide size and emission. Register contract analysis
181
- uses instruction, label and comment items to build routines. D8 map output uses
182
- spans to connect emitted bytes back to files and lines.
182
+ optional `sourceUnit`, `sourceRelation` and `sourceUnitRelation` fields when
183
+ the loader attached them. Assembly uses item kind to decide size and emission.
184
+ Register contract analysis uses instruction, label and comment items to build
185
+ routines. D8 map output uses spans to connect emitted bytes back to files and
186
+ lines.
183
187
 
184
188
  ## Top-Level Parse Order
185
189
 
@@ -168,7 +168,10 @@ without exposing them outside the module.
168
168
  `src/register-contracts/smartComments.ts` reads AZMDoc comments from the comment maps
169
169
  captured during loading. Comment-block splitting and token parsing live in
170
170
  `smartCommentBlocks.ts` and `smartCommentParsing.ts`. External `.asmi`
171
- contracts are parsed in `interfaceContracts.ts`.
171
+ contracts are parsed in `interfaceContracts.ts`. The same parser also accepts
172
+ inline suppression comments in the form
173
+ `;! rc-ignore-next <finding-kind>: <reason>` for the next finding at that
174
+ location.
172
175
 
173
176
  Contracts can describe:
174
177
 
@@ -184,6 +187,11 @@ routine contract. Source comments attach to routines in the current program.
184
187
  The compact source form can place several clauses on one `;!` line, for
185
188
  example `;! in A; out A; clobbers F`.
186
189
 
190
+ `.asmi` parsing stays strict. Files may contain `extern` or `service rst`
191
+ boundaries, `in`, `out`, `clobbers` and `preserves` clauses, then `end`.
192
+ Comment lines are rejected so interface files stay machine-generated and
193
+ deterministic.
194
+
187
195
  ## Effects, Summaries and Liveness
188
196
 
189
197
  Register contract analysis depends on `src/z80/effects.ts`. Effects describe which registers
@@ -212,10 +220,45 @@ summaries before the final pass. Strict mode uses `stackBalanced` and
212
220
  `hasUnknownStackEffect` to distinguish balanced stack use from a routine whose
213
221
  boundary may leave the stack in an unknown state.
214
222
 
223
+ Some monitor ABIs deliberately consume a caller-prepared stack frame at a known
224
+ boundary. The built-in MON3/TecMate profile models `RST $10` with `C=$53`
225
+ (`MON_BANK_CALL`) as a service-specific boundary that consumes the top stack
226
+ entries `AF`, `DE` and `HL`, returns `A` and carry from the banked target, and
227
+ leaves the caller stack balanced. This is not a blanket relaxation for `RST`;
228
+ the behavior is selected by the proven `C` service value. The same profile also
229
+ provides a `C >= $60` TecMate expansion-service range fallback that returns
230
+ `A` and carry for installed expansion services.
231
+
232
+ The analysis now emits typed findings rather than only free-form conflict text.
233
+ The current finding set covers:
234
+
235
+ - `definite_contract_violation`
236
+ - `flag_lifetime_risk`
237
+ - `missing_callee_contract`
238
+ - `external_interface_unknown`
239
+ - `unknown_control_flow`
240
+ - `output_candidate`
241
+
242
+ Policy and suppression handling operate on those finding kinds. Report JSON,
243
+ tooling consumers and baseline ratchets all use the same typed finding model.
244
+
245
+ `policy.ts` resolves per-file register-contract modes from `strict`, `audit`
246
+ and `off` glob lists. Specific patterns win over broad patterns, then stricter
247
+ matches win ties. This lets one compile run audit imported wrappers while
248
+ keeping core files in strict mode or disabling analysis for known external
249
+ shims. When policy disables a file, unresolved call targets in that file no
250
+ longer surface as ordinary unknown-symbol diagnostics.
251
+
215
252
  ## Reports, Interfaces and Tooling
216
253
 
217
- `report.ts` renders human-readable `.regcontracts.txt` reports and `.asmi` interface
218
- metadata. It also renders generated source contracts as one compact `;!` line,
254
+ `report.ts` renders human-readable `.regcontracts.txt` reports, JSON report
255
+ payloads, `.asmi` interface metadata and review-oriented inference exports. The
256
+ text and JSON reports share one model with routine summaries, findings,
257
+ suppressed findings, unknown calls and optional ratchet results. Inference
258
+ exports are lighter-weight routine summaries for review workflows and can be
259
+ rendered as JSON or Markdown.
260
+
261
+ `report.ts` also renders generated source contracts as one compact `;!` line,
219
262
  joining `in`, `out`, `maybe-out` and `clobbers` clauses with semicolons.
220
263
  When a routine may clobber the full flag set, the source form uses `F` as the
221
264
  compact carrier name. `annotate.ts`, `annotations.ts`, `fix.ts` and
@@ -225,7 +268,12 @@ conservative fixes.
225
268
  The CLI can request these behaviours through:
226
269
 
227
270
  - `--reg-report`
271
+ - `--reg-report-format`
272
+ - `--reg-baseline`
273
+ - `--reg-ratchet`
228
274
  - `--reg-interface`
275
+ - `--reg-infer`
276
+ - `--reg-infer-format`
229
277
  - `--contracts`
230
278
  - `--fix`
231
279
  - `--accept-out`
@@ -236,6 +284,11 @@ file, line, column, message, fixability and optional text edits. An editor can
236
284
  show the same register contract information that the CLI reports while using
237
285
  normal editor actions for accepted fixes.
238
286
 
287
+ `analyzeRegisterContractsForTools()` now also returns the structured findings
288
+ list and output candidate list beside candidate diagnostics and code actions, so
289
+ tooling integrations can render the same categorised analysis that the CLI
290
+ writes to report artifacts.
291
+
239
292
  ## Changing Ops or Register Contracts
240
293
 
241
294
  Op changes belong in `src/expansion/`, with tests under
@@ -249,7 +302,8 @@ Register contract changes usually begin in one of these files:
249
302
  `instruction-operands.ts`, `instruction-predicates.ts`
250
303
  - Summary inference: `summary.ts`
251
304
  - Caller liveness: `liveness.ts`
252
- - Output text: `report.ts`
305
+ - Policy selection and ratchets: `policy.ts`, `ratchet.ts`
306
+ - Output text and structured exports: `report.ts`
253
307
  - Source edits: `annotate.ts`, `fix.ts`, `annotations.ts`
254
308
  - Tooling surface: `tooling.ts`
255
309
 
@@ -68,6 +68,14 @@ when artifact writing succeeds.
68
68
  artifact suppression, include paths, source-root, case-style linting, directive
69
69
  aliases and register contract options.
70
70
 
71
+ Register-contract parsing now includes:
72
+
73
+ - report format selection with `--reg-report-format text|json`
74
+ - baseline comparison with `--reg-baseline <report.json>`
75
+ - ratcheting with `--reg-ratchet`
76
+ - inference export with `--reg-infer`
77
+ - inference format selection with `--reg-infer-format json|markdown`
78
+
71
79
  `src/cli/write-artifacts.ts` maps parsed options into
72
80
  `CompileNextFunctionOptions` and calculates the output stem.
73
81
  `src/cli/artifact-files.ts` writes in-memory artifacts to disk. If the user
@@ -76,6 +84,13 @@ path and side artifacts use the same base. If the user supplies only
76
84
  `program.asm`, AZM writes outputs next to the entry source using the source
77
85
  stem.
78
86
 
87
+ Register-contract side artifacts now use suffixes derived from the selected
88
+ format:
89
+
90
+ - `.regcontracts.txt` or `.regcontracts.json` for reports
91
+ - `.asmi` for inferred interfaces
92
+ - `.regcontracts.inference.json` or `.regcontracts.inference.md` for review exports
93
+
79
94
  ## Compile API
80
95
 
81
96
  `src/api-compile.ts` exports:
@@ -103,8 +118,14 @@ Important options include:
103
118
  | `d8mInputs` | Artifact paths recorded in D8 metadata. |
104
119
  | `emitBin`, `emitHex`, `emitD8m`, `emitAsm80` | Artifact selection. |
105
120
  | `registerContracts` | Register contract mode. |
106
- | `emitRegisterReport` | Emit `.regcontracts.txt` artifact. |
121
+ | `registerContractsPolicy` | Per-file strict, audit and off policy. |
122
+ | `emitRegisterReport` | Emit text or JSON report artifact. |
123
+ | `registerContractsReportFormat` | Report format, `text` or `json`. |
124
+ | `registerContractsBaseline` | Baseline JSON report for ratcheting. |
125
+ | `registerContractsRatchet` | Fail when findings increase. |
107
126
  | `emitRegisterInterface` | Emit `.asmi` artifact. |
127
+ | `emitRegisterInference` | Emit inference review artifact. |
128
+ | `registerContractsInferenceFormat` | Inference format, `json` or `markdown`. |
108
129
  | `emitRegisterAnnotations` | Emit source annotation artifact. |
109
130
  | `fixRegisterContracts` | Apply conservative source fixes. |
110
131
  | `acceptRegisterOutputCandidates` | Promote selected output candidates. |
@@ -112,6 +133,19 @@ Important options include:
112
133
  | `registerContractsInterfaces` | External `.asmi` contract files. |
113
134
  | `skipAssembly` | Run loading and analysis only. |
114
135
 
136
+ `registerContractsPolicy` matches the physical source file recorded on each
137
+ register-contract routine, direct call and finding. In a single assembled
138
+ translation unit, included files remain distinct physical files for diagnostics
139
+ and policy matching. For example, if `monitor.asm` includes `rtc.asm` and
140
+ `disassembler.asm`, a finding owned by `rtc.asm` is matched against `rtc.asm`,
141
+ not only against the root `monitor.asm` path.
142
+
143
+ This is independent of source ownership units: `.include` keeps the surrounding
144
+ source ownership unit for import/visibility semantics, but policy matching uses
145
+ the physical `sourceName`/finding file. This allows projects to audit retained
146
+ legacy source one included file at a time while keeping the whole program in one
147
+ assembled unit.
148
+
115
149
  `compile()` returns:
116
150
 
117
151
  ```ts
@@ -125,6 +159,11 @@ Diagnostics describe every warning or error observed during loading, analysis,
125
159
  register contract analysis, assembly or artifact creation. Artifacts contain the
126
160
  in-memory outputs requested by options.
127
161
 
162
+ When analysis is enabled, `compile()` can now suppress ordinary unresolved-call
163
+ diagnostics for files whose register-contract policy resolves to `off`. That
164
+ keeps policy-controlled external boundaries in the register-contract report path
165
+ rather than duplicating them as symbol failures.
166
+
128
167
  The older option names `registerCare`, `registerCareProfile` and
129
168
  `registerCareInterfaces` remain as deprecated aliases for package consumers.
130
169
  New callers should use the `registerContracts...` names.
@@ -138,17 +177,29 @@ helpers.
138
177
  `loadProgramNext()` returns a loaded program with source items, source texts and
139
178
  source line comments. `analyzeProgramNext()` runs semantic checks and returns
140
179
  symbols. `analyzeRegisterContractsForTools()` returns register contract
141
- diagnostics and code actions in a form suitable for editors.
180
+ diagnostics, typed findings, output candidates and code actions in a form
181
+ suitable for editors.
142
182
 
143
183
  The tooling loader now recognises both `.include` and `.import`. Both directives
144
184
  flatten source into one parse stream. `.import` also marks parsed spans with a
145
185
  new source ownership unit, while `.include` keeps the surrounding owner's unit.
146
- Editor features can use `item.span.sourceUnit` and `item.span.sourceRelation`
147
- to distinguish module-owned declarations from text included into another unit.
186
+ Editor features can use `item.span.sourceUnit`, `item.span.sourceRelation` and
187
+ `item.span.sourceUnitRelation` together: `sourceRelation` tracks the physical
188
+ file edge that produced the parsed item, while `sourceUnitRelation` tracks
189
+ whether the owning unit entered as the entry source or through `.import`.
148
190
  Diagnostics, symbols and source segments continue to use physical source file
149
191
  paths, so imported files appear as their own files in editor and Debug80 map
150
192
  metadata.
151
193
 
194
+ `analyzeProgramNext()` now also enforces the completed import-visibility pass.
195
+ Visibility checks walk instruction operands, data expressions, equates,
196
+ alignment and bin-range expressions and bare `.ds` size expressions. Imported
197
+ private labels remain visible inside their own source unit, including op
198
+ generated references and text included into that same unit. When a name already
199
+ collides with an equate, enum member or case-insensitive type or enum
200
+ declaration, assembly keeps the duplicate-symbol or duplicate-type diagnostic
201
+ instead of replacing it with a visibility error.
202
+
152
203
  An editor integration usually starts with:
153
204
 
154
205
  ```ts
@@ -179,6 +230,7 @@ The output layer uses structured artifact objects from `src/outputs/types.ts`:
179
230
  - `Asm80Artifact`
180
231
  - `RegisterContractsReportArtifact`
181
232
  - `RegisterContractsInterfaceArtifact`
233
+ - `RegisterContractsInferenceArtifact`
182
234
  - `RegisterContractsAnnotationsArtifact`
183
235
 
184
236
  Each artifact has a `kind` field. Callers can switch on `kind` to find the
@@ -241,11 +293,22 @@ supported.
241
293
 
242
294
  Register contract report, interface and annotation artifacts are created through
243
295
  `runRegisterContracts()` in `src/api-register-contracts.ts` and flow through
244
- the same compile result and CLI write path. The report is human-readable. The `.asmi`
245
- interface is metadata that can be loaded by later compile runs through
296
+ the same compile result and CLI write path. Reports can be human-readable text
297
+ or JSON. The `.asmi` interface is metadata that can be loaded by later compile runs through
246
298
  `--interface`. Annotation artifacts write source files when `--contracts` or
247
299
  `--fix` is used.
248
300
 
301
+ The register-contract report artifact now carries optional structured payloads:
302
+ `format`, `json` and the active `findings` list. JSON reports use the stable
303
+ `azm-register-contracts-report` envelope with summaries, findings, suppressed
304
+ findings, unknown calls and optional ratchet results.
305
+
306
+ Inference artifacts carry a routine-oriented `azm-register-contracts-inference`
307
+ model. Each routine records inferred `in`, `out`, `clobbers` and `preserves`
308
+ carriers, an inference confidence and caller-impact counts for output
309
+ candidates. Markdown rendering is intended for review, while JSON is suitable
310
+ for automation and regression tooling.
311
+
249
312
  ## Public API Compatibility
250
313
 
251
314
  The public API is defined by package exports and exported TypeScript types.
@@ -55,8 +55,9 @@ between modules rather than inside one helper.
55
55
 
56
56
  Source-loading and tooling provenance changes belong here as well. The
57
57
  `stage-11-tooling-api.test.ts` integration suite covers include and import
58
- resolution, recursive load failures and the span ownership metadata that tools
59
- read from parsed items.
58
+ resolution, recursive load failures, span ownership metadata and import
59
+ visibility analysis across direct calls, jump fixups, data expressions, bare
60
+ `.ds` sizes, duplicate-symbol precedence and include-only wrappers.
60
61
 
61
62
  ## CLI, ASM80 and Differential Tests
62
63
 
@@ -65,6 +66,10 @@ failure modes, determinism, case-style linting and register contract switches.
65
66
  Users experience the command-line behaviour through argument parsing,
66
67
  diagnostics, output paths and exit status.
67
68
 
69
+ The register-contract CLI suite now also covers JSON report output, inference
70
+ artifact output, baseline loading and ratchet failures so option parsing and
71
+ artifact suffix rules stay stable.
72
+
68
73
  CLI tests also protect deterministic output. `compareDiagnosticsForCli()` sorts
69
74
  diagnostics by file, line, column, severity, code and message. A CLI test can
70
75
  catch changes that leave the compiler correct but make terminal output unstable.
@@ -153,6 +158,8 @@ Ask what kind of fact the change affects:
153
158
  - Instruction-chain splitting belongs in `source/`, while inline source
154
159
  generation and op template expansion belong in `expansion/`.
155
160
  - Routine contracts and liveness belong in `register-contracts/`.
161
+ - Register-contract policy matching, suppression parsing and ratchets also
162
+ belong in `register-contracts/`.
156
163
  - Artifact shape belongs in `outputs/`.
157
164
  - User commands belong in `cli/`.
158
165
  - Package consumers belong in `api-compile.ts`, `api-tooling.ts` and
@@ -187,6 +194,7 @@ This book should change when:
187
194
  - public package exports change
188
195
  - CLI option groups change
189
196
  - output artifact shapes change
197
+ - register-contract finding kinds, policy rules or report schemas change
190
198
  - a major subsystem gains a new responsibility
191
199
  - tests or guardrails are reorganised
192
200