@latysh-development/monolith 0.1.0-alpha.0 → 0.1.0-alpha.2

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 (114) hide show
  1. package/README.md +63 -16
  2. package/dist/brief/collect.d.ts.map +1 -1
  3. package/dist/brief/collect.js +41 -1
  4. package/dist/brief/constants.d.ts +2 -0
  5. package/dist/brief/constants.d.ts.map +1 -1
  6. package/dist/brief/constants.js +2 -0
  7. package/dist/brief/render.d.ts +7 -0
  8. package/dist/brief/render.d.ts.map +1 -1
  9. package/dist/brief/render.js +52 -1
  10. package/dist/brief/types.d.ts +17 -1
  11. package/dist/brief/types.d.ts.map +1 -1
  12. package/dist/cli/index.js +45 -0
  13. package/dist/discovery/compose.d.ts +28 -0
  14. package/dist/discovery/compose.d.ts.map +1 -0
  15. package/dist/discovery/compose.js +159 -0
  16. package/dist/discovery/contracts.d.ts +211 -0
  17. package/dist/discovery/contracts.d.ts.map +1 -0
  18. package/dist/discovery/contracts.js +555 -0
  19. package/dist/discovery/docs.d.ts +278 -0
  20. package/dist/discovery/docs.d.ts.map +1 -0
  21. package/dist/discovery/docs.js +1182 -0
  22. package/dist/discovery/facts.d.ts +49 -0
  23. package/dist/discovery/facts.d.ts.map +1 -0
  24. package/dist/discovery/facts.js +541 -0
  25. package/dist/discovery/index.d.ts +8 -0
  26. package/dist/discovery/index.d.ts.map +1 -0
  27. package/dist/discovery/index.js +9 -0
  28. package/dist/discovery/orchestrate.d.ts +35 -0
  29. package/dist/discovery/orchestrate.d.ts.map +1 -0
  30. package/dist/discovery/orchestrate.js +32 -0
  31. package/dist/discovery/render.d.ts +36 -0
  32. package/dist/discovery/render.d.ts.map +1 -0
  33. package/dist/discovery/render.js +278 -0
  34. package/dist/discovery/traverse.d.ts +197 -0
  35. package/dist/discovery/traverse.d.ts.map +1 -0
  36. package/dist/discovery/traverse.js +510 -0
  37. package/dist/durable/errors.d.ts +6 -2
  38. package/dist/durable/errors.d.ts.map +1 -1
  39. package/dist/durable/errors.js +9 -3
  40. package/dist/durable/fs-store.d.ts +21 -0
  41. package/dist/durable/fs-store.d.ts.map +1 -1
  42. package/dist/durable/fs-store.js +103 -3
  43. package/dist/durable/prune-check.d.ts +17 -0
  44. package/dist/durable/prune-check.d.ts.map +1 -1
  45. package/dist/durable/prune-check.js +27 -0
  46. package/dist/durable/runtime.d.ts +5 -3
  47. package/dist/durable/runtime.d.ts.map +1 -1
  48. package/dist/durable/runtime.js +23 -6
  49. package/dist/durable/seams.d.ts +4 -0
  50. package/dist/durable/seams.d.ts.map +1 -1
  51. package/dist/durable/testing.js +2 -2
  52. package/dist/finalize/cli.d.ts +10 -0
  53. package/dist/finalize/cli.d.ts.map +1 -0
  54. package/dist/finalize/cli.js +134 -0
  55. package/dist/finalize/core.d.ts +108 -0
  56. package/dist/finalize/core.d.ts.map +1 -0
  57. package/dist/finalize/core.js +767 -0
  58. package/dist/finalize/index.d.ts +5 -0
  59. package/dist/finalize/index.d.ts.map +1 -0
  60. package/dist/finalize/index.js +2 -0
  61. package/dist/finalize/proposal.d.ts +97 -0
  62. package/dist/finalize/proposal.d.ts.map +1 -0
  63. package/dist/finalize/proposal.js +213 -0
  64. package/dist/ideas/cli.d.ts +33 -0
  65. package/dist/ideas/cli.d.ts.map +1 -0
  66. package/dist/ideas/cli.js +158 -0
  67. package/dist/ideas/format.d.ts +38 -0
  68. package/dist/ideas/format.d.ts.map +1 -0
  69. package/dist/ideas/format.js +194 -0
  70. package/dist/ideas/index.d.ts +6 -0
  71. package/dist/ideas/index.d.ts.map +1 -0
  72. package/dist/ideas/index.js +3 -0
  73. package/dist/ideas/store.d.ts +55 -0
  74. package/dist/ideas/store.d.ts.map +1 -0
  75. package/dist/ideas/store.js +262 -0
  76. package/dist/ideas/types.d.ts +54 -0
  77. package/dist/ideas/types.d.ts.map +1 -0
  78. package/dist/ideas/types.js +26 -0
  79. package/dist/index.d.ts +1 -0
  80. package/dist/index.d.ts.map +1 -1
  81. package/dist/index.js +2 -0
  82. package/dist/init/scaffold.d.ts +4 -0
  83. package/dist/init/scaffold.d.ts.map +1 -1
  84. package/dist/init/scaffold.js +6 -2
  85. package/dist/managed/index.d.ts +73 -0
  86. package/dist/managed/index.d.ts.map +1 -0
  87. package/dist/managed/index.js +222 -0
  88. package/dist/root/index.d.ts +7 -0
  89. package/dist/root/index.d.ts.map +1 -1
  90. package/dist/root/index.js +13 -1
  91. package/dist/update/cli.d.ts +8 -0
  92. package/dist/update/cli.d.ts.map +1 -0
  93. package/dist/update/cli.js +115 -0
  94. package/dist/update/index.d.ts +78 -0
  95. package/dist/update/index.d.ts.map +1 -0
  96. package/dist/update/index.js +285 -0
  97. package/dist/version.d.ts +1 -1
  98. package/dist/version.js +1 -1
  99. package/package.json +1 -1
  100. package/templates/default-instance/instruction/.cursorrules +6 -0
  101. package/templates/default-instance/instruction/.windsurfrules +6 -0
  102. package/templates/default-instance/instruction/CLAUDE.md +4 -0
  103. package/templates/default-instance/managed/agent.md +20 -0
  104. package/templates/default-instance/managed/bootstrap.md +7 -0
  105. package/templates/default-instance/managed/index-idea.md +5 -0
  106. package/templates/default-instance/managed/routing-idea.md +14 -0
  107. package/templates/default-instance/monolith/AGENT.md +1 -0
  108. package/templates/default-instance/monolith/context/maps/routing-map.md +4 -0
  109. package/templates/default-instance/monolith/context/maps/semantic-map.md +2 -0
  110. package/templates/default-instance/monolith/memory/working/active.md +11 -0
  111. package/templates/default-instance/monolith/skills/INDEX.md +4 -0
  112. package/templates/default-instance/monolith/skills/finalize-story/SKILL.md +51 -1
  113. package/templates/default-instance/monolith/skills/idea-capture/SKILL.md +157 -0
  114. package/templates/default-instance/projectroot/AGENTS.md +4 -0
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Deterministic structural fact extraction (DISCOVERY-001B-2, ADR-021).
3
+ *
4
+ * Input: a `TraversalResult` (what exists). Output: a `DiscoveryResult` (facts with sources).
5
+ * It states what the files show and nothing about what the project is: no component roles,
6
+ * no verified entrypoints, no "main language", no installed or resolved dependencies.
7
+ *
8
+ * Reading: the only content read is `package.json` (at most `maxManifestReads` of them, at most
9
+ * `maxManifestBytes` each), through `readProjectFile` (ADR-011/014: every component below the project
10
+ * root is `lstat`-checked and none may be a symlink, `O_NOFOLLOW|O_NONBLOCK`, regular single-link
11
+ * file, bounded read). The traversal having listed a path proves nothing about the next read, so
12
+ * every read is made on its own terms. It only extracts JSON via `JSON.parse`; it never evaluates,
13
+ * runs or imports project code, never uses a subprocess or the network.
14
+ * `scripts` values and dependency version specifiers are never read into a fact.
15
+ * TOML/YAML manifests are recognised by name and not parsed (no home-made parsers).
16
+ *
17
+ * NOT defended (ADR-013, SEC-001B.3 open; TM-C unsupported): a process that swaps a directory or
18
+ * file between the check and the open. Do not run it on a tree an untrusted process is modifying.
19
+ *
20
+ * Nested MONOLITH: a sub-project's source is part of this project. A directory named `.monolith`
21
+ * (other than the real root, which the traversal already excluded) is treated as that sub-project's
22
+ * internal state only when its content proves it (`state.json` plus `memory/` or `context/` in the
23
+ * inventory); then its entries are left out of the facts. A bare name proves nothing: the directory
24
+ * stays in and a diagnostic says it is unconfirmed. A nested instance whose root is renamed through
25
+ * its own config is not recognised (that needs reading foreign config: a separate decision).
26
+ *
27
+ * Determinism: every input is sorted by UTF-16 code units before use, so the order of
28
+ * `traversal.entries` cannot change the result; no clock, absolute path or host value is read.
29
+ */
30
+ import { type ProjectFileRead } from '../root/index.js';
31
+ import type { TraversalResult } from './traverse.js';
32
+ import { type DiscoveryLimits, type DiscoveryResult } from './contracts.js';
33
+ export declare const DEFAULT_DISCOVERY_LIMITS: Readonly<DiscoveryLimits>;
34
+ /** Extension (lower case, with the dot) to language. Evidence is the extension and nothing else. */
35
+ export declare const LANGUAGE_BY_EXTENSION: Readonly<Record<string, string>>;
36
+ /** Known manifest file name to the ecosystem its presence points at (a presence, not a runtime use). */
37
+ export declare const KNOWN_MANIFESTS: Readonly<Record<string, string>>;
38
+ /** Project-relative file names that may start a program. Only candidates, ever. */
39
+ export declare const ENTRYPOINT_CANDIDATE_PATHS: readonly string[];
40
+ export type ManifestReader = (projectRoot: string, relPath: string, opts: {
41
+ maxBytes: number;
42
+ }) => ProjectFileRead;
43
+ export interface ExtractOptions {
44
+ limits?: Partial<DiscoveryLimits>;
45
+ /** @internal Test seam; production uses `readProjectFile`. */
46
+ readManifest?: ManifestReader;
47
+ }
48
+ export declare function extractProjectFacts(projectRoot: string, traversal: TraversalResult, options?: ExtractOptions): DiscoveryResult;
49
+ //# sourceMappingURL=facts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"facts.d.ts","sourceRoot":"","sources":["../../src/discovery/facts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAGH,OAAO,EAA8C,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACpG,OAAO,KAAK,EAAkB,eAAe,EAAE,MAAM,eAAe,CAAC;AACrE,OAAO,EAmBL,KAAK,eAAe,EACpB,KAAK,eAAe,EAGrB,MAAM,gBAAgB,CAAC;AAExB,eAAO,MAAM,wBAAwB,EAAE,QAAQ,CAAC,eAAe,CAc7D,CAAC;AAEH,oGAAoG;AACpG,eAAO,MAAM,qBAAqB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAwBjE,CAAC;AAEH,wGAAwG;AACxG,eAAO,MAAM,eAAe,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAO3D,CAAC;AAEH,mFAAmF;AACnF,eAAO,MAAM,0BAA0B,EAAE,SAAS,MAAM,EAwBvD,CAAC;AAIF,MAAM,MAAM,cAAc,GAAG,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,KAAK,eAAe,CAAC;AAEnH,MAAM,WAAW,cAAc;IAC7B,MAAM,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC;IAClC,8DAA8D;IAC9D,YAAY,CAAC,EAAE,cAAc,CAAC;CAC/B;AAyOD,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,eAAe,EAAE,OAAO,GAAE,cAAmB,GAAG,eAAe,CAwNlI"}
@@ -0,0 +1,541 @@
1
+ /**
2
+ * Deterministic structural fact extraction (DISCOVERY-001B-2, ADR-021).
3
+ *
4
+ * Input: a `TraversalResult` (what exists). Output: a `DiscoveryResult` (facts with sources).
5
+ * It states what the files show and nothing about what the project is: no component roles,
6
+ * no verified entrypoints, no "main language", no installed or resolved dependencies.
7
+ *
8
+ * Reading: the only content read is `package.json` (at most `maxManifestReads` of them, at most
9
+ * `maxManifestBytes` each), through `readProjectFile` (ADR-011/014: every component below the project
10
+ * root is `lstat`-checked and none may be a symlink, `O_NOFOLLOW|O_NONBLOCK`, regular single-link
11
+ * file, bounded read). The traversal having listed a path proves nothing about the next read, so
12
+ * every read is made on its own terms. It only extracts JSON via `JSON.parse`; it never evaluates,
13
+ * runs or imports project code, never uses a subprocess or the network.
14
+ * `scripts` values and dependency version specifiers are never read into a fact.
15
+ * TOML/YAML manifests are recognised by name and not parsed (no home-made parsers).
16
+ *
17
+ * NOT defended (ADR-013, SEC-001B.3 open; TM-C unsupported): a process that swaps a directory or
18
+ * file between the check and the open. Do not run it on a tree an untrusted process is modifying.
19
+ *
20
+ * Nested MONOLITH: a sub-project's source is part of this project. A directory named `.monolith`
21
+ * (other than the real root, which the traversal already excluded) is treated as that sub-project's
22
+ * internal state only when its content proves it (`state.json` plus `memory/` or `context/` in the
23
+ * inventory); then its entries are left out of the facts. A bare name proves nothing: the directory
24
+ * stays in and a diagnostic says it is unconfirmed. A nested instance whose root is renamed through
25
+ * its own config is not recognised (that needs reading foreign config: a separate decision).
26
+ *
27
+ * Determinism: every input is sorted by UTF-16 code units before use, so the order of
28
+ * `traversal.entries` cannot change the result; no clock, absolute path or host value is read.
29
+ */
30
+ import { isAbsolute } from 'path';
31
+ import { readProjectFile } from '../root/index.js';
32
+ import { DEPENDENCY_SECTIONS, DISCOVERY_SCHEMA_VERSION, DiscoveryError, EXTRACTION_DIAGNOSTIC_AFFECTS_COMPLETENESS, EXTRACTION_DIAGNOSTIC_IS_STABLE, FACT_LEVEL_BY_KIND, compareDiagnostics, compareFacts, compareSources, compareUtf16, computeFactId, isKeepableName, isSafeRelativePath, pointerToken, validateDiscoveryResult, } from './contracts.js';
33
+ export const DEFAULT_DISCOVERY_LIMITS = Object.freeze({
34
+ maxDirectoryFacts: 200,
35
+ maxDirectoryDepth: 2,
36
+ maxLanguageFacts: 64,
37
+ maxManifestFacts: 50,
38
+ maxManifestReads: 20,
39
+ maxManifestBytes: 256 * 1024,
40
+ maxDependenciesPerManifest: 500,
41
+ maxScriptsPerManifest: 100,
42
+ maxEntrypointFacts: 20,
43
+ maxFacts: 5000,
44
+ maxDiagnostics: 200,
45
+ maxNameLength: 214,
46
+ maxSourcesPerFact: 3,
47
+ });
48
+ /** Extension (lower case, with the dot) to language. Evidence is the extension and nothing else. */
49
+ export const LANGUAGE_BY_EXTENSION = Object.freeze({
50
+ '.ts': 'TypeScript',
51
+ '.tsx': 'TypeScript',
52
+ '.mts': 'TypeScript',
53
+ '.cts': 'TypeScript',
54
+ '.js': 'JavaScript',
55
+ '.jsx': 'JavaScript',
56
+ '.mjs': 'JavaScript',
57
+ '.cjs': 'JavaScript',
58
+ '.py': 'Python',
59
+ '.rs': 'Rust',
60
+ '.go': 'Go',
61
+ '.java': 'Java',
62
+ '.kt': 'Kotlin',
63
+ '.rb': 'Ruby',
64
+ '.php': 'PHP',
65
+ '.cs': 'C#',
66
+ '.c': 'C',
67
+ '.h': 'C',
68
+ '.cc': 'C++',
69
+ '.cpp': 'C++',
70
+ '.hpp': 'C++',
71
+ '.swift': 'Swift',
72
+ '.vue': 'Vue',
73
+ });
74
+ /** Known manifest file name to the ecosystem its presence points at (a presence, not a runtime use). */
75
+ export const KNOWN_MANIFESTS = Object.freeze({
76
+ 'package.json': 'npm',
77
+ 'pnpm-workspace.yaml': 'pnpm',
78
+ 'pyproject.toml': 'python',
79
+ 'requirements.txt': 'pip',
80
+ 'Cargo.toml': 'cargo',
81
+ 'go.mod': 'go',
82
+ });
83
+ /** Project-relative file names that may start a program. Only candidates, ever. */
84
+ export const ENTRYPOINT_CANDIDATE_PATHS = [
85
+ 'index.js',
86
+ 'index.ts',
87
+ 'main.js',
88
+ 'main.ts',
89
+ 'server.js',
90
+ 'server.ts',
91
+ 'app.js',
92
+ 'app.ts',
93
+ 'src/index.js',
94
+ 'src/index.ts',
95
+ 'src/main.js',
96
+ 'src/main.ts',
97
+ 'src/server.js',
98
+ 'src/server.ts',
99
+ 'src/app.js',
100
+ 'src/app.ts',
101
+ 'app.py',
102
+ 'main.py',
103
+ 'manage.py',
104
+ 'src/app.py',
105
+ 'src/main.py',
106
+ 'main.go',
107
+ 'src/main.rs',
108
+ ];
109
+ const LIMIT_KEYS = Object.keys(DEFAULT_DISCOVERY_LIMITS);
110
+ function resolveLimits(partial) {
111
+ const limits = { ...DEFAULT_DISCOVERY_LIMITS };
112
+ for (const [key, value] of Object.entries(partial ?? {})) {
113
+ if (!LIMIT_KEYS.includes(key))
114
+ throw new DiscoveryError('INVALID_LIMITS', `unknown limit "${key}".`);
115
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 1) {
116
+ throw new DiscoveryError('INVALID_LIMITS', `limit "${key}" must be a positive integer.`);
117
+ }
118
+ limits[key] = value;
119
+ }
120
+ return limits;
121
+ }
122
+ // ---------------------------------------------------------------------------
123
+ // Diagnostics
124
+ // ---------------------------------------------------------------------------
125
+ class Diagnostics {
126
+ list = [];
127
+ add(code, message, path, count) {
128
+ const d = { code, message };
129
+ if (path !== undefined)
130
+ d.path = path;
131
+ if (count !== undefined)
132
+ d.count = count;
133
+ this.list.push(d);
134
+ }
135
+ all() {
136
+ return this.list;
137
+ }
138
+ }
139
+ // ---------------------------------------------------------------------------
140
+ // Fact sink: one place that builds ids, merges equal facts, enforces the global budget
141
+ // ---------------------------------------------------------------------------
142
+ class FactSink {
143
+ max;
144
+ byId = new Map();
145
+ dropped = 0;
146
+ constructor(max) {
147
+ this.max = max;
148
+ }
149
+ get size() {
150
+ return this.byId.size;
151
+ }
152
+ /** Adds facts as one unit (an interpretation and its basis stay together); false when the budget refuses. */
153
+ addAll(specs) {
154
+ const ids = specs.map((s) => computeFactId(s.kind, s.subject, s.object, s.value));
155
+ const added = new Set(ids.filter((id) => !this.byId.has(id)));
156
+ if (this.byId.size + added.size > this.max) {
157
+ this.dropped += specs.length;
158
+ return false;
159
+ }
160
+ for (const s of specs)
161
+ this.put(s);
162
+ return true;
163
+ }
164
+ put(s) {
165
+ const id = computeFactId(s.kind, s.subject, s.object, s.value);
166
+ const existing = this.byId.get(id);
167
+ if (existing) {
168
+ if (existing.kind !== s.kind || existing.subject !== s.subject || existing.object !== s.object || existing.value !== s.value) {
169
+ throw new DiscoveryError('ID_COLLISION', `fact id ${id} is shared by two different facts.`);
170
+ }
171
+ const merged = new Map(existing.sources.map((x) => [JSON.stringify(x), x]));
172
+ for (const x of s.sources)
173
+ merged.set(JSON.stringify(x), x);
174
+ existing.sources = [...merged.values()].sort(compareSources);
175
+ return;
176
+ }
177
+ const fact = { id, kind: s.kind, level: FACT_LEVEL_BY_KIND[s.kind], subject: s.subject, rule: s.rule, sources: [...s.sources].sort(compareSources) };
178
+ if (s.object !== undefined)
179
+ fact.object = s.object;
180
+ if (s.value !== undefined)
181
+ fact.value = s.value;
182
+ if (s.basis)
183
+ fact.basis = [...s.basis].sort(compareUtf16);
184
+ this.byId.set(id, fact);
185
+ }
186
+ facts() {
187
+ return [...this.byId.values()].sort(compareFacts);
188
+ }
189
+ }
190
+ // ---------------------------------------------------------------------------
191
+ // Helpers
192
+ // ---------------------------------------------------------------------------
193
+ const depthOf = (p) => p.split('/').length;
194
+ const baseName = (p) => p.slice(p.lastIndexOf('/') + 1);
195
+ function extensionOf(path) {
196
+ const name = baseName(path);
197
+ const dot = name.lastIndexOf('.');
198
+ // `.gitignore`-style names and names ending in a dot have no extension.
199
+ if (dot <= 0 || dot === name.length - 1)
200
+ return undefined;
201
+ return name.slice(dot).toLowerCase();
202
+ }
203
+ function compareDepthThenPath(a, b) {
204
+ return depthOf(a) - depthOf(b) || compareUtf16(a, b);
205
+ }
206
+ function isPlainObject(v) {
207
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
208
+ }
209
+ /** Every code `readProjectFile` can return; anything else is reported as `UNKNOWN`. Exhaustive by type. */
210
+ const READ_ERROR_CODES = new Set(Object.keys({
211
+ FILE_MISSING: 0,
212
+ FILE_OUTSIDE_PROJECT: 0,
213
+ FILE_UNRESOLVABLE: 0,
214
+ FILE_NOT_REGULAR: 0,
215
+ FILE_EMPTY: 0,
216
+ FILE_TOO_LARGE: 0,
217
+ FILE_CHANGED: 0,
218
+ FILE_UNREADABLE: 0,
219
+ FILE_SYMLINK: 0,
220
+ FILE_HARDLINKED: 0,
221
+ READ_BOUNDARY_INVALID: 0,
222
+ READ_UNSUPPORTED_PLATFORM: 0,
223
+ }));
224
+ function parsePackageJson(projectRoot, path, limits, read, diag) {
225
+ // The reader is a seam: whatever it returns or throws is untrusted. Only a code from the closed
226
+ // list below, a size-checked string and a fixed message can come out of this block.
227
+ let content;
228
+ try {
229
+ const r = read(projectRoot, path, { maxBytes: limits.maxManifestBytes });
230
+ if (r.ok) {
231
+ if (typeof r.content !== 'string')
232
+ throw new TypeError('content');
233
+ content = r.content;
234
+ }
235
+ else {
236
+ // `detail` may carry an absolute path or a system message: never read.
237
+ const code = READ_ERROR_CODES.has(r.code) ? r.code : 'UNKNOWN';
238
+ if (code === 'FILE_EMPTY')
239
+ diag.add('MANIFEST_EMPTY', 'the manifest is empty; nothing was extracted.', path);
240
+ else if (code === 'FILE_TOO_LARGE')
241
+ diag.add('MANIFEST_TOO_LARGE', `the manifest exceeds ${limits.maxManifestBytes} bytes and was not read.`, path);
242
+ else
243
+ diag.add('MANIFEST_READ_FAILED', `the manifest was refused or could not be read (${code}).`, path);
244
+ return undefined;
245
+ }
246
+ }
247
+ catch {
248
+ diag.add('MANIFEST_READ_FAILED', 'the manifest could not be read (unexpected failure).', path);
249
+ return undefined;
250
+ }
251
+ let doc;
252
+ try {
253
+ // JSON.parse only: no reviver, no evaluation. Its error text can quote the input, so it is dropped.
254
+ doc = JSON.parse(content.charCodeAt(0) === 0xfeff ? content.slice(1) : content);
255
+ }
256
+ catch {
257
+ diag.add('MANIFEST_PARSE_ERROR', 'the manifest is not valid JSON; nothing was extracted.', path);
258
+ return undefined;
259
+ }
260
+ if (!isPlainObject(doc)) {
261
+ diag.add('MANIFEST_FIELD_INVALID', 'the top-level JSON value is not an object; nothing was extracted.', path);
262
+ return undefined;
263
+ }
264
+ const section = (name) => {
265
+ if (!Object.prototype.hasOwnProperty.call(doc, name))
266
+ return undefined;
267
+ const v = doc[name];
268
+ if (!isPlainObject(v)) {
269
+ diag.add('MANIFEST_FIELD_INVALID', `field "${name}" is not an object and was ignored.`, path);
270
+ return undefined;
271
+ }
272
+ // Keys only. The values (version specifiers, commands) are never touched.
273
+ return Object.keys(v).sort(compareUtf16);
274
+ };
275
+ let rejected = 0;
276
+ const dependencies = [];
277
+ let droppedDeps = 0;
278
+ const seen = new Set();
279
+ for (const sec of DEPENDENCY_SECTIONS) {
280
+ for (const name of section(sec) ?? []) {
281
+ if (!isKeepableName(name, limits.maxNameLength)) {
282
+ rejected++;
283
+ continue;
284
+ }
285
+ const key = `${sec}\u0000${name}`;
286
+ if (seen.has(key))
287
+ continue;
288
+ seen.add(key);
289
+ if (dependencies.length >= limits.maxDependenciesPerManifest)
290
+ droppedDeps++;
291
+ else
292
+ dependencies.push({ name, section: sec });
293
+ }
294
+ }
295
+ const scripts = [];
296
+ let droppedScripts = 0;
297
+ for (const name of section('scripts') ?? []) {
298
+ if (!isKeepableName(name, limits.maxNameLength)) {
299
+ rejected++;
300
+ continue;
301
+ }
302
+ if (scripts.length >= limits.maxScriptsPerManifest)
303
+ droppedScripts++;
304
+ else
305
+ scripts.push(name);
306
+ }
307
+ if (rejected > 0)
308
+ diag.add('MANIFEST_VALUE_REJECTED', 'names that are empty, too long or contain control, bidirectional or invalid characters were not kept.', path, rejected);
309
+ if (droppedDeps > 0)
310
+ diag.add('LIMIT_DEPENDENCIES', `dependency names beyond ${limits.maxDependenciesPerManifest} were not kept.`, path, droppedDeps);
311
+ if (droppedScripts > 0)
312
+ diag.add('LIMIT_SCRIPTS', `script names beyond ${limits.maxScriptsPerManifest} were not kept.`, path, droppedScripts);
313
+ return { path, dependencies, scripts };
314
+ }
315
+ // ---------------------------------------------------------------------------
316
+ // Extraction
317
+ // ---------------------------------------------------------------------------
318
+ const ENTRY_KINDS = new Set(['file', 'directory', 'symlink', 'other']);
319
+ export function extractProjectFacts(projectRoot, traversal, options = {}) {
320
+ if (typeof projectRoot !== 'string' || !isAbsolute(projectRoot)) {
321
+ throw new DiscoveryError('INVALID_INPUT', 'the project root must be an explicit absolute path.');
322
+ }
323
+ if (!traversal ||
324
+ !Array.isArray(traversal.entries) ||
325
+ !Array.isArray(traversal.diagnostics) ||
326
+ (traversal.completeness !== 'COMPLETE' && traversal.completeness !== 'PARTIAL') ||
327
+ typeof traversal.reproducible !== 'boolean') {
328
+ throw new DiscoveryError('INVALID_INPUT', 'the traversal result is malformed.');
329
+ }
330
+ const limits = resolveLimits(options.limits);
331
+ const read = options.readManifest ?? ((root, rel, o) => readProjectFile(root, rel, { maxBytes: o.maxBytes }));
332
+ const diag = new Diagnostics();
333
+ // 1. Input hygiene: a handcrafted or corrupted entry must not reach a path join or a fact.
334
+ let rejectedPaths = 0;
335
+ const kindByPath = new Map();
336
+ for (const e of traversal.entries) {
337
+ if (!e || !ENTRY_KINDS.has(e.kind) || !isSafeRelativePath(e.path)) {
338
+ rejectedPaths++;
339
+ continue;
340
+ }
341
+ // Duplicate path with another kind: the lexically smaller kind wins, independent of input order.
342
+ const prior = kindByPath.get(e.path);
343
+ if (prior === undefined || compareUtf16(e.kind, prior) < 0)
344
+ kindByPath.set(e.path, e.kind);
345
+ }
346
+ if (rejectedPaths > 0)
347
+ diag.add('INPUT_PATH_REJECTED', 'traversal entries with an unsafe path or an unknown kind were ignored.', undefined, rejectedPaths);
348
+ // 2. Nested MONOLITH state (confirmed by content only; a name alone proves nothing).
349
+ const nestedState = [];
350
+ for (const [path, kind] of [...kindByPath].sort((a, b) => compareUtf16(a[0], b[0]))) {
351
+ if (kind !== 'directory' || baseName(path) !== '.monolith')
352
+ continue;
353
+ const has = (rel, k) => kindByPath.get(`${path}/${rel}`) === k;
354
+ if (has('state.json', 'file') && (has('memory', 'directory') || has('context', 'directory'))) {
355
+ nestedState.push(path);
356
+ diag.add('NESTED_MONOLITH_STATE_EXCLUDED', 'a nested MONOLITH instance state directory (confirmed by its content) is not part of the facts.', path);
357
+ }
358
+ else {
359
+ diag.add('NESTED_MONOLITH_UNCONFIRMED', 'a directory named .monolith was found but its content does not confirm a MONOLITH instance; it is kept as ordinary content.', path);
360
+ }
361
+ }
362
+ const nestedSet = new Set(nestedState);
363
+ const insideNested = (path) => {
364
+ for (let i = path.indexOf('/');; i = path.indexOf('/', i + 1)) {
365
+ if (nestedSet.has(i === -1 ? path : path.slice(0, i)))
366
+ return true;
367
+ if (i === -1)
368
+ return false;
369
+ }
370
+ };
371
+ const kept = [...kindByPath].filter(([p]) => !insideNested(p)).sort((a, b) => compareUtf16(a[0], b[0]));
372
+ const excludedEntries = traversal.entries.length - kept.length;
373
+ const files = kept.filter(([, k]) => k === 'file').map(([p]) => p);
374
+ const sink = new FactSink(limits.maxFacts);
375
+ // 3. Manifests: presence (by name) is separated from content (read below).
376
+ const manifestCandidates = [];
377
+ for (const [path, kind] of kept) {
378
+ if (!Object.prototype.hasOwnProperty.call(KNOWN_MANIFESTS, baseName(path)))
379
+ continue;
380
+ if (kind === 'file')
381
+ manifestCandidates.push(path);
382
+ else if (kind === 'symlink' || kind === 'other')
383
+ diag.add('MANIFEST_NOT_REGULAR_FILE', `an entry with the name of a known manifest is not a regular file (${kind}); it was not treated as a manifest.`, path);
384
+ }
385
+ manifestCandidates.sort(compareDepthThenPath);
386
+ const manifests = manifestCandidates.slice(0, limits.maxManifestFacts);
387
+ if (manifestCandidates.length > manifests.length) {
388
+ diag.add('LIMIT_MANIFEST_FACTS', `manifests beyond ${limits.maxManifestFacts} (deepest last) were not recorded.`, undefined, manifestCandidates.length - manifests.length);
389
+ }
390
+ // 4. Entrypoint candidates (INTERPRETED) rest on a DETECTED file fact.
391
+ const present = new Set(files);
392
+ const candidates = ENTRYPOINT_CANDIDATE_PATHS.filter((p) => present.has(p)).sort(compareUtf16);
393
+ const candidateSpecs = candidates.slice(0, limits.maxEntrypointFacts);
394
+ if (candidates.length > candidateSpecs.length) {
395
+ diag.add('LIMIT_ENTRYPOINT_FACTS', `entrypoint candidates beyond ${limits.maxEntrypointFacts} were not recorded.`, undefined, candidates.length - candidateSpecs.length);
396
+ }
397
+ // 5. Languages by extension.
398
+ const groups = new Map();
399
+ let withoutLanguage = 0;
400
+ for (const path of files) {
401
+ const ext = extensionOf(path);
402
+ const language = ext !== undefined && Object.prototype.hasOwnProperty.call(LANGUAGE_BY_EXTENSION, ext) ? LANGUAGE_BY_EXTENSION[ext] : undefined;
403
+ if (ext === undefined || language === undefined) {
404
+ withoutLanguage++;
405
+ continue;
406
+ }
407
+ const g = groups.get(ext) ?? { language, ext, paths: [] };
408
+ g.paths.push(path);
409
+ groups.set(ext, g);
410
+ }
411
+ const languageGroups = [...groups.values()].sort((a, b) => compareUtf16(a.language, b.language) || compareUtf16(a.ext, b.ext));
412
+ // 6. Directories (shallow, bounded).
413
+ const dirs = kept
414
+ .filter(([p, k]) => k === 'directory' && depthOf(p) <= limits.maxDirectoryDepth)
415
+ .map(([p]) => p)
416
+ .sort(compareDepthThenPath);
417
+ // 7. Manifest content: package.json only, bounded in count and size.
418
+ const packageJsons = manifests.filter((p) => baseName(p) === 'package.json');
419
+ const toRead = packageJsons.slice(0, limits.maxManifestReads);
420
+ if (packageJsons.length > toRead.length) {
421
+ diag.add('LIMIT_MANIFEST_READS', `package.json files beyond ${limits.maxManifestReads} were recorded as present but not read.`, undefined, packageJsons.length - toRead.length);
422
+ }
423
+ const parsed = [];
424
+ for (const p of toRead) {
425
+ const m = parsePackageJson(projectRoot, p, limits, read, diag);
426
+ if (m)
427
+ parsed.push(m);
428
+ }
429
+ const unparsedNames = new Map();
430
+ for (const p of manifests) {
431
+ const name = baseName(p);
432
+ if (name !== 'package.json')
433
+ unparsedNames.set(name, (unparsedNames.get(name) ?? 0) + 1);
434
+ }
435
+ for (const [name, count] of [...unparsedNames].sort((a, b) => compareUtf16(a[0], b[0]))) {
436
+ diag.add('MANIFEST_CONTENT_NOT_ANALYZED', `${name}: recorded as present; its content is not analyzed in this version.`, undefined, count);
437
+ }
438
+ // 8. Emit in priority order, so the global budget (`maxFacts`) cuts the bulkiest, least structural
439
+ // facts (dependency names) first and deterministically.
440
+ const emit = (kind, subject, rule, source, extra = {}) => ({
441
+ kind,
442
+ subject,
443
+ rule,
444
+ sources: [source],
445
+ ...extra,
446
+ });
447
+ for (const p of manifests) {
448
+ sink.addAll([emit('MANIFEST_PRESENT', p, 'known-manifest-file-name', { path: p, observation: 'file-name' }, { object: KNOWN_MANIFESTS[baseName(p)] })]);
449
+ }
450
+ for (const p of candidateSpecs) {
451
+ const fileId = computeFactId('FILE_PRESENT', p);
452
+ sink.addAll([
453
+ emit('FILE_PRESENT', p, 'conventional-file-name', { path: p, observation: 'file-entry' }),
454
+ emit('ENTRYPOINT_CANDIDATE', p, 'entrypoint-convention', { path: p, observation: 'file-name' }, { value: 'candidate', basis: [fileId] }),
455
+ ]);
456
+ }
457
+ let languageDropped = 0;
458
+ languageGroups.forEach((g, i) => {
459
+ if (i >= limits.maxLanguageFacts) {
460
+ languageDropped++;
461
+ return;
462
+ }
463
+ // `value` counts every file of the group; the sources are the first few in UTF-16 order (a sample).
464
+ const example = g.paths.slice(0, limits.maxSourcesPerFact);
465
+ sink.addAll([
466
+ {
467
+ kind: 'LANGUAGE_BY_EXTENSION',
468
+ subject: g.language,
469
+ object: g.ext,
470
+ value: String(g.paths.length),
471
+ rule: 'extension-map',
472
+ sources: example.map((path) => ({ path, observation: 'extension' })),
473
+ },
474
+ ]);
475
+ });
476
+ if (languageDropped > 0)
477
+ diag.add('LIMIT_LANGUAGE_FACTS', `language facts beyond ${limits.maxLanguageFacts} were not recorded.`, undefined, languageDropped);
478
+ const dirsKept = dirs.slice(0, limits.maxDirectoryFacts);
479
+ for (const p of dirsKept)
480
+ sink.addAll([emit('DIRECTORY', p, 'walk-directory', { path: p, observation: 'directory-entry' })]);
481
+ if (dirs.length > dirsKept.length)
482
+ diag.add('LIMIT_DIRECTORY_FACTS', `directories beyond ${limits.maxDirectoryFacts} (deepest last) were not recorded.`, undefined, dirs.length - dirsKept.length);
483
+ for (const m of parsed) {
484
+ for (const name of m.scripts) {
485
+ sink.addAll([emit('SCRIPT_DECLARED', m.path, 'package-json-scripts', { path: m.path, observation: 'manifest-field', field: `/scripts/${pointerToken(name)}` }, { object: name })]);
486
+ }
487
+ }
488
+ for (const m of parsed) {
489
+ for (const d of m.dependencies) {
490
+ sink.addAll([
491
+ emit('DEPENDENCY_DECLARED', m.path, `package-json-${d.section}`, { path: m.path, observation: 'manifest-field', field: `/${d.section}/${pointerToken(d.name)}` }, { object: d.name, value: d.section }),
492
+ ]);
493
+ }
494
+ }
495
+ if (sink.dropped > 0)
496
+ diag.add('LIMIT_FACTS', `facts beyond ${limits.maxFacts} were not recorded (dependency and script names are cut first).`, undefined, sink.dropped);
497
+ // 9. Diagnostics: extraction first (completeness-affecting before the rest), then the traversal's.
498
+ const fromTraversal = traversal.diagnostics.map((d) => {
499
+ const out = { code: `TRAVERSAL_${String(d.code)}`, message: String(d.message) };
500
+ if (d.path !== undefined && isSafeRelativePath(d.path))
501
+ out.path = d.path;
502
+ if (d.count !== undefined)
503
+ out.count = d.count;
504
+ return out;
505
+ });
506
+ let diagnostics = [...diag.all(), ...fromTraversal].sort(compareDiagnostics);
507
+ let diagnosticsDropped = 0;
508
+ if (diagnostics.length > limits.maxDiagnostics) {
509
+ diagnosticsDropped = diagnostics.length - (limits.maxDiagnostics - 1);
510
+ diagnostics = diagnostics.slice(0, limits.maxDiagnostics - 1);
511
+ diagnostics.unshift({ code: 'LIMIT_DIAGNOSTICS', count: diagnosticsDropped, message: `${diagnosticsDropped} diagnostics beyond the limit of ${limits.maxDiagnostics} were not kept.` });
512
+ }
513
+ // 10. Completeness, reproducibility: separate properties.
514
+ const extractionCodes = [...diag.all()].map((d) => d.code);
515
+ if (diagnosticsDropped > 0)
516
+ extractionCodes.push('LIMIT_DIAGNOSTICS');
517
+ const partial = traversal.completeness === 'PARTIAL' || extractionCodes.some((c) => EXTRACTION_DIAGNOSTIC_AFFECTS_COMPLETENESS[c]);
518
+ const reproducible = traversal.reproducible && extractionCodes.every((c) => EXTRACTION_DIAGNOSTIC_IS_STABLE[c]);
519
+ const facts = sink.facts();
520
+ const result = {
521
+ schemaVersion: DISCOVERY_SCHEMA_VERSION,
522
+ limits,
523
+ completeness: partial ? 'PARTIAL' : 'COMPLETE',
524
+ reproducible,
525
+ traversal: { completeness: traversal.completeness, reproducible: traversal.reproducible },
526
+ stats: {
527
+ inputEntries: traversal.entries.length,
528
+ excludedEntries,
529
+ filesWithoutKnownLanguage: withoutLanguage,
530
+ manifestsFound: manifestCandidates.length,
531
+ manifestsRead: toRead.length,
532
+ facts: facts.length,
533
+ },
534
+ facts,
535
+ diagnostics,
536
+ };
537
+ const violations = validateDiscoveryResult(result);
538
+ if (violations.length > 0)
539
+ throw new DiscoveryError('RESULT_INVALID', `the result failed validation: ${violations.slice(0, 3).join('; ')}`);
540
+ return result;
541
+ }
@@ -0,0 +1,8 @@
1
+ export * from './traverse.js';
2
+ export * from './contracts.js';
3
+ export * from './facts.js';
4
+ export * from './render.js';
5
+ export * from './docs.js';
6
+ export * from './compose.js';
7
+ export * from './orchestrate.js';
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/discovery/index.ts"],"names":[],"mappings":"AAEA,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,cAAc,CAAC;AAC7B,cAAc,kBAAkB,CAAC"}
@@ -0,0 +1,9 @@
1
+ // Discovery (ADR-021). Internal module: deliberately NOT exported from `src/index.ts`
2
+ // until the owner decides (ADR-021 Q-7). DISCOVERY-001B-1: safe traversal; 001B-2: fact extraction; 001B-3: Markdown renderer; 001B-2D: documentation analyzer; 001C: unified project map composer; 001C-G: orchestration.
3
+ export * from './traverse.js';
4
+ export * from './contracts.js';
5
+ export * from './facts.js';
6
+ export * from './render.js';
7
+ export * from './docs.js';
8
+ export * from './compose.js';
9
+ export * from './orchestrate.js';
@@ -0,0 +1,35 @@
1
+ /**
2
+ * DISCOVERY-001C-G — unified discovery orchestration (ADR-021). Internal.
3
+ *
4
+ * One entry point that runs the whole Project Discovery chain: ONE safe traversal, then fact
5
+ * extraction and documentation analysis over that same `TraversalResult` object and the same
6
+ * `projectRoot` string, then the composer.
7
+ *
8
+ * Guarantees, by construction of this function:
9
+ * - same invocation: both results come from one call;
10
+ * - same traversal: exactly one `traverseProject` call; both analyzers receive that object;
11
+ * - same project root: the one string given here is passed unchanged to all three steps.
12
+ *
13
+ * NOT guaranteed: same file system snapshot. The traversal records a listing; the analyzers read
14
+ * file contents (package.json, Markdown documents) later and independently, so a file may change
15
+ * between the listing and either read, or between the two reads. `reproducible`/`completeness`
16
+ * remain the only signals about that.
17
+ *
18
+ * Errors from any step propagate unchanged; nothing is retried, caught or replaced by a fallback.
19
+ * No other data source, no network, no model, no write. The existing path-safety and bounded-read
20
+ * rules live inside the steps and are not bypassed here.
21
+ */
22
+ import { type AnalyzeDocumentationOptions } from './docs.js';
23
+ import { type ExtractOptions } from './facts.js';
24
+ import { type TraverseOptions } from './traverse.js';
25
+ export interface DiscoverProjectMapOptions {
26
+ /** Passed to the single traversal. */
27
+ traversal?: TraverseOptions;
28
+ /** Passed to fact extraction. */
29
+ facts?: ExtractOptions;
30
+ /** Passed to the documentation analyzer. */
31
+ documentation?: AnalyzeDocumentationOptions;
32
+ }
33
+ /** Runs traversal → facts + documentation (same traversal object) → composer and returns the Project Map Markdown. */
34
+ export declare function discoverProjectMap(projectRoot: string, options?: DiscoverProjectMapOptions): string;
35
+ //# sourceMappingURL=orchestrate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"orchestrate.d.ts","sourceRoot":"","sources":["../../src/discovery/orchestrate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAwB,KAAK,2BAA2B,EAAE,MAAM,WAAW,CAAC;AAEnF,OAAO,EAAuB,KAAK,cAAc,EAAE,MAAM,YAAY,CAAC;AACtE,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AAEtE,MAAM,WAAW,yBAAyB;IACxC,sCAAsC;IACtC,SAAS,CAAC,EAAE,eAAe,CAAC;IAC5B,iCAAiC;IACjC,KAAK,CAAC,EAAE,cAAc,CAAC;IACvB,4CAA4C;IAC5C,aAAa,CAAC,EAAE,2BAA2B,CAAC;CAC7C;AAED,sHAAsH;AACtH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,GAAE,yBAA8B,GAAG,MAAM,CAKvG"}