peaks-loop 4.0.42 → 4.0.44

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 (76) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README-en.md +1 -1
  3. package/README.md +1 -1
  4. package/dist/cli/commands/_register.js +4 -0
  5. package/dist/cli/commands/api-diff-commands.d.ts +16 -0
  6. package/dist/cli/commands/api-diff-commands.js +55 -0
  7. package/dist/cli/commands/audit-commands.d.ts +16 -3
  8. package/dist/cli/commands/audit-commands.js +84 -31
  9. package/dist/cli/commands/codegraph-commands.js +191 -6
  10. package/dist/cli/commands/final-review-commands.d.ts +34 -10
  11. package/dist/cli/commands/final-review-commands.js +130 -34
  12. package/dist/cli/commands/job-commands.js +4 -2
  13. package/dist/cli/commands/scan-commands.js +1 -1
  14. package/dist/cli/commands/share-commands.d.ts +49 -0
  15. package/dist/cli/commands/share-commands.js +114 -14
  16. package/dist/cli/commands/test-commands.d.ts +60 -3
  17. package/dist/cli/commands/test-commands.js +125 -7
  18. package/dist/services/audit/audit-goal-service.js +38 -3
  19. package/dist/services/codegraph/codegraph-autorefresh.js +12 -0
  20. package/dist/services/codegraph/codegraph-exclude-integrity.d.ts +61 -0
  21. package/dist/services/codegraph/codegraph-exclude-integrity.js +98 -0
  22. package/dist/services/codegraph/codegraph-exclude-reconciler.d.ts +26 -0
  23. package/dist/services/codegraph/codegraph-exclude-reconciler.js +217 -0
  24. package/dist/services/codegraph/codegraph-exclude-repair.d.ts +102 -0
  25. package/dist/services/codegraph/codegraph-exclude-repair.js +266 -0
  26. package/dist/services/codegraph/codegraph-preflight-service.js +12 -0
  27. package/dist/services/codegraph/codegraph-service.d.ts +0 -1
  28. package/dist/services/codegraph/codegraph-service.js +5 -4
  29. package/dist/services/doctor/doctor-service/checks/codegraph-exclude-integrity.d.ts +29 -0
  30. package/dist/services/doctor/doctor-service/checks/codegraph-exclude-integrity.js +88 -0
  31. package/dist/services/doctor/doctor-service/checks/ecc-hooks-schema-drift.d.ts +65 -0
  32. package/dist/services/doctor/doctor-service/checks/ecc-hooks-schema-drift.js +186 -0
  33. package/dist/services/doctor/doctor-service/plugin-registry.js +4 -0
  34. package/dist/services/doctor/doctor-service/types.d.ts +47 -0
  35. package/dist/services/final-review/final-review-service.d.ts +154 -0
  36. package/dist/services/final-review/final-review-service.js +621 -7
  37. package/dist/services/final-review/index.d.ts +1 -1
  38. package/dist/services/final-review/index.js +1 -1
  39. package/dist/services/llm/anthropic-runner.d.ts +87 -0
  40. package/dist/services/llm/anthropic-runner.js +171 -0
  41. package/dist/services/llm/stub-runner.d.ts +11 -0
  42. package/dist/services/llm/stub-runner.js +33 -0
  43. package/dist/services/prd/handoff-auto-regen.js +0 -1
  44. package/dist/services/prd/handoff-service.d.ts +9 -1
  45. package/dist/services/prd/handoff-service.js +48 -6
  46. package/dist/services/prd/project-scan-bootstrap-service.js +7 -7
  47. package/dist/services/scan/api-diff-openapi.d.ts +32 -0
  48. package/dist/services/scan/api-diff-openapi.js +359 -0
  49. package/dist/services/scan/api-diff-recorded.d.ts +96 -0
  50. package/dist/services/scan/api-diff-recorded.js +577 -0
  51. package/dist/services/scan/api-diff-service.d.ts +34 -0
  52. package/dist/services/scan/api-diff-service.js +407 -0
  53. package/dist/services/scan/api-diff-types.d.ts +116 -0
  54. package/dist/services/scan/api-diff-types.js +46 -0
  55. package/dist/services/scan/archetype-service.js +27 -1
  56. package/dist/services/scan/existing-system-service.js +17 -4
  57. package/dist/services/scan/hook-convention-service.d.ts +26 -0
  58. package/dist/services/scan/hook-convention-service.js +562 -0
  59. package/dist/services/scan/scan-types.d.ts +47 -0
  60. package/dist/services/session/caller-binding-service.d.ts +28 -0
  61. package/dist/services/session/caller-binding-service.js +10 -2
  62. package/dist/services/session/caller-id-types.d.ts +12 -2
  63. package/dist/services/session/index.d.ts +2 -2
  64. package/dist/services/session/index.js +2 -2
  65. package/dist/services/session/session-binding-bridge.js +11 -6
  66. package/dist/services/session/session-manager.d.ts +33 -1
  67. package/dist/services/session/session-manager.js +84 -25
  68. package/dist/services/skills/skill-presence-service.d.ts +17 -3
  69. package/dist/services/skills/skill-presence-service.js +23 -3
  70. package/package.json +7 -5
  71. package/skills/bee/peaks-rd/SKILL.md +11 -3
  72. package/skills/peaks-code/references/existing-system-extraction.md +5 -1
  73. package/skills/peaks-code/references/frontend-only-mode.md +48 -6
  74. package/skills/peaks-code/references/project-scan-checklist.md +20 -1
  75. package/skills/peaks-doctor/references/doctor-check-catalog.md +1 -0
  76. package/skills/peaks-final-review/SKILL.md +43 -32
@@ -0,0 +1,359 @@
1
+ /**
2
+ * S1 / rid=api-diff-report — OpenAPI 3.x parsing for `peaks scan api-diff`.
3
+ *
4
+ * `.json` via `JSON.parse`; `.yaml` / `.yml` via the `yaml` runtime dependency.
5
+ * A document that is not OpenAPI 3.x, or that declares no operations, raises
6
+ * `ApiDiffInputError` — the command must never print an empty-but-successful
7
+ * diff.
8
+ *
9
+ * THE SAME INVERTED COMPLETENESS RULE THE RECORDED SIDE GETS APPLIES HERE
10
+ * (QA final gate): `collectFields` returns a *partial* field set just as easily
11
+ * as a complete one, and the two are indistinguishable downstream. Any schema
12
+ * construct this reader cannot fully account for marks its LOCATION as
13
+ * incomplete, which suppresses every exact line for that location. Partial is
14
+ * never presented as complete.
15
+ */
16
+ import { readFileSync } from 'node:fs';
17
+ import { parse as parseYaml } from 'yaml';
18
+ import { ApiDiffInputError, HTTP_METHODS, isRecord } from './api-diff-types.js';
19
+ const MAX_REF_DEPTH = 4;
20
+ /**
21
+ * OpenAPI scalar types mapped to their TypeScript spelling. `integer` is NOT a
22
+ * TypeScript type — an unmapped `integer` produced a false `number -> integer`
23
+ * exact line against a recorded `id: number`.
24
+ */
25
+ const SCALAR_TYPES = {
26
+ string: 'string',
27
+ number: 'number',
28
+ integer: 'number',
29
+ boolean: 'boolean'
30
+ };
31
+ const COMPOSITION_KEYS = ['allOf', 'oneOf', 'anyOf'];
32
+ function refName(ref) {
33
+ const tail = ref.slice(ref.lastIndexOf('/') + 1);
34
+ return tail.length > 0 ? tail : ref;
35
+ }
36
+ /** Resolves a `$ref` chain. Callers that only need the NAME must not use this — see `schemaToTypeString`. */
37
+ function deref(schema, doc, depth) {
38
+ if (!isRecord(schema))
39
+ return null;
40
+ const ref = schema['$ref'];
41
+ if (typeof ref === 'string' && depth < MAX_REF_DEPTH) {
42
+ let node = doc;
43
+ for (const part of ref.replace(/^#\//, '').split('/')) {
44
+ if (!isRecord(node))
45
+ return null;
46
+ node = node[part];
47
+ }
48
+ return deref(node, doc, depth + 1);
49
+ }
50
+ return schema;
51
+ }
52
+ /** The composition/open-shape construct a schema node carries, if any — those cannot be flattened without a compiler. */
53
+ function unreadableConstruct(node) {
54
+ for (const key of COMPOSITION_KEYS) {
55
+ const value = node[key];
56
+ if (Array.isArray(value) && value.length > 0) {
57
+ return `it composes schemas with \`${key}\`, which cannot be flattened here`;
58
+ }
59
+ }
60
+ const additional = node['additionalProperties'];
61
+ if (additional !== undefined && additional !== false) {
62
+ return 'it declares `additionalProperties`, so it may carry fields it does not list';
63
+ }
64
+ return null;
65
+ }
66
+ /**
67
+ * Renders an OpenAPI schema as a TypeScript-ish type text. Arrays render as
68
+ * `Array<X>` (not `X[]`) on purpose: `normalizeType` rewrites both sides
69
+ * identically, so a recorded `X[]` and a document `Array<X>` still compare equal.
70
+ *
71
+ * A `$ref` is answered with its referenced NAME and never dereferenced. Checking
72
+ * after `deref()` made `refName` unreachable for every ref inside
73
+ * `MAX_REF_DEPTH`, so a nested `owner: {$ref: User}` was reported as `object`
74
+ * against a recorded `owner: User` — an exact line for a field that did not
75
+ * change. The name is the faithful reading, and it is what a recorded interface
76
+ * writes.
77
+ */
78
+ function schemaToTypeString(schema, doc, depth) {
79
+ if (isRecord(schema) && typeof schema['$ref'] === 'string') {
80
+ const named = refName(schema['$ref']);
81
+ return schema['nullable'] === true ? `${named} | null` : named;
82
+ }
83
+ const node = deref(schema, doc, depth);
84
+ if (!node)
85
+ return 'unknown';
86
+ const nullable = node['nullable'] === true;
87
+ const enumValues = node['enum'];
88
+ let base;
89
+ if (Array.isArray(enumValues) && enumValues.length > 0) {
90
+ base = enumValues.map((value) => (typeof value === 'string' ? JSON.stringify(value) : String(value))).join(' | ');
91
+ }
92
+ else if (COMPOSITION_KEYS.some((key) => Array.isArray(node[key]) && node[key].length > 0)) {
93
+ const key = COMPOSITION_KEYS.find((candidate) => Array.isArray(node[candidate]));
94
+ const variants = node[key];
95
+ const glue = key === 'allOf' ? ' & ' : ' | ';
96
+ base = variants.map((variant) => schemaToTypeString(variant, doc, depth + 1)).join(glue);
97
+ }
98
+ else if (Array.isArray(node['type'])) {
99
+ // OpenAPI 3.1 spells nullability as `type: ['string', 'null']`. The same
100
+ // SCALAR_TYPES mapping must apply here — joining with bare `String(entry)`
101
+ // leaked an unmapped `integer`, which this file's own comment records as a
102
+ // source of false `number -> integer` exact lines.
103
+ base = node['type']
104
+ .map((entry) => (typeof entry === 'string' && SCALAR_TYPES[entry] !== undefined ? SCALAR_TYPES[entry] : String(entry)))
105
+ .join(' | ');
106
+ }
107
+ else {
108
+ const type = node['type'];
109
+ if (type === 'array') {
110
+ const items = node['items'];
111
+ base = `Array<${items === undefined ? 'unknown' : schemaToTypeString(items, doc, depth + 1)}>`;
112
+ }
113
+ else if (type === 'object') {
114
+ base = 'object';
115
+ }
116
+ else if (typeof type === 'string' && SCALAR_TYPES[type] !== undefined) {
117
+ base = SCALAR_TYPES[type];
118
+ }
119
+ else {
120
+ base = 'unknown';
121
+ }
122
+ }
123
+ return nullable ? `${base} | null` : base;
124
+ }
125
+ /**
126
+ * Flat leaf fields of a body/response schema. Arrays descend one level into
127
+ * `items`. The ROOT `$ref` is resolved on purpose — a top-level `$ref` response
128
+ * IS the interface — while nested refs stay named (see `schemaToTypeString`).
129
+ *
130
+ * A node carrying `allOf`/`oneOf`/`anyOf` or `additionalProperties` alongside its
131
+ * own `properties` is INCOMPLETE: the sibling properties made the location look
132
+ * non-empty, so there was neither suppression nor a note, and a field declared
133
+ * under `allOf` was reported as an exact `-> (absent)`.
134
+ */
135
+ const UNRESOLVED_REF = 'its schema is a `$ref` chain deeper than the reader resolves';
136
+ const NO_PROPERTIES = 'its schema lists no `properties`, so no field set could be read from it';
137
+ function collectFields(schema, doc) {
138
+ const fields = new Map();
139
+ const node = deref(schema, doc, 0);
140
+ if (!node)
141
+ return { fields, incompleteReason: 'its schema could not be read' };
142
+ if (typeof node['$ref'] === 'string')
143
+ return { fields, incompleteReason: UNRESOLVED_REF };
144
+ const rootIssue = unreadableConstruct(node);
145
+ if (rootIssue !== null)
146
+ return { fields, incompleteReason: rootIssue };
147
+ let target = node;
148
+ if (target['type'] === 'array' || target['items'] !== undefined) {
149
+ const items = deref(target['items'], doc, 0);
150
+ if (!items)
151
+ return { fields, incompleteReason: 'its array items schema could not be read' };
152
+ if (typeof items['$ref'] === 'string')
153
+ return { fields, incompleteReason: UNRESOLVED_REF };
154
+ const itemsIssue = unreadableConstruct(items);
155
+ if (itemsIssue !== null)
156
+ return { fields, incompleteReason: itemsIssue };
157
+ target = items;
158
+ }
159
+ const properties = target['properties'];
160
+ // An ABSENT `properties` is not an empty field set: it is a schema this reader
161
+ // cannot produce a field set from at all. Returning `{fields: {}, null}` here
162
+ // dropped the whole location, which is the "empty result treated as a
163
+ // successful one" failure the inverted rule exists to prevent — and dropping
164
+ // a readable `200` also let a `*Response` interface be compared against a
165
+ // surviving `404`, defeating the multi-status guard.
166
+ if (!isRecord(properties))
167
+ return { fields, incompleteReason: NO_PROPERTIES };
168
+ // Per OpenAPI, OMITTING `required` means every property is optional — which is
169
+ // how `openapi-typescript` renders it (`id?: T`). Treating an absent
170
+ // `required` as "everything is required" produced false
171
+ // `string | undefined -> string` lines against generated recordings.
172
+ const requiredRaw = target['required'];
173
+ const required = Array.isArray(requiredRaw)
174
+ ? new Set(requiredRaw.filter((entry) => typeof entry === 'string'))
175
+ : new Set();
176
+ for (const [key, propSchema] of Object.entries(properties)) {
177
+ const type = schemaToTypeString(propSchema, doc, 1);
178
+ fields.set(key, required.has(key) ? type : `${type} | undefined`);
179
+ }
180
+ return { fields, incompleteReason: null };
181
+ }
182
+ function jsonContent(content) {
183
+ if (!isRecord(content))
184
+ return undefined;
185
+ if (content['application/json'] !== undefined)
186
+ return content['application/json'];
187
+ return Object.values(content)[0];
188
+ }
189
+ function schemaOf(media) {
190
+ return isRecord(media) ? media['schema'] : undefined;
191
+ }
192
+ function collectParameters(parameters, locations) {
193
+ if (!Array.isArray(parameters))
194
+ return;
195
+ for (const raw of parameters) {
196
+ if (!isRecord(raw))
197
+ continue;
198
+ const where = raw['in'];
199
+ const name = raw['name'];
200
+ if (where !== 'path' && where !== 'query')
201
+ continue;
202
+ if (typeof name !== 'string')
203
+ continue;
204
+ const key = where === 'path' ? 'pathParams' : 'queryParams';
205
+ const bucket = locations.get(key) ?? new Map();
206
+ bucket.set(name, schemaToTypeString(raw['schema'], {}, 0));
207
+ locations.set(key, bucket);
208
+ }
209
+ }
210
+ export function parseOpenApiDocument(file, doc) {
211
+ const version = doc['openapi'];
212
+ if (typeof version !== 'string' || !/^3\.\d+/.test(version.trim())) {
213
+ throw new ApiDiffInputError('NOT_OPENAPI_3', `${file} is not an OpenAPI 3.x document (expected a top-level \`openapi: 3.x\` string, found ${version === undefined ? 'nothing' : JSON.stringify(version)})`);
214
+ }
215
+ const paths = doc['paths'];
216
+ if (!isRecord(paths)) {
217
+ throw new ApiDiffInputError('NO_PATHS', `${file} declares openapi ${version} but has no \`paths\` object — nothing to diff`);
218
+ }
219
+ const operations = [];
220
+ for (const [path, pathItemRaw] of Object.entries(paths)) {
221
+ const pathItem = deref(pathItemRaw, doc, 0);
222
+ if (!pathItem)
223
+ continue;
224
+ for (const method of HTTP_METHODS) {
225
+ const opRaw = pathItem[method];
226
+ if (!isRecord(opRaw))
227
+ continue;
228
+ const locations = new Map();
229
+ const locationIssues = new Map();
230
+ const record = (location, read) => {
231
+ if (read.fields.size === 0 && read.incompleteReason === null)
232
+ return;
233
+ locations.set(location, read.fields);
234
+ if (read.incompleteReason !== null)
235
+ locationIssues.set(location, read.incompleteReason);
236
+ };
237
+ const shared = Array.isArray(pathItem['parameters']) ? pathItem['parameters'] : [];
238
+ const own = Array.isArray(opRaw['parameters']) ? opRaw['parameters'] : [];
239
+ collectParameters([...shared, ...own], locations);
240
+ const requestBody = opRaw['requestBody'];
241
+ const bodySchema = schemaOf(jsonContent(isRecord(requestBody) ? requestBody['content'] : undefined));
242
+ if (bodySchema !== undefined)
243
+ record('request', collectFields(bodySchema, doc));
244
+ const responses = opRaw['responses'];
245
+ if (isRecord(responses)) {
246
+ for (const [status, responseRaw] of Object.entries(responses)) {
247
+ if (!isRecord(responseRaw))
248
+ continue;
249
+ const responseSchema = schemaOf(jsonContent(responseRaw['content']));
250
+ if (responseSchema === undefined)
251
+ continue;
252
+ record(`response.${status}`, collectFields(responseSchema, doc));
253
+ }
254
+ }
255
+ const operationId = typeof opRaw['operationId'] === 'string' ? opRaw['operationId'] : undefined;
256
+ operations.push(operationId === undefined
257
+ ? { method, path, locations, locationIssues }
258
+ : { method, path, operationId, locations, locationIssues });
259
+ }
260
+ }
261
+ if (operations.length === 0) {
262
+ throw new ApiDiffInputError('NO_OPERATIONS', `${file} is an OpenAPI ${version} document but declares no operations — refusing to print an empty diff`);
263
+ }
264
+ const info = doc['info'];
265
+ const title = isRecord(info) && typeof info['title'] === 'string' ? info['title'] : undefined;
266
+ return title === undefined ? { openapi: version, operations } : { openapi: version, title, operations };
267
+ }
268
+ /** Reads and parses `.json` / `.yaml` / `.yml`. Throws `ApiDiffInputError` on anything else. */
269
+ export function loadApiDocument(file) {
270
+ let raw;
271
+ try {
272
+ raw = readFileSync(file, 'utf8');
273
+ }
274
+ catch (error) {
275
+ throw new ApiDiffInputError('UNREADABLE', `cannot read ${file}: ${error.message}`);
276
+ }
277
+ const lower = file.toLowerCase();
278
+ let parsed;
279
+ try {
280
+ if (lower.endsWith('.yaml') || lower.endsWith('.yml'))
281
+ parsed = parseYaml(raw);
282
+ else if (lower.endsWith('.json') || raw.trimStart().startsWith('{'))
283
+ parsed = JSON.parse(raw);
284
+ else
285
+ parsed = parseYaml(raw);
286
+ }
287
+ catch (error) {
288
+ throw new ApiDiffInputError('PARSE_FAILED', `cannot parse ${file} as JSON or YAML: ${error.message}`);
289
+ }
290
+ if (!isRecord(parsed)) {
291
+ throw new ApiDiffInputError('NOT_AN_OBJECT', `${file} did not parse to an object`);
292
+ }
293
+ return parseOpenApiDocument(file, parsed);
294
+ }
295
+ /** Splits on a separator that sits at bracket depth 0, leaving `("a" | "b")[]` and `Record<string, number>` intact. */
296
+ function splitTopLevel(text, separator) {
297
+ const parts = [];
298
+ let current = '';
299
+ let inString = null;
300
+ let depth = 0;
301
+ for (let i = 0; i < text.length; i += 1) {
302
+ const ch = text[i];
303
+ if (inString !== null) {
304
+ if (ch === '\\') {
305
+ current += ch + (text[i + 1] ?? '');
306
+ i += 1;
307
+ continue;
308
+ }
309
+ if (ch === inString)
310
+ inString = null;
311
+ current += ch;
312
+ continue;
313
+ }
314
+ if (ch === '"' || ch === "'" || ch === '`') {
315
+ inString = ch;
316
+ current += ch;
317
+ continue;
318
+ }
319
+ if (ch === '<' || ch === '(' || ch === '[' || ch === '{')
320
+ depth += 1;
321
+ else if (ch === '>' || ch === ')' || ch === ']' || ch === '}')
322
+ depth = Math.max(0, depth - 1);
323
+ if (depth === 0 && text.startsWith(separator, i)) {
324
+ parts.push(current);
325
+ current = '';
326
+ i += separator.length - 1;
327
+ continue;
328
+ }
329
+ current += ch;
330
+ }
331
+ parts.push(current);
332
+ return parts;
333
+ }
334
+ /**
335
+ * Normalizes type text so `Array<string>`, `string[]`, `string | null` and
336
+ * `string|null` compare equal.
337
+ *
338
+ * Quote style is unified because it is a RENDERING artifact, not a change: the
339
+ * document renders enums via `JSON.stringify` (`"a"`) while hand-written
340
+ * recorded types use single quotes (`'a'`), so the same project written with
341
+ * double quotes emitted nothing and with single quotes emitted an exact
342
+ * CHANGED. Duplicate union members are collapsed for the same reason — a
343
+ * recorded `a?: X` normalises to `X | undefined`, which against a document's
344
+ * `X | undefined` used to become `X | undefined | undefined`.
345
+ */
346
+ export function normalizeType(text) {
347
+ let out = text.trim().replace(/[;,]\s*$/, '').replace(/\s+/g, ' ');
348
+ out = out.replace(/'([^'\\]*)'/g, '"$1"');
349
+ out = out.replace(/\s*\|\s*/g, ' | ').replace(/\s*&\s*/g, ' & ');
350
+ const union = splitTopLevel(out, ' | ');
351
+ if (union.length > 1)
352
+ out = [...new Set(union)].join(' | ');
353
+ out = out.replace(/\s*\[\s*\]/g, '[]');
354
+ for (let pass = 0; pass < 3; pass += 1) {
355
+ out = out.replace(/Array<([^<>]*)>/g, (_all, inner) => (/[|&]/.test(inner) ? `(${inner})[]` : `${inner}[]`));
356
+ }
357
+ out = out.replace(/\s*<\s*/g, '<').replace(/\s*>\s*/g, '>');
358
+ return out.trim();
359
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * S1 / rid=api-diff-report — the three RECORDED sources for `peaks scan api-diff`,
3
+ * plus the candidate name-grep.
4
+ *
5
+ * 1. `.peaks/_runtime/<sid>/rd/mock-plan.md` (most recent by mtime)
6
+ * 2. recorded `*-api.types.ts` interfaces (from (1) + `src/services/types`)
7
+ * 3. the `## API Migration` endpoint list in `.peaks/_runtime/<sid>/txt/*.md`
8
+ *
9
+ * All three are OPTIONAL: absence is reported in the report's `notes`, never
10
+ * silently. `typescript` is a devDependency and is NOT available at runtime, so
11
+ * recorded interfaces are read by the line-based extractor below — its limits
12
+ * are documented on the function and exercised by tests.
13
+ */
14
+ import { type CandidateMention, type DocOperation, type InterfaceRole, type RecordedEndpoint, type RecordedInterface } from './api-diff-types.js';
15
+ export declare const MAX_CANDIDATE_NAMES = 100;
16
+ /**
17
+ * Line-based extractor for recorded `*-api.types.ts` interfaces.
18
+ *
19
+ * INVERTED DEFAULT (QA round 2): a line-based reader cannot tell "this line is
20
+ * not a member" from "I failed to read this member", so the model is not
21
+ * "exact unless I found a reason to suppress". It is the opposite — **an
22
+ * interface is COMPLETE only while every depth-1 line in its body is
23
+ * classified**. The first line the extractor cannot classify makes the whole
24
+ * interface INCOMPLETE, which suppresses every exact line for it and emits one
25
+ * note naming it and the reason.
26
+ *
27
+ * That is what catches, without needing to enumerate them in advance: quoted
28
+ * keys, index signatures, multi-line unions (the continuation line is the
29
+ * unclassifiable one), intersections and mapped types (which never reach a body
30
+ * at all — see `DECLARATION_LIKE`), and nested inline objects.
31
+ *
32
+ * KNOWN LIMITS, stated rather than papered over:
33
+ * - members are matched at depth 1 only; a nested shape is `object` and makes
34
+ * the interface incomplete rather than being half-read;
35
+ * - `extends` is unresolvable without a compiler, so such an interface is
36
+ * incomplete by construction;
37
+ * - a regex literal containing a brace would still mis-count (`braceBalance`).
38
+ */
39
+ export declare function parseRecordedInterfaces(source: string, file: string): RecordedInterface[];
40
+ /**
41
+ * Which half of an operation an interface describes, from the suffix its name
42
+ * carries. `unknown` is a refusal, not a default: diffing an interface against
43
+ * a location it does not describe is how a request field got reported at a
44
+ * response location, and how a response-shaped `*Dto` got reported at the
45
+ * requestBody.
46
+ *
47
+ * Only suffixes that actually name a side are accepted. `Dto` names neither
48
+ * side (a `UserDto` is as likely to be a response), and a bare `Body` does not
49
+ * say which side it belongs to either — `GetThingResponseBody` ends in `Body`.
50
+ * `...RequestBody` still classifies as a request via the `request` suffix. A
51
+ * suffix that carries no information must not receive LESS caution than no
52
+ * suffix at all, so both become `unknown` and are refused with a note.
53
+ */
54
+ export declare function roleOf(name: string): InterfaceRole;
55
+ /** Locations of `operation` that `role` is allowed to describe. */
56
+ export declare function locationsForRole(operation: DocOperation, role: InterfaceRole): string[];
57
+ /** Source 1: the most recent `.peaks/_runtime/<sid>/rd/mock-plan.md`. */
58
+ export declare function findMockPlan(projectRoot: string): string | null;
59
+ /** Pulls the file paths a mock plan records (backticked or whitespace-delimited). */
60
+ export declare function extractMockPlanPaths(source: string): string[];
61
+ /** Source 2: recorded interface files — mock-plan-named plus the prescribed `src/services/types/*-api.types.ts` layout. */
62
+ export declare function findRecordedInterfaceFiles(projectRoot: string, mockPlan: string | null): string[];
63
+ /** Source 3: the newest TXT handoff that actually carries an `## API Migration` section. */
64
+ export declare function findHandoffWithApiMigration(projectRoot: string): string | null;
65
+ /** Parses the `## API Migration` body for `<METHOD> <path>` pairs. */
66
+ export declare function parseHandoffEndpoints(source: string): RecordedEndpoint[];
67
+ /**
68
+ * Key used to pair a document operation with a recorded interface. Deliberately
69
+ * conservative: only lowercasing, punctuation removal, and one trailing
70
+ * `Response`/`Request`/`Dto`/`Payload`/`Body` strip. Verb stripping (`getUser`
71
+ * -> `user`) was considered and rejected — a false pairing produces a
72
+ * confidently wrong diff, which is worse than the visible false negative of an
73
+ * unpaired interface (design §2.3).
74
+ */
75
+ export declare function pairingKey(name: string): string;
76
+ export declare function operationKey(operation: DocOperation): string;
77
+ /**
78
+ * Greps every candidate name across the consumer's source and non-TS assets.
79
+ * Over-reports (test fixtures, unrelated identifiers) and under-reports (i18n
80
+ * keys, AntD `columns` arrays, monorepo barrels) — the report says so.
81
+ *
82
+ * `exclude` carries the OpenAPI document itself: it is the input, not a change
83
+ * site, and every field name would otherwise match on its own definition line.
84
+ */
85
+ export declare function grepCandidateMentions(projectRoot: string, names: readonly string[], exclude?: readonly string[]): {
86
+ mentions: CandidateMention[];
87
+ truncated: boolean;
88
+ hitsTruncated: boolean;
89
+ };
90
+ /**
91
+ * Canonical endpoint path for comparison: `{id}` and `:id` are the same
92
+ * endpoint spelled two ways, and raw equality reported them as an exact
93
+ * ADDED + REMOVED pair. Parameter NAMES are collapsed too — `/users/{id}` and
94
+ * `/users/{userId}` are one endpoint.
95
+ */
96
+ export declare function normalizeEndpointPath(path: string): string;