@withtruing/companion 0.13.3 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +24 -25
  2. package/dist/anthropic-output-schema.js +41 -0
  3. package/dist/api-key-caller.js +9 -2
  4. package/dist/claude-cli-caller.js +107 -8
  5. package/dist/cli.js +833 -206
  6. package/dist/codex-cli-caller.js +85 -14
  7. package/dist/config.js +177 -2
  8. package/dist/git-range.js +88 -46
  9. package/dist/http-client.js +107 -10
  10. package/dist/investigation-reader.js +12 -6
  11. package/dist/managed-repo.js +132 -0
  12. package/dist/observed-sources.js +132 -6
  13. package/dist/reader-result-contract.js +296 -2
  14. package/dist/repository-provider.js +608 -0
  15. package/dist/schema-entities.js +18 -18
  16. package/dist/service.js +7 -2
  17. package/dist/surface-mapper.js +53 -11
  18. package/dist/text-model.js +91 -8
  19. package/dist/whole-read/archive.js +196 -0
  20. package/dist/whole-read/checks.js +297 -0
  21. package/dist/whole-read/claims.js +196 -0
  22. package/dist/whole-read/coverage-ledger.js +51 -0
  23. package/dist/whole-read/decode.js +234 -0
  24. package/dist/whole-read/drafting.js +502 -0
  25. package/dist/whole-read/generated-rules.js +68 -0
  26. package/dist/whole-read/go-proof.js +255 -0
  27. package/dist/whole-read/grouping-prompt.js +19 -0
  28. package/dist/whole-read/grouping.js +247 -0
  29. package/dist/whole-read/identity.js +103 -0
  30. package/dist/whole-read/import-graph.js +293 -0
  31. package/dist/whole-read/inventory-collector.js +55 -0
  32. package/dist/whole-read/inventory-rules.js +141 -0
  33. package/dist/whole-read/lockfiles.js +947 -0
  34. package/dist/whole-read/notes.js +216 -0
  35. package/dist/whole-read/npm-proof.js +272 -0
  36. package/dist/whole-read/owner-decision-detectors.js +270 -0
  37. package/dist/whole-read/packing.js +612 -0
  38. package/dist/whole-read/pages.js +429 -0
  39. package/dist/whole-read/phase-policy.js +54 -0
  40. package/dist/whole-read/prd-build.js +279 -0
  41. package/dist/whole-read/prd-fidelity.js +14 -0
  42. package/dist/whole-read/python-proof.js +489 -0
  43. package/dist/whole-read/read-prompt.js +67 -0
  44. package/dist/whole-read/redaction.js +411 -0
  45. package/dist/whole-read/relationship-mapping.js +98 -0
  46. package/dist/whole-read/sanitize.js +266 -0
  47. package/dist/whole-read/secret-rules.js +26 -0
  48. package/dist/whole-read/structure.js +300 -0
  49. package/dist/whole-read/types.js +2 -0
  50. package/dist/whole-read-claims.js +12 -0
  51. package/dist/whole-read-git.js +340 -0
  52. package/dist/whole-read-pool.js +80 -0
  53. package/dist/whole-read-runner.js +945 -0
  54. package/dist/whole-read-shared-source.js +194 -0
  55. package/dist/whole-read-temp.js +479 -0
  56. package/package.json +8 -2
@@ -0,0 +1,266 @@
1
+ import { validateNoSecretValues } from './redaction.js';
2
+ const SAFE_KEY = /^[A-Za-z][A-Za-z0-9_-]{0,127}$/;
3
+ const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$/;
4
+ const ABSOLUTE_HOST_PATH = /(?:^|[\s"'(])(?:[A-Za-z]:[\\/]|\\\\|\/(?:home|Users|tmp|var\/tmp|private\/tmp)\/)/;
5
+ const FORBIDDEN_FIELDS = new Set([
6
+ 'prompt',
7
+ 'rawprompt',
8
+ 'stdout',
9
+ 'stderr',
10
+ 'rawoutput',
11
+ 'rawmodeloutput',
12
+ 'sourceexcerpt',
13
+ ]);
14
+ const BUILD_FAILURE_CODES = new Set([
15
+ 'usage_limit',
16
+ 'provider_rate_limit',
17
+ 'provider_auth',
18
+ 'provider_unavailable',
19
+ 'provider_request_rejected',
20
+ 'billing_exhausted',
21
+ 'capability_failure',
22
+ 'model_declined',
23
+ 'output_truncated',
24
+ 'companion_offline',
25
+ 'github_rate_limit',
26
+ 'consent_invalidated',
27
+ 'allowance_reached',
28
+ 'snapshot_unavailable',
29
+ 'source_acquisition_required',
30
+ 'cleanup_pending',
31
+ 'read_result_invalid',
32
+ 'draft_attempt_invalid',
33
+ 'draft_input_invalid',
34
+ 'draft_note_invalid',
35
+ 'check_attempt_invalid',
36
+ 'check_input_invalid',
37
+ 'check_claim_invalid',
38
+ 'check_note_use_invalid',
39
+ 'check_result_invalid',
40
+ 'claim_check_input_invalid',
41
+ 'claim_check_citation_invalid',
42
+ 'claim_check_result_invalid',
43
+ 'grouping_attempt_invalid',
44
+ 'grouping_input_invalid',
45
+ 'relationship_attempt_invalid',
46
+ 'relationship_input_invalid',
47
+ 'relationship_result_invalid',
48
+ 'canonical_source_invalid',
49
+ 'internal_error',
50
+ ]);
51
+ const BUILD_FAILURE_SOURCES = new Set([
52
+ 'claude_cli',
53
+ 'codex',
54
+ 'anthropic_api',
55
+ 'github',
56
+ 'server',
57
+ 'companion',
58
+ ]);
59
+ const DIAGNOSTIC_OPERATIONS = new Set([
60
+ 'collection_page',
61
+ 'github_archive',
62
+ 'github_blob',
63
+ 'github_tree',
64
+ 'git_blob',
65
+ 'git_tree',
66
+ 'model_request',
67
+ 'registry_lookup',
68
+ ]);
69
+ const CLEANUP_ERROR_CODES = new Set([
70
+ 'cleanup_delete_failed',
71
+ 'cleanup_reader_cancel_failed',
72
+ 'cleanup_path_outside_root',
73
+ 'cleanup_unsafe_link',
74
+ 'cleanup_marker_invalid',
75
+ ]);
76
+ const isRecord = (value) => value !== null && typeof value === 'object' && !Array.isArray(value)
77
+ && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
78
+ const exactFields = (value, required, optional = []) => {
79
+ const fields = new Set([...required, ...optional]);
80
+ return required.every((field) => Object.hasOwn(value, field))
81
+ && Object.keys(value).every((field) => fields.has(field));
82
+ };
83
+ const isSafeId = (value) => typeof value === 'string' && SAFE_ID.test(value);
84
+ const isCount = (value) => typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
85
+ const isIsoTime = (value) => {
86
+ if (typeof value !== 'string')
87
+ return false;
88
+ const time = Date.parse(value);
89
+ return Number.isFinite(time) && new Date(time).toISOString() === value;
90
+ };
91
+ const containsHostPath = (value) => ABSOLUTE_HOST_PATH.test(value);
92
+ function containsCodeQuote(value) {
93
+ const characters = Array.from(value);
94
+ if (characters.length <= 120)
95
+ return false;
96
+ const tokens = [];
97
+ for (let index = 0; index < characters.length; index += 1) {
98
+ const character = characters[index];
99
+ if (character === '=' && characters[index + 1] === '>') {
100
+ tokens.push({ start: index, end: index + 2 });
101
+ index += 1;
102
+ }
103
+ else if (character === '{' || character === '}' || character === ';'
104
+ || character === '(' || character === ')') {
105
+ tokens.push({ start: index, end: index + 1 });
106
+ }
107
+ }
108
+ for (let index = 0; index + 2 < tokens.length; index += 1) {
109
+ if (tokens[index + 2].end - tokens[index].start <= 40)
110
+ return true;
111
+ }
112
+ return false;
113
+ }
114
+ function inspectString(value) {
115
+ if (validateNoSecretValues(value).ok === false)
116
+ return 'secret_value_detected';
117
+ return containsCodeQuote(value) ? 'code_quote_detected' : null;
118
+ }
119
+ function cloneMetadata(value, allowCleanupPath, field, ancestors = new Set(), depth = 0) {
120
+ if (depth > 64)
121
+ return { ok: false, code: 'metadata_schema_invalid' };
122
+ if (value === null || typeof value === 'boolean')
123
+ return { ok: true, value };
124
+ if (typeof value === 'number') {
125
+ return Number.isFinite(value)
126
+ ? { ok: true, value }
127
+ : { ok: false, code: 'metadata_schema_invalid' };
128
+ }
129
+ if (typeof value === 'string') {
130
+ const pathField = field?.toLowerCase().endsWith('path') === true;
131
+ if (!allowCleanupPath && containsHostPath(value)) {
132
+ return { ok: false, code: 'cleanup_path_forbidden' };
133
+ }
134
+ if (pathField && allowCleanupPath) {
135
+ return validateNoSecretValues(value).ok
136
+ ? { ok: true, value }
137
+ : { ok: false, code: 'secret_value_detected' };
138
+ }
139
+ const unsafe = inspectString(value);
140
+ return unsafe ? { ok: false, code: unsafe } : { ok: true, value };
141
+ }
142
+ if (!value || typeof value !== 'object' || ancestors.has(value)) {
143
+ return { ok: false, code: 'metadata_schema_invalid' };
144
+ }
145
+ ancestors.add(value);
146
+ if (Array.isArray(value)) {
147
+ const result = [];
148
+ for (const item of value) {
149
+ const safe = cloneMetadata(item, allowCleanupPath, undefined, ancestors, depth + 1);
150
+ if (!safe.ok)
151
+ return safe;
152
+ result.push(safe.value);
153
+ }
154
+ ancestors.delete(value);
155
+ return { ok: true, value: result };
156
+ }
157
+ if (!isRecord(value))
158
+ return { ok: false, code: 'metadata_schema_invalid' };
159
+ const result = {};
160
+ for (const [key, item] of Object.entries(value)) {
161
+ const normalizedKey = key.toLowerCase().replace(/[_-]/g, '');
162
+ if (!SAFE_KEY.test(key) || FORBIDDEN_FIELDS.has(normalizedKey)) {
163
+ return { ok: false, code: 'metadata_schema_invalid' };
164
+ }
165
+ const safeKey = inspectString(key);
166
+ if (safeKey)
167
+ return { ok: false, code: safeKey };
168
+ const safe = cloneMetadata(item, allowCleanupPath, key, ancestors, depth + 1);
169
+ if (!safe.ok)
170
+ return safe;
171
+ result[key] = safe.value;
172
+ }
173
+ ancestors.delete(value);
174
+ return { ok: true, value: result };
175
+ }
176
+ function validBuildFailure(value) {
177
+ if (!isRecord(value) || !exactFields(value, ['code', 'source'], ['resetAt', 'retryAfterMs', 'category']))
178
+ return false;
179
+ if (typeof value.code !== 'string' || !BUILD_FAILURE_CODES.has(value.code)
180
+ || typeof value.source !== 'string' || !BUILD_FAILURE_SOURCES.has(value.source))
181
+ return false;
182
+ if (value.resetAt !== undefined && !isIsoTime(value.resetAt))
183
+ return false;
184
+ if (value.category !== undefined && value.category !== null
185
+ && (typeof value.category !== 'string' || !SAFE_ID.test(value.category)))
186
+ return false;
187
+ return value.retryAfterMs === undefined
188
+ || (typeof value.retryAfterMs === 'number' && Number.isFinite(value.retryAfterMs)
189
+ && value.retryAfterMs >= 0);
190
+ }
191
+ function validDiagnostic(value) {
192
+ if (!isRecord(value)
193
+ || !exactFields(value, ['operation'], ['objectId', 'count', 'retryAt', 'retryAfterMs']))
194
+ return false;
195
+ if (typeof value.operation !== 'string' || !DIAGNOSTIC_OPERATIONS.has(value.operation))
196
+ return false;
197
+ if (value.objectId !== undefined && !isSafeId(value.objectId))
198
+ return false;
199
+ if (value.count !== undefined && !isCount(value.count))
200
+ return false;
201
+ if (value.retryAt !== undefined && !isIsoTime(value.retryAt))
202
+ return false;
203
+ return value.retryAfterMs === undefined
204
+ || (typeof value.retryAfterMs === 'number' && Number.isFinite(value.retryAfterMs)
205
+ && value.retryAfterMs >= 0);
206
+ }
207
+ function validResultReference(value) {
208
+ if (!isRecord(value))
209
+ return false;
210
+ if (Object.keys(value).length === 0)
211
+ return true;
212
+ if (value.type !== 'sealed_build_collection') {
213
+ return ['collection', 'ref', 'publishedLane'].some((field) => exactFields(value, [field]) && isSafeId(value[field]));
214
+ }
215
+ return exactFields(value, ['type', 'protocolVersion', 'collectionId', 'kind', 'digest'])
216
+ && value.protocolVersion === 'whole-read-pages@1'
217
+ && isSafeId(value.collectionId)
218
+ && ['inventory', 'notes', 'groups', 'claims', 'progress'].includes(String(value.kind))
219
+ && typeof value.digest === 'string'
220
+ && /^sha256:[0-9a-f]{64}$/.test(value.digest);
221
+ }
222
+ function validCleanupRecord(value) {
223
+ if (!isRecord(value) || !exactFields(value, [
224
+ 'workspaceId',
225
+ 'buildId',
226
+ 'attemptId',
227
+ 'hostId',
228
+ 'leaseExpiresAt',
229
+ 'locationId',
230
+ 'path',
231
+ 'state',
232
+ 'cleanupAttempts',
233
+ 'errorCode',
234
+ 'deletedAt',
235
+ ]))
236
+ return false;
237
+ return isSafeId(value.workspaceId)
238
+ && isSafeId(value.buildId)
239
+ && isSafeId(value.attemptId)
240
+ && isSafeId(value.hostId)
241
+ && isIsoTime(value.leaseExpiresAt)
242
+ && isSafeId(value.locationId)
243
+ && typeof value.path === 'string'
244
+ && value.path.length > 0
245
+ && value.path.length <= 4096
246
+ && !value.path.includes('\0')
247
+ && (value.state === 'deleted' || value.state === 'cleanup_pending')
248
+ && isCount(value.cleanupAttempts)
249
+ && (value.errorCode === null
250
+ || (typeof value.errorCode === 'string' && CLEANUP_ERROR_CODES.has(value.errorCode)))
251
+ && (value.deletedAt === null || isIsoTime(value.deletedAt));
252
+ }
253
+ /**
254
+ * Copies JSON metadata only after recursively checking every string. Unsafe records are rejected;
255
+ * source-like or secret-bearing values are never shortened into apparently valid metadata.
256
+ */
257
+ export function sanitizeBuildMetadata(value, schema = 'metadata') {
258
+ const schemaValid = schema === 'metadata'
259
+ || (schema === 'build_failure' && validBuildFailure(value))
260
+ || (schema === 'diagnostic' && validDiagnostic(value))
261
+ || (schema === 'result_reference' && validResultReference(value))
262
+ || (schema === 'owner_scoped_cleanup' && validCleanupRecord(value));
263
+ if (!schemaValid)
264
+ return { ok: false, code: 'metadata_schema_invalid' };
265
+ return cloneMetadata(value, schema === 'owner_scoped_cleanup');
266
+ }
@@ -0,0 +1,26 @@
1
+ /** Closed, deliberately conservative secret-rules@1 matcher table. */
2
+ export const SECRET_RULES_VERSION = 'secret-rules@1';
3
+ export const SECRET_RULES = [
4
+ { id: 'private-key', priority: 100, matcher: /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/g,
5
+ terminator: /-----END [A-Z0-9 ]*PRIVATE KEY-----/g, capture: 'private-key' },
6
+ // Anchor at the maximal scheme run so a failed URL cannot rescan a long run from every letter.
7
+ // The url capture starts at its first letter, preserving the previous match boundary exactly.
8
+ { id: 'url-credentials', priority: 95, matcher: /(?<![a-z0-9+.-])(?=[a-z0-9+.-]*[a-z])[a-z0-9+.-]+:\/\/[^\s/:@]+:[^\s/@]+@[^\s/]+[^\s]*/gi, capture: 'url' },
9
+ // The scanner checks each maximal JWT segment once; \b can also occur after every hyphen in a run.
10
+ { id: 'jwt', priority: 90, matcher: /[a-zA-Z0-9_-]+/g, capture: 'jwt' },
11
+ { id: 'aws-access-key', priority: 80, matcher: /\b(?:AKIA|ASIA|AIDA|AROA)[A-Z0-9]{16}\b/g, capture: 'whole' },
12
+ { id: 'aws-secret-key', priority: 80, matcher: /\baws_secret_access_key\s*[=:]\s*([A-Za-z0-9/+=]{40})\b/gi, capture: 'aws-secret' },
13
+ { id: 'gcp-api-key', priority: 80, matcher: /\bAIza[0-9A-Za-z_-]{35}\b/g, capture: 'whole' },
14
+ { id: 'azure-storage-connection-string', priority: 80, matcher: /\bDefaultEndpointsProtocol=https?;AccountName=[A-Za-z0-9-]+;AccountKey=[A-Za-z0-9+/=]+(?:;EndpointSuffix=[A-Za-z0-9.-]+)?\b/g, capture: 'whole' },
15
+ // The scanner parses each maximal query run once; the former expression could rescan it at every ?sv=.
16
+ { id: 'azure-sas-token', priority: 80, matcher: /[^\s#]+/g, capture: 'azure-sas' },
17
+ { id: 'github-token', priority: 80, matcher: /\b(?:gh[pousr]_[A-Za-z0-9]{36,255}|github_pat_[A-Za-z0-9_]{22,255})\b/g, capture: 'whole' },
18
+ { id: 'gitlab-token', priority: 80, matcher: /\bglpat-[A-Za-z0-9_-]{20,255}\b/g, capture: 'whole' },
19
+ { id: 'slack-token', priority: 80, matcher: /\bxox[baprs]-[A-Za-z0-9-]{10,255}\b/g, capture: 'whole' },
20
+ { id: 'stripe-key', priority: 80, matcher: /\b(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{16,255}\b/g, capture: 'whole' },
21
+ { id: 'openai-token', priority: 80, matcher: /\bsk-(?:proj-)?[A-Za-z0-9_-]{20,255}\b/g, capture: 'whole' },
22
+ { id: 'anthropic-token', priority: 80, matcher: /\bsk-ant-[A-Za-z0-9_-]{20,255}\b/g, capture: 'whole' },
23
+ // Match each maximal assignment name once; the scanner applies the exact keyword/boundary and value rules.
24
+ { id: 'named-high-entropy-assignment', priority: 10, matcher: /(?<![A-Za-z0-9_.-])[A-Za-z0-9_.-]+\s*[=:]/g,
25
+ keywords: ['KEY', 'SECRET', 'TOKEN', 'PASSWORD'], capture: 'assignment' },
26
+ ];
@@ -0,0 +1,300 @@
1
+ import { structureDigest } from './identity.js';
2
+ const COLORS = ['#2563eb', '#7c3aed', '#db2777', '#ea580c', '#ca8a04', '#16a34a', '#0891b2', '#4f46e5'];
3
+ const METADATA_NAMES = new Set([
4
+ 'package.json', 'package-lock.json', 'npm-shrinkwrap.json', 'pnpm-lock.yaml', 'yarn.lock',
5
+ 'bun.lock', 'bun.lockb', 'cargo.toml', 'cargo.lock', 'go.mod', 'go.sum', 'go.work',
6
+ 'pyproject.toml', 'setup.py', 'pom.xml', 'build.gradle', 'build.gradle.kts', 'gemfile',
7
+ 'gemfile.lock', 'composer.json', 'composer.lock', 'pubspec.yaml', 'package.swift', 'mix.exs',
8
+ 'readme', 'readme.md', 'readme.txt', 'license', 'license.md', 'notice', '.gitignore',
9
+ '.gitattributes', '.npmignore',
10
+ ]);
11
+ const SOURCE_EXTENSIONS = new Set([
12
+ '.c', '.cc', '.cpp', '.cs', '.css', '.dart', '.ex', '.exs', '.go', '.h', '.hpp', '.html',
13
+ '.java', '.js', '.jsx', '.kt', '.kts', '.m', '.mm', '.php', '.py', '.rb', '.rs', '.scala',
14
+ '.scss', '.sh', '.sql', '.swift', '.ts', '.tsx', '.vue', '.xml', '.yaml', '.yml',
15
+ ]);
16
+ const invalid = () => { throw new TypeError('structure_invalid'); };
17
+ const pathParts = (path) => path.split('/').filter(Boolean);
18
+ const directory = (path) => {
19
+ const parts = pathParts(path);
20
+ parts.pop();
21
+ return parts.join('/');
22
+ };
23
+ const baseName = (path) => pathParts(path).at(-1) ?? '';
24
+ const isUnder = (path, root) => root === '' || path === root || path.startsWith(`${root}/`);
25
+ const depth = (root) => root === '' ? 0 : pathParts(root).length;
26
+ const entryKey = (entry) => `${entry.sourceId}\u0000${entry.pathBase64}`;
27
+ const candidateKey = (sourceId, root) => `${sourceId}\u0000${root}`;
28
+ const laneId = (sourceId, root) => `lane-${structureDigest({ root, sourceId }).slice('sha256:'.length, 'sha256:'.length + 24)}`;
29
+ const proofFor = (entry) => ({
30
+ entryId: entry.id, sourceId: entry.sourceId, blobId: entry.blobId,
31
+ pathBase64: entry.pathBase64, pathDisplay: entry.pathDisplay,
32
+ });
33
+ function text(bytes) {
34
+ if (!bytes)
35
+ return undefined;
36
+ try {
37
+ return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
38
+ }
39
+ catch {
40
+ return undefined;
41
+ }
42
+ }
43
+ function packageInfo(bytes) {
44
+ const source = text(bytes);
45
+ if (source === undefined)
46
+ return { workspace: false, uncertain: true };
47
+ try {
48
+ const parsed = JSON.parse(source);
49
+ return {
50
+ name: typeof parsed.name === 'string' && parsed.name.trim() ? parsed.name : undefined,
51
+ workspace: parsed.workspaces !== undefined,
52
+ uncertain: false,
53
+ };
54
+ }
55
+ catch {
56
+ return { workspace: false, uncertain: true };
57
+ }
58
+ }
59
+ function cargoInfo(bytes) {
60
+ const source = text(bytes);
61
+ if (source === undefined)
62
+ return { package: false, workspace: false, uncertain: true };
63
+ return {
64
+ package: /(?:^|\n)\s*\[package\]\s*(?:#.*)?(?:\n|$)/.test(source),
65
+ workspace: /(?:^|\n)\s*\[workspace\]\s*(?:#.*)?(?:\n|$)/.test(source),
66
+ uncertain: false,
67
+ };
68
+ }
69
+ function manifestKind(name) {
70
+ return name === 'go.mod' || name === 'pyproject.toml' || name === 'setup.py' || name === 'pom.xml'
71
+ || name === 'build.gradle' || name === 'build.gradle.kts' || name === 'gemfile'
72
+ || name === 'composer.json' || name === 'pubspec.yaml' || name === 'package.swift'
73
+ || name === 'mix.exs' || name.endsWith('.csproj');
74
+ }
75
+ function isKnownNonSource(entry) {
76
+ const name = baseName(entry.pathDisplay).toLowerCase();
77
+ return METADATA_NAMES.has(name) || name.endsWith('.csproj');
78
+ }
79
+ function sourceStatus(entry) {
80
+ if (entry.mode !== '100644' && entry.mode !== '100755')
81
+ return 'non-source';
82
+ if (isKnownNonSource(entry))
83
+ return 'non-source';
84
+ const name = baseName(entry.pathDisplay).toLowerCase();
85
+ const dot = name.lastIndexOf('.');
86
+ if (dot >= 0 && SOURCE_EXTENSIONS.has(name.slice(dot)))
87
+ return 'source';
88
+ return 'uncertain';
89
+ }
90
+ function defaultName(root) {
91
+ return root === '' ? 'Repository root' : root;
92
+ }
93
+ function canonicalRevision(inventoryRevision, lanes, assignments, merges) {
94
+ const value = {
95
+ inventoryRevision,
96
+ lanes: lanes.map(({ id, name, color, order }) => ({ id, name, color, order })),
97
+ assignments: [...assignments].sort((left, right) => left.entryId.localeCompare(right.entryId)),
98
+ merges: merges.map((merge) => ({ fromLaneIds: [...merge.fromLaneIds].sort(), intoLaneId: merge.intoLaneId })),
99
+ };
100
+ return { buildId: '', ...value, structureDigest: structureDigest(value) };
101
+ }
102
+ /**
103
+ * Mechanically derives lanes from an already-sealed inventory. Manifest contents are parsed as
104
+ * bytes only; no manifest is loaded, imported, or executed.
105
+ */
106
+ export function deriveStructure(input) {
107
+ if (!Number.isSafeInteger(input.inventoryRevision) || input.inventoryRevision < 1)
108
+ invalid();
109
+ const entries = [...input.entries];
110
+ const seenEntries = new Set();
111
+ const seenIds = new Set();
112
+ for (const entry of entries) {
113
+ if (!entry.id || !entry.sourceId || !entry.pathBase64 || !entry.pathDisplay || !entry.blobId
114
+ || seenEntries.has(entryKey(entry)) || seenIds.has(entry.id))
115
+ invalid();
116
+ seenEntries.add(entryKey(entry));
117
+ seenIds.add(entry.id);
118
+ }
119
+ const candidates = new Map();
120
+ const add = (entry, name, workspaceAggregator = false, uncertain = false) => {
121
+ const root = directory(entry.pathDisplay);
122
+ const key = candidateKey(entry.sourceId, root);
123
+ const existing = candidates.get(key);
124
+ if (existing) {
125
+ existing.proofs.push(proofFor(entry));
126
+ existing.workspaceAggregator ||= workspaceAggregator;
127
+ existing.uncertain ||= uncertain;
128
+ return;
129
+ }
130
+ candidates.set(key, { sourceId: entry.sourceId, root, name, proofs: [proofFor(entry)], workspaceAggregator, uncertain });
131
+ };
132
+ for (const entry of entries) {
133
+ if (entry.mode !== '100644' && entry.mode !== '100755')
134
+ continue;
135
+ const name = baseName(entry.pathDisplay);
136
+ const lower = name.toLowerCase();
137
+ if (lower === 'package.json') {
138
+ const info = packageInfo(input.readCanonicalBytes(entry));
139
+ if (info.name)
140
+ add(entry, info.name, info.workspace);
141
+ else if (info.uncertain)
142
+ add(entry, defaultName(directory(entry.pathDisplay)), true, true);
143
+ }
144
+ else if (lower === 'cargo.toml') {
145
+ const info = cargoInfo(input.readCanonicalBytes(entry));
146
+ if (info.package || info.uncertain)
147
+ add(entry, defaultName(directory(entry.pathDisplay)), info.workspace && !info.package, info.uncertain);
148
+ }
149
+ else if (manifestKind(lower)) {
150
+ add(entry, defaultName(directory(entry.pathDisplay)));
151
+ }
152
+ }
153
+ const candidateList = [...candidates.values()];
154
+ const kept = candidateList.filter((candidate) => {
155
+ if (!candidate.workspaceAggregator || candidate.uncertain)
156
+ return true;
157
+ for (const entry of entries) {
158
+ if (entry.sourceId !== candidate.sourceId || !isUnder(entry.pathDisplay, candidate.root))
159
+ continue;
160
+ const deeper = candidateList.some((other) => other !== candidate && other.sourceId === candidate.sourceId
161
+ && depth(other.root) > depth(candidate.root) && isUnder(entry.pathDisplay, other.root));
162
+ if (!deeper && sourceStatus(entry) !== 'non-source')
163
+ return true;
164
+ }
165
+ return false;
166
+ }).sort((left, right) => left.sourceId.localeCompare(right.sourceId) || left.root.localeCompare(right.root));
167
+ const lanes = kept.map((candidate, index) => ({
168
+ id: laneId(candidate.sourceId, candidate.root), name: candidate.name,
169
+ color: COLORS[index % COLORS.length], order: index,
170
+ manifestProofs: [...candidate.proofs].sort((left, right) => left.pathBase64.localeCompare(right.pathBase64)),
171
+ }));
172
+ const laneByCandidate = new Map(kept.map((candidate, index) => [candidateKey(candidate.sourceId, candidate.root), lanes[index]]));
173
+ const assignments = [];
174
+ const rootEntries = [];
175
+ for (const entry of entries) {
176
+ const owner = kept.filter((candidate) => candidate.sourceId === entry.sourceId && isUnder(entry.pathDisplay, candidate.root))
177
+ .sort((left, right) => depth(right.root) - depth(left.root))[0];
178
+ const lane = owner ? laneByCandidate.get(candidateKey(owner.sourceId, owner.root)) : undefined;
179
+ if (lane)
180
+ assignments.push({ entryId: entry.id, laneId: lane.id });
181
+ else
182
+ rootEntries.push(entry);
183
+ }
184
+ if (rootEntries.length > 0) {
185
+ const rootLane = {
186
+ id: 'repository-root', name: 'Repository root', color: COLORS[lanes.length % COLORS.length],
187
+ order: lanes.length, manifestProofs: [],
188
+ };
189
+ lanes.push(rootLane);
190
+ assignments.push(...rootEntries.map((entry) => ({ entryId: entry.id, laneId: rootLane.id })));
191
+ }
192
+ const revision = canonicalRevision(input.inventoryRevision, lanes, assignments, []);
193
+ return { ...revision, lanes };
194
+ }
195
+ /** Includes declared generator inputs without implicitly selecting or publishing their lanes. */
196
+ export function deriveSelectedLaneReadPlan(input) {
197
+ const laneIds = new Set(input.structure.lanes.map((lane) => lane.id));
198
+ const publishedLaneIds = [...input.selectedLaneIds];
199
+ if (publishedLaneIds.length === 0 || new Set(publishedLaneIds).size !== publishedLaneIds.length
200
+ || publishedLaneIds.some((laneId) => !laneIds.has(laneId)))
201
+ invalid();
202
+ const laneByEntry = new Map();
203
+ for (const assignment of input.structure.assignments) {
204
+ if (!assignment.entryId || laneByEntry.has(assignment.entryId) || !laneIds.has(assignment.laneId))
205
+ invalid();
206
+ laneByEntry.set(assignment.entryId, assignment.laneId);
207
+ }
208
+ const dependencyKeys = new Set();
209
+ for (const dependency of input.generatedSourceDependencies) {
210
+ const key = `${dependency.generatedEntryId}\0${dependency.sourceEntryId}`;
211
+ if (!laneByEntry.has(dependency.generatedEntryId) || !laneByEntry.has(dependency.sourceEntryId)
212
+ || dependency.generatedEntryId === dependency.sourceEntryId || dependencyKeys.has(key))
213
+ invalid();
214
+ dependencyKeys.add(key);
215
+ }
216
+ const selected = new Set(publishedLaneIds);
217
+ const included = new Set();
218
+ const requiredBy = new Map();
219
+ for (const [entryId, laneId] of laneByEntry) {
220
+ if (!selected.has(laneId))
221
+ continue;
222
+ included.add(entryId);
223
+ requiredBy.set(entryId, laneId);
224
+ }
225
+ const recorded = [];
226
+ let changed = true;
227
+ while (changed) {
228
+ changed = false;
229
+ for (const dependency of input.generatedSourceDependencies) {
230
+ if (!included.has(dependency.generatedEntryId))
231
+ continue;
232
+ const sourceLaneId = laneByEntry.get(dependency.sourceEntryId);
233
+ const requiredByLaneId = requiredBy.get(dependency.generatedEntryId);
234
+ if (!recorded.some((value) => value.generatedEntryId === dependency.generatedEntryId
235
+ && value.sourceEntryId === dependency.sourceEntryId)) {
236
+ recorded.push({ ...dependency, sourceLaneId, requiredByLaneId });
237
+ }
238
+ if (!included.has(dependency.sourceEntryId)) {
239
+ included.add(dependency.sourceEntryId);
240
+ requiredBy.set(dependency.sourceEntryId, requiredByLaneId);
241
+ changed = true;
242
+ }
243
+ }
244
+ }
245
+ return {
246
+ publishedLaneIds,
247
+ entryIds: [...included].sort(),
248
+ generatedSourceDependencies: recorded.sort((left, right) => left.generatedEntryId.localeCompare(right.generatedEntryId) || left.sourceEntryId.localeCompare(right.sourceEntryId)),
249
+ };
250
+ }
251
+ export function reviseStructureRevision(current, edits) {
252
+ const lanes = current.lanes.map((lane) => ({ ...lane }));
253
+ const assignments = current.assignments.map((assignment) => ({ ...assignment }));
254
+ const merges = current.merges.map((merge) => ({ fromLaneIds: [...merge.fromLaneIds], intoLaneId: merge.intoLaneId }));
255
+ const requireLane = (id) => {
256
+ const lane = lanes.find((candidate) => candidate.id === id);
257
+ if (!lane)
258
+ throw new TypeError('structure_edit_invalid');
259
+ return lane;
260
+ };
261
+ for (const edit of edits) {
262
+ if (edit.kind === 'rename') {
263
+ if (!edit.name.trim())
264
+ throw new TypeError('structure_edit_invalid');
265
+ requireLane(edit.laneId).name = edit.name;
266
+ }
267
+ else if (edit.kind === 'recolor') {
268
+ if (!/^#[0-9a-f]{6}$/i.test(edit.color))
269
+ throw new TypeError('structure_edit_invalid');
270
+ requireLane(edit.laneId).color = edit.color;
271
+ }
272
+ else if (edit.kind === 'reorder') {
273
+ if (edit.laneIds.length !== lanes.length || new Set(edit.laneIds).size !== edit.laneIds.length
274
+ || edit.laneIds.some((id) => !lanes.some((lane) => lane.id === id)))
275
+ throw new TypeError('structure_edit_invalid');
276
+ edit.laneIds.forEach((id, index) => { requireLane(id).order = index; });
277
+ }
278
+ else if (edit.kind === 'merge') {
279
+ if (edit.fromLaneIds.length === 0 || new Set(edit.fromLaneIds).size !== edit.fromLaneIds.length
280
+ || edit.fromLaneIds.includes(edit.intoLaneId))
281
+ throw new TypeError('structure_edit_invalid');
282
+ const into = requireLane(edit.intoLaneId);
283
+ edit.fromLaneIds.forEach(requireLane);
284
+ for (const assignment of assignments) {
285
+ if (edit.fromLaneIds.includes(assignment.laneId))
286
+ assignment.laneId = into.id;
287
+ }
288
+ for (const laneId of edit.fromLaneIds)
289
+ lanes.splice(lanes.findIndex((lane) => lane.id === laneId), 1);
290
+ merges.push({ fromLaneIds: [...edit.fromLaneIds], intoLaneId: into.id });
291
+ }
292
+ else {
293
+ throw new TypeError('structure_edit_invalid');
294
+ }
295
+ }
296
+ lanes.sort((left, right) => left.order - right.order || left.id.localeCompare(right.id));
297
+ lanes.forEach((lane, index) => { lane.order = index; });
298
+ const result = canonicalRevision(current.inventoryRevision, lanes, assignments, merges);
299
+ return { ...result, buildId: current.buildId };
300
+ }
@@ -0,0 +1,2 @@
1
+ export const WHOLE_READ_PAGE_PROTOCOL = 'whole-read-pages@1';
2
+ export const INVENTORY_RULES_VERSION = 'inventory-rules@1';
@@ -0,0 +1,12 @@
1
+ /** Capabilities the companion advertises only when their whole-read runners exist. Kept apart from the
2
+ * runner so the HTTP client (also imported by server tests) does not pull in process-spawning code. */
3
+ export const WHOLE_READ_COMPANION_CAPABILITIES = [
4
+ 'whole-read-inventory@1',
5
+ 'whole-read-plan@1',
6
+ 'whole-read-units@1',
7
+ 'whole-read-grouping@1',
8
+ 'whole-read-drafting@1',
9
+ 'whole-read-checks@1',
10
+ 'whole-read-prd@1',
11
+ 'whole-read-relationships@1',
12
+ ];