@topogram/cli 0.3.78 → 0.3.80

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 (100) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/package.json +2 -2
  3. package/src/agent-brief.js +29 -23
  4. package/src/agent-ops/query-builders/change-risk/{import-plan.js → extract-plan.js} +1 -1
  5. package/src/agent-ops/query-builders/change-risk/review-packets.js +5 -5
  6. package/src/agent-ops/query-builders/change-risk.js +1 -1
  7. package/src/agent-ops/query-builders/common.js +2 -2
  8. package/src/agent-ops/query-builders/multi-agent.js +1 -1
  9. package/src/agent-ops/query-builders/workflow-context-shared.js +4 -4
  10. package/src/catalog/provenance.js +1 -1
  11. package/src/cli/catalog-alias.d.ts +2 -0
  12. package/src/cli/catalog-alias.js +2 -2
  13. package/src/cli/command-parser.js +2 -0
  14. package/src/cli/command-parsers/core.js +9 -5
  15. package/src/cli/command-parsers/extractor.js +40 -0
  16. package/src/cli/command-parsers/import.js +11 -17
  17. package/src/cli/command-parsers/project.js +0 -3
  18. package/src/cli/commands/catalog/copy.js +3 -3
  19. package/src/cli/commands/catalog/help.js +1 -2
  20. package/src/cli/commands/catalog/list.js +7 -4
  21. package/src/cli/commands/catalog/show.js +4 -4
  22. package/src/cli/commands/copy.js +356 -0
  23. package/src/cli/commands/doctor.js +1 -1
  24. package/src/cli/commands/extractor.js +451 -0
  25. package/src/cli/commands/import/adopt.js +9 -9
  26. package/src/cli/commands/import/check.js +15 -15
  27. package/src/cli/commands/import/diff.js +6 -6
  28. package/src/cli/commands/import/help.js +45 -34
  29. package/src/cli/commands/import/paths.js +3 -3
  30. package/src/cli/commands/import/plan.js +8 -8
  31. package/src/cli/commands/import/refresh.js +25 -24
  32. package/src/cli/commands/import/status-history.js +4 -4
  33. package/src/cli/commands/import/workspace.js +24 -18
  34. package/src/cli/commands/import-runner.js +10 -7
  35. package/src/cli/commands/import.js +4 -1
  36. package/src/cli/commands/init.js +67 -0
  37. package/src/cli/commands/query/{import-adopt.js → extract-adopt.js} +2 -2
  38. package/src/cli/commands/query/runner/change.js +2 -2
  39. package/src/cli/commands/query/runner/{import-adopt.js → extract-adopt.js} +9 -9
  40. package/src/cli/commands/query/runner/index.js +1 -1
  41. package/src/cli/commands/query/runner/workflow.js +7 -7
  42. package/src/cli/commands/query/workspace.js +4 -4
  43. package/src/cli/commands/release-status.js +2 -2
  44. package/src/cli/commands/source.js +2 -2
  45. package/src/cli/commands/template/check.js +2 -2
  46. package/src/cli/commands/template/list-show.js +4 -4
  47. package/src/cli/dispatcher.js +32 -3
  48. package/src/cli/help-dispatch.js +33 -8
  49. package/src/cli/help.js +79 -52
  50. package/src/cli/migration-guidance.js +9 -0
  51. package/src/cli/options.js +17 -0
  52. package/src/extractor/check.js +155 -0
  53. package/src/extractor/packages.js +295 -0
  54. package/src/extractor/registry.js +196 -0
  55. package/src/extractor-policy.js +249 -0
  56. package/src/generator/check.js +24 -87
  57. package/src/generator/context/bundle.js +14 -7
  58. package/src/generator/context/diff.js +8 -1
  59. package/src/generator/context/digest.js +10 -1
  60. package/src/generator/context/shared/domain-sdlc.js +5 -1
  61. package/src/generator/context/shared/relationships.js +20 -5
  62. package/src/generator/context/shared/summaries.js +26 -0
  63. package/src/generator/context/shared.d.ts +1 -0
  64. package/src/generator/context/shared.js +1 -0
  65. package/src/generator/context/slice/core.js +9 -5
  66. package/src/generator/context/slice/sdlc.js +31 -2
  67. package/src/generator/context/task-mode.js +3 -3
  68. package/src/generator/registry/index.js +16 -75
  69. package/src/generator-policy.js +9 -57
  70. package/src/import/core/registry.d.ts +3 -0
  71. package/src/import/core/registry.js +82 -8
  72. package/src/import/core/runner/reports.js +4 -4
  73. package/src/import/core/runner/run.js +2 -0
  74. package/src/import/core/runner/tracks.js +66 -4
  75. package/src/import/provenance.js +18 -17
  76. package/src/init-project.js +215 -0
  77. package/src/new-project/constants.js +1 -1
  78. package/src/new-project/create.js +2 -2
  79. package/src/new-project/project-files.js +7 -7
  80. package/src/package-adapters/adapter.js +64 -0
  81. package/src/package-adapters/file-map.js +30 -0
  82. package/src/package-adapters/index.js +27 -0
  83. package/src/package-adapters/manifest.js +108 -0
  84. package/src/package-adapters/policy.js +81 -0
  85. package/src/package-adapters/spec.js +51 -0
  86. package/src/reconcile/journeys.js +8 -3
  87. package/src/record-blocks.js +125 -0
  88. package/src/resolver/index.js +3 -0
  89. package/src/resolver/journeys.js +74 -0
  90. package/src/resolver/normalize.js +25 -0
  91. package/src/sdlc/adopt.js +1 -1
  92. package/src/validator/common.js +34 -1
  93. package/src/validator/index.js +4 -0
  94. package/src/validator/kinds.d.ts +2 -0
  95. package/src/validator/kinds.js +34 -1
  96. package/src/validator/per-kind/journey.js +233 -0
  97. package/src/workflows/docs-generate.js +4 -1
  98. package/src/workflows/reconcile/bundle-core/index.js +4 -2
  99. package/src/workflows/reconcile/canonical-surface.js +4 -1
  100. package/src/cli/commands/new.js +0 -94
@@ -4,7 +4,7 @@ import path from "node:path";
4
4
 
5
5
  import { listFilesRecursive, relativeTo } from "./core/shared.js";
6
6
 
7
- export const TOPOGRAM_IMPORT_FILE = ".topogram-import.json";
7
+ export const TOPOGRAM_IMPORT_FILE = ".topogram-extract.json";
8
8
 
9
9
  function fileHash(filePath) {
10
10
  const bytes = fs.readFileSync(filePath);
@@ -42,22 +42,23 @@ export function writeTopogramImportRecord(projectRoot, input) {
42
42
  const timestamp = input.timestamp || new Date().toISOString();
43
43
  const record = {
44
44
  version: "0.1",
45
- kind: "brownfield-import",
46
- importedAt: input.importedAt || timestamp,
45
+ kind: "brownfield-extract",
46
+ extractedAt: input.importedAt || timestamp,
47
47
  ...(input.refreshedAt ? { refreshedAt: input.refreshedAt } : {}),
48
48
  source: {
49
49
  path: path.resolve(input.sourceRoot),
50
50
  hashAlgorithm: "sha256",
51
51
  ignoredRoots: (input.ignoredRoots || []).map((item) => path.resolve(item))
52
52
  },
53
- import: {
53
+ extract: {
54
54
  tracks: input.tracks || [],
55
55
  findingsCount: input.findingsCount || 0,
56
- candidateCounts: input.candidateCounts || {}
56
+ candidateCounts: input.candidateCounts || {},
57
+ extractorPackages: input.extractorPackages || []
57
58
  },
58
59
  ownership: {
59
- importedArtifacts: "project-owned",
60
- note: "Topogram artifacts created by import are editable after import. Source hashes record the brownfield app evidence trusted at import time."
60
+ extractedArtifacts: "project-owned",
61
+ note: "Topogram artifacts created by extraction are editable after extraction. Source hashes record the brownfield app evidence trusted at extraction time."
61
62
  },
62
63
  ...(input.refresh ? { refresh: input.refresh } : {}),
63
64
  files: input.files || []
@@ -79,11 +80,11 @@ export function buildTopogramImportStatus(projectRoot) {
79
80
  source: null,
80
81
  content: { changed: [], added: [], removed: [] },
81
82
  diagnostics: [{
82
- code: "topogram_import_missing",
83
+ code: "topogram_extract_missing",
83
84
  severity: "error",
84
- message: `${TOPOGRAM_IMPORT_FILE} was not found. This workspace does not have brownfield import provenance.`,
85
+ message: `${TOPOGRAM_IMPORT_FILE} was not found. This workspace does not have brownfield extraction provenance.`,
85
86
  path: importPath,
86
- suggestedFix: "Run `topogram import <app-path> --out <target>` to create an imported Topogram workspace."
87
+ suggestedFix: "Run `topogram extract <app-path> --out <target>` to create an extracted Topogram workspace."
87
88
  }],
88
89
  errors: [`${TOPOGRAM_IMPORT_FILE} was not found.`]
89
90
  };
@@ -92,7 +93,7 @@ export function buildTopogramImportStatus(projectRoot) {
92
93
  const source = JSON.parse(fs.readFileSync(importPath, "utf8"));
93
94
  const sourceRoot = path.resolve(source.source?.path || "");
94
95
  if (!sourceRoot || !fs.existsSync(sourceRoot) || !fs.statSync(sourceRoot).isDirectory()) {
95
- const message = `Imported source path was not found: ${source.source?.path || "unknown"}`;
96
+ const message = `Extracted source path was not found: ${source.source?.path || "unknown"}`;
96
97
  return {
97
98
  ok: false,
98
99
  exists: true,
@@ -101,11 +102,11 @@ export function buildTopogramImportStatus(projectRoot) {
101
102
  source,
102
103
  content: { changed: [], added: [], removed: [] },
103
104
  diagnostics: [{
104
- code: "topogram_import_source_missing",
105
+ code: "topogram_extract_source_missing",
105
106
  severity: "error",
106
107
  message,
107
108
  path: source.source?.path || null,
108
- suggestedFix: "Restore the imported source path or rerun import from the current brownfield app location."
109
+ suggestedFix: "Restore the extracted source path or rerun extract from the current brownfield app location."
109
110
  }],
110
111
  errors: [message]
111
112
  };
@@ -147,12 +148,12 @@ export function buildTopogramImportStatus(projectRoot) {
147
148
  source,
148
149
  content,
149
150
  diagnostics: clean ? [] : [{
150
- code: "topogram_import_source_changed",
151
+ code: "topogram_extract_source_changed",
151
152
  severity: "error",
152
- message: "Imported source files changed since they were trusted for this import.",
153
+ message: "Extracted source files changed since they were trusted for this extraction.",
153
154
  path: sourceRoot,
154
- suggestedFix: "Review the source changes. If they should drive Topogram changes, rerun import or update the Topogram artifacts manually."
155
+ suggestedFix: "Review the source changes. If they should drive Topogram changes, rerun extract or update the Topogram artifacts manually."
155
156
  }],
156
- errors: clean ? [] : ["Imported source files changed since import."]
157
+ errors: clean ? [] : ["Extracted source files changed since extraction."]
157
158
  };
158
159
  }
@@ -0,0 +1,215 @@
1
+ // @ts-check
2
+
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+
6
+ import { defaultSdlcPolicy, SDLC_POLICY_FILE } from "./sdlc/policy.js";
7
+ import { DEFAULT_TOPO_FOLDER_NAME, DEFAULT_WORKSPACE_PATH, PROJECT_CONFIG_FILE } from "./workspace-paths.js";
8
+
9
+ /**
10
+ * @typedef {Object} InitProjectOptions
11
+ * @property {string} [targetPath]
12
+ * @property {boolean} [withSdlc]
13
+ */
14
+
15
+ /**
16
+ * @typedef {Object} InitProjectResult
17
+ * @property {boolean} ok
18
+ * @property {string} projectRoot
19
+ * @property {string} workspaceRoot
20
+ * @property {string} projectConfigPath
21
+ * @property {string[]} created
22
+ * @property {string[]} skipped
23
+ * @property {Record<string, any>} projectConfig
24
+ * @property {{ enabled: boolean, path: string|null }} sdlc
25
+ */
26
+
27
+ /**
28
+ * @param {string} projectRoot
29
+ * @param {string} targetPath
30
+ * @returns {string}
31
+ */
32
+ function relativeProjectPath(projectRoot, targetPath) {
33
+ const relative = path.relative(projectRoot, targetPath);
34
+ return relative ? relative.split(path.sep).join("/") : ".";
35
+ }
36
+
37
+ /**
38
+ * @param {string} projectRoot
39
+ * @param {string} filePath
40
+ * @param {string} content
41
+ * @param {string[]} created
42
+ * @param {string[]} skipped
43
+ * @returns {void}
44
+ */
45
+ function writeIfMissing(projectRoot, filePath, content, created, skipped) {
46
+ if (fs.existsSync(filePath)) {
47
+ skipped.push(relativeProjectPath(projectRoot, filePath));
48
+ return;
49
+ }
50
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
51
+ fs.writeFileSync(filePath, content, "utf8");
52
+ created.push(relativeProjectPath(projectRoot, filePath));
53
+ }
54
+
55
+ /**
56
+ * @returns {Record<string, any>}
57
+ */
58
+ function defaultMaintainedProjectConfig() {
59
+ return {
60
+ version: "0.1",
61
+ workspace: DEFAULT_WORKSPACE_PATH,
62
+ outputs: {
63
+ app: {
64
+ path: ".",
65
+ ownership: "maintained"
66
+ }
67
+ },
68
+ topology: {
69
+ runtimes: []
70
+ }
71
+ };
72
+ }
73
+
74
+ /**
75
+ * @returns {string}
76
+ */
77
+ function initializedReadme() {
78
+ return `# Topogram Project
79
+
80
+ Initialized with \`topogram init\`.
81
+
82
+ This repository is treated as a maintained app or workspace: Topogram will not
83
+ overwrite source code under \`./\`. Use \`topogram emit\` for contracts, reports,
84
+ snapshots, and proposals, and edit maintained app code directly after reading
85
+ focused query packets.
86
+
87
+ ## First Commands
88
+
89
+ \`\`\`bash
90
+ topogram agent brief --json
91
+ topogram check --json
92
+ topogram query list --json
93
+ \`\`\`
94
+
95
+ To adopt enforced SDLC after initialization, run:
96
+
97
+ \`\`\`bash
98
+ topogram sdlc policy init .
99
+ \`\`\`
100
+
101
+ ## Source
102
+
103
+ - \`topo/\` is the project-owned Topogram workspace.
104
+ - \`topogram.project.json\` declares workspace, output ownership, and runtime topology.
105
+ - Output \`app\` points at \`.\` with \`maintained\` ownership.
106
+ `;
107
+ }
108
+
109
+ /**
110
+ * @returns {string}
111
+ */
112
+ function initializedAgentsGuide() {
113
+ return `# Agent Guide
114
+
115
+ This repository was initialized with \`topogram init\`.
116
+
117
+ Start with:
118
+
119
+ \`\`\`bash
120
+ topogram agent brief --json
121
+ topogram check --json
122
+ topogram query list --json
123
+ \`\`\`
124
+
125
+ Edit \`topo/**\` and \`topogram.project.json\` for Topogram source. The project
126
+ output is maintained, so app/source files under \`./\` are human-owned and may be
127
+ edited directly after reading focused packets.
128
+
129
+ Use \`topogram emit <target>\` for contracts, reports, snapshots, migration
130
+ plans, and agent context. Do not expect \`topogram generate\` to overwrite this
131
+ maintained app unless output ownership is deliberately changed.
132
+
133
+ If \`topogram.sdlc-policy.json\` exists, use SDLC commands for task and status
134
+ work before protected edits:
135
+
136
+ \`\`\`bash
137
+ topogram sdlc policy explain --json
138
+ topogram sdlc prep commit . --json
139
+ \`\`\`
140
+ `;
141
+ }
142
+
143
+ /**
144
+ * @param {string} projectRoot
145
+ * @returns {void}
146
+ */
147
+ function assertInitTarget(projectRoot) {
148
+ if (fs.existsSync(projectRoot) && !fs.statSync(projectRoot).isDirectory()) {
149
+ throw new Error(`Cannot initialize Topogram at '${projectRoot}' because it is not a directory.`);
150
+ }
151
+ const configPath = path.join(projectRoot, PROJECT_CONFIG_FILE);
152
+ if (fs.existsSync(configPath)) {
153
+ throw new Error(`Refusing to initialize Topogram because ${PROJECT_CONFIG_FILE} already exists.`);
154
+ }
155
+ const workspaceRoot = path.join(projectRoot, DEFAULT_TOPO_FOLDER_NAME);
156
+ if (fs.existsSync(workspaceRoot)) {
157
+ if (!fs.statSync(workspaceRoot).isDirectory()) {
158
+ throw new Error(`Refusing to initialize Topogram because ${DEFAULT_TOPO_FOLDER_NAME}/ exists and is not a directory.`);
159
+ }
160
+ const entries = fs.readdirSync(workspaceRoot).filter(/** @param {string} entry */ (entry) => entry !== ".DS_Store");
161
+ if (entries.length > 0) {
162
+ throw new Error(`Refusing to initialize Topogram because ${DEFAULT_TOPO_FOLDER_NAME}/ already exists and is not empty.`);
163
+ }
164
+ }
165
+ }
166
+
167
+ /**
168
+ * @param {InitProjectOptions} [options]
169
+ * @returns {InitProjectResult}
170
+ */
171
+ export function initTopogramProject(options = {}) {
172
+ const projectRoot = path.resolve(options.targetPath || ".");
173
+ assertInitTarget(projectRoot);
174
+ fs.mkdirSync(projectRoot, { recursive: true });
175
+
176
+ /** @type {string[]} */
177
+ const created = [];
178
+ /** @type {string[]} */
179
+ const skipped = [];
180
+ const workspaceRoot = path.join(projectRoot, DEFAULT_TOPO_FOLDER_NAME);
181
+ fs.mkdirSync(workspaceRoot, { recursive: true });
182
+ created.push(DEFAULT_TOPO_FOLDER_NAME);
183
+
184
+ writeIfMissing(projectRoot, path.join(workspaceRoot, ".gitkeep"), "", created, skipped);
185
+ const projectConfig = defaultMaintainedProjectConfig();
186
+ const projectConfigPath = path.join(projectRoot, PROJECT_CONFIG_FILE);
187
+ fs.writeFileSync(projectConfigPath, `${JSON.stringify(projectConfig, null, 2)}\n`, "utf8");
188
+ created.push(PROJECT_CONFIG_FILE);
189
+ writeIfMissing(projectRoot, path.join(projectRoot, "README.md"), initializedReadme(), created, skipped);
190
+ writeIfMissing(projectRoot, path.join(projectRoot, "AGENTS.md"), initializedAgentsGuide(), created, skipped);
191
+ const sdlcPolicyPath = path.join(projectRoot, SDLC_POLICY_FILE);
192
+ if (options.withSdlc) {
193
+ writeIfMissing(
194
+ projectRoot,
195
+ sdlcPolicyPath,
196
+ `${JSON.stringify(defaultSdlcPolicy(), null, 2)}\n`,
197
+ created,
198
+ skipped
199
+ );
200
+ }
201
+
202
+ return {
203
+ ok: true,
204
+ projectRoot,
205
+ workspaceRoot,
206
+ projectConfigPath,
207
+ created,
208
+ skipped,
209
+ projectConfig,
210
+ sdlc: {
211
+ enabled: options.withSdlc ? fs.existsSync(sdlcPolicyPath) : false,
212
+ path: options.withSdlc ? sdlcPolicyPath : null
213
+ }
214
+ };
215
+ }
@@ -31,7 +31,7 @@ export const SURFACE_ORDER = new Map([
31
31
  * @returns {string}
32
32
  */
33
33
  export function unsupportedTemplateSymlinkMessage(templateId, relativePath) {
34
- return `Template '${templateId}' contains unsupported symlink '${relativePath}'. Template packs must copy real files because Topogram records hashes for copied workspace and implementation/ content; symlinks can point outside the trusted template root. Replace the symlink with a real file or directory before running topogram new or topogram template check.`;
34
+ return `Template '${templateId}' contains unsupported symlink '${relativePath}'. Template packs must copy real files because Topogram records hashes for copied workspace and implementation/ content; symlinks can point outside the trusted template root. Replace the symlink with a real file or directory before running topogram copy or topogram template check.`;
35
35
  }
36
36
 
37
37
  /**
@@ -36,7 +36,7 @@ export function createNewProject({
36
36
  templateProvenance = null
37
37
  }) {
38
38
  if (!targetPath) {
39
- throw new Error("topogram new requires <path>.");
39
+ throw new Error("topogram copy requires <target>.");
40
40
  }
41
41
  const projectRoot = path.resolve(targetPath);
42
42
  assertProjectOutsideEngine(projectRoot, engineRoot);
@@ -68,7 +68,7 @@ export function createNewProject({
68
68
  writeTemplateTrustRecord(projectRoot, projectConfig);
69
69
  warnings.push(
70
70
  `Template '${template.manifest.id}' copied implementation/ code into this project. ` +
71
- "topogram new did not execute it, but topogram generate may load it later. " +
71
+ "topogram copy did not execute it, but topogram generate may load it later. " +
72
72
  "Recorded local trust in .topogram-template-trust.json."
73
73
  );
74
74
  }
@@ -247,7 +247,7 @@ export function writeProjectReadme(projectRoot, projectConfig) {
247
247
  provenanceLines.push(`- Executable implementation: \`${template.includesExecutableImplementation ? "yes" : "no"}\``);
248
248
  const readme = `# ${packageNameFromPath(projectRoot)}
249
249
 
250
- Generated by \`topogram new\`.
250
+ Copied by \`topogram copy\`.
251
251
 
252
252
  ## Template
253
253
 
@@ -263,7 +263,7 @@ Edit the workspace folder \`${DEFAULT_TOPO_FOLDER_NAME}/\` and \`topogram.projec
263
263
  Generated app code is written to \`app/\`.
264
264
  Use \`topogram emit <target>\` to inspect contracts, reports, snapshots, and other artifacts without regenerating the app.
265
265
  Agents should start with \`AGENTS.md\` and \`npm run agent:brief\`. The direct \`topogram agent brief --json\` command is the canonical machine-readable first-run guidance.
266
- ${template.includesExecutableImplementation ? "\nThis template copied `implementation/` code. `topogram new` did not execute it; review `implementation/`, `topogram.template-policy.json`, and `.topogram-template-trust.json` before regenerating after edits.\n" : ""}
266
+ ${template.includesExecutableImplementation ? "\nThis template copied `implementation/` code. `topogram copy` did not execute it; review `implementation/`, `topogram.template-policy.json`, and `.topogram-template-trust.json` before regenerating after edits.\n" : ""}
267
267
  `;
268
268
  fs.writeFileSync(path.join(projectRoot, "README.md"), readme, "utf8");
269
269
  }
@@ -339,12 +339,12 @@ npm run query:show -- widget-behavior
339
339
 
340
340
  - Local edits to template-derived Topogram files are project-owned.
341
341
  - Use \`npm run source:status\` and \`npm run template:update:recommend\` before applying template updates.
342
- ${hasImplementation ? "- This project has executable `implementation/` code. `topogram new` did not execute it. Do not refresh trust until the implementation has been reviewed.\n" : "- This template does not declare executable implementation code.\n"}
343
- ## Import And Adoption
342
+ ${hasImplementation ? "- This project has executable `implementation/` code. `topogram copy` did not execute it. Do not refresh trust until the implementation has been reviewed.\n" : "- This template does not declare executable implementation code.\n"}
343
+ ## Extract And Adopt
344
344
 
345
- - If \`.topogram-import.json\` exists, agents should run \`topogram import check . --json\`, \`topogram import plan . --json\`, \`topogram import adopt --list . --json\`, \`topogram import status . --json\`, and \`topogram import history . --verify --json\`.
346
- - Import JSON payloads expose \`workspaceRoot\`; prefer it as the canonical project-owned workspace path.
347
- - Imported Topogram files are project-owned after adoption; source hashes record trusted import evidence at the time of import.
345
+ - If \`.topogram-extract.json\` exists, agents should run \`topogram extract check . --json\`, \`topogram extract plan . --json\`, \`topogram adopt --list . --json\`, \`topogram extract status . --json\`, and \`topogram extract history . --verify --json\`.
346
+ - Extract JSON payloads expose \`workspaceRoot\`; prefer it as the canonical project-owned workspace path.
347
+ - Extracted Topogram files are project-owned after adoption; source hashes record trusted source evidence at the time of extraction.
348
348
 
349
349
  ## Verification Gates
350
350
 
@@ -0,0 +1,64 @@
1
+ // @ts-check
2
+
3
+ import path from "node:path";
4
+ import { createRequire } from "node:module";
5
+
6
+ /**
7
+ * @param {any} moduleValue
8
+ * @param {string|null|undefined} exportName
9
+ * @returns {any}
10
+ */
11
+ export function selectPackageExport(moduleValue, exportName) {
12
+ if (exportName) {
13
+ return moduleValue?.[exportName] || moduleValue?.default?.[exportName] || null;
14
+ }
15
+ return moduleValue?.default || moduleValue;
16
+ }
17
+
18
+ /**
19
+ * @param {{
20
+ * packageRoot: string,
21
+ * exportName?: string|null,
22
+ * packageLabel: string
23
+ * }} options
24
+ * @returns {{ adapter: any|null, error: string|null }}
25
+ */
26
+ export function loadLocalPackageAdapter(options) {
27
+ try {
28
+ const packageJsonPath = path.join(options.packageRoot, "package.json");
29
+ const requireFromPackage = createRequire(packageJsonPath);
30
+ return {
31
+ adapter: selectPackageExport(requireFromPackage(options.packageRoot), options.exportName),
32
+ error: null
33
+ };
34
+ } catch (error) {
35
+ return {
36
+ adapter: null,
37
+ error: `${options.packageLabel} export could not be loaded from '${options.packageRoot}': ${error instanceof Error ? error.message : String(error)}`
38
+ };
39
+ }
40
+ }
41
+
42
+ /**
43
+ * @param {{
44
+ * packageName: string,
45
+ * rootDir: string,
46
+ * exportName?: string|null,
47
+ * packageLabel: string
48
+ * }} options
49
+ * @returns {{ adapter: any|null, error: string|null }}
50
+ */
51
+ export function loadInstalledPackageAdapter(options) {
52
+ try {
53
+ const requireFromRoot = createRequire(path.join(options.rootDir, "package.json"));
54
+ return {
55
+ adapter: selectPackageExport(requireFromRoot(options.packageName), options.exportName),
56
+ error: null
57
+ };
58
+ } catch (error) {
59
+ return {
60
+ adapter: null,
61
+ error: `${options.packageLabel} '${options.packageName}' export could not be loaded from '${options.rootDir}': ${error instanceof Error ? error.message : String(error)}`
62
+ };
63
+ }
64
+ }
@@ -0,0 +1,30 @@
1
+ // @ts-check
2
+
3
+ import path from "node:path";
4
+
5
+ /**
6
+ * @param {any} files
7
+ * @param {{ filePathMessage: string, contentMessage: (filePath: string) => string }} messages
8
+ * @returns {{ ok: boolean, message: string }}
9
+ */
10
+ export function validateRelativeStringFileMap(files, messages) {
11
+ if (!files || typeof files !== "object" || Array.isArray(files)) {
12
+ return { ok: false, message: "files must be an object" };
13
+ }
14
+ for (const [filePath, content] of Object.entries(files)) {
15
+ const normalizedPath = typeof filePath === "string" ? path.normalize(filePath) : "";
16
+ if (
17
+ typeof filePath !== "string" ||
18
+ filePath.length === 0 ||
19
+ path.isAbsolute(filePath) ||
20
+ normalizedPath === ".." ||
21
+ normalizedPath.startsWith(`..${path.sep}`)
22
+ ) {
23
+ return { ok: false, message: messages.filePathMessage };
24
+ }
25
+ if (typeof content !== "string") {
26
+ return { ok: false, message: messages.contentMessage(filePath) };
27
+ }
28
+ }
29
+ return { ok: true, message: "files are valid" };
30
+ }
@@ -0,0 +1,27 @@
1
+ // @ts-check
2
+
3
+ export {
4
+ loadInstalledPackageAdapter,
5
+ loadLocalPackageAdapter,
6
+ selectPackageExport
7
+ } from "./adapter.js";
8
+ export {
9
+ loadPackageManifest,
10
+ resolvePackageManifestPath
11
+ } from "./manifest.js";
12
+ export {
13
+ isPathSpec,
14
+ packageInstallCommand,
15
+ packageInstallHint,
16
+ packageNameFromSpec,
17
+ packageResolutionBase
18
+ } from "./spec.js";
19
+ export {
20
+ optionalStringArray,
21
+ optionalStringRecord,
22
+ packageAllowedByPolicy,
23
+ packageScopeFromName
24
+ } from "./policy.js";
25
+ export {
26
+ validateRelativeStringFileMap
27
+ } from "./file-map.js";
@@ -0,0 +1,108 @@
1
+ // @ts-check
2
+
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { createRequire } from "node:module";
6
+
7
+ import { packageInstallHint, packageResolutionBase } from "./spec.js";
8
+
9
+ /**
10
+ * @typedef {Object} PackageManifestResolution
11
+ * @property {string|null} manifestPath
12
+ * @property {string|null} packageRoot
13
+ * @property {string|null} error
14
+ */
15
+
16
+ /**
17
+ * @typedef {{ ok: boolean, errors: string[] }} ManifestValidation
18
+ */
19
+
20
+ /**
21
+ * @param {string} packageName
22
+ * @param {string} manifestFile
23
+ * @param {string|null|undefined} rootDir
24
+ * @param {string} packageLabel
25
+ * @returns {PackageManifestResolution}
26
+ */
27
+ export function resolvePackageManifestPath(packageName, manifestFile, rootDir = process.cwd(), packageLabel = "Package") {
28
+ const requireFromRoot = createRequire(packageResolutionBase(rootDir));
29
+ try {
30
+ const manifestPath = requireFromRoot.resolve(`${packageName}/${manifestFile}`);
31
+ return {
32
+ manifestPath,
33
+ packageRoot: path.dirname(manifestPath),
34
+ error: null
35
+ };
36
+ } catch (manifestError) {
37
+ try {
38
+ const packageJsonPath = requireFromRoot.resolve(`${packageName}/package.json`);
39
+ const packageRoot = path.dirname(packageJsonPath);
40
+ const manifestPath = path.join(packageRoot, manifestFile);
41
+ if (!fs.existsSync(manifestPath)) {
42
+ return {
43
+ manifestPath: null,
44
+ packageRoot,
45
+ error: `${packageLabel} '${packageName}' is missing ${manifestFile}`
46
+ };
47
+ }
48
+ return {
49
+ manifestPath,
50
+ packageRoot,
51
+ error: null
52
+ };
53
+ } catch {
54
+ const detail = manifestError instanceof Error ? manifestError.message : String(manifestError);
55
+ const installHint = packageInstallHint(packageName);
56
+ return {
57
+ manifestPath: null,
58
+ packageRoot: null,
59
+ error: `${packageLabel} '${packageName}' could not be resolved from '${rootDir || process.cwd()}': ${detail}${installHint ? `. ${installHint}` : ""}`
60
+ };
61
+ }
62
+ }
63
+ }
64
+
65
+ /**
66
+ * @template T
67
+ * @param {{
68
+ * packageName: string,
69
+ * rootDir?: string|null,
70
+ * manifestFile: string,
71
+ * packageLabel: string,
72
+ * validateManifest: (manifest: any) => ManifestValidation
73
+ * }} options
74
+ * @returns {{ manifest: T|null, errors: string[], manifestPath: string|null, packageRoot: string|null }}
75
+ */
76
+ export function loadPackageManifest(options) {
77
+ const resolved = resolvePackageManifestPath(
78
+ options.packageName,
79
+ options.manifestFile,
80
+ options.rootDir || process.cwd(),
81
+ options.packageLabel
82
+ );
83
+ if (!resolved.manifestPath) {
84
+ return {
85
+ manifest: null,
86
+ errors: [resolved.error || `${options.packageLabel} '${options.packageName}' could not be resolved`],
87
+ manifestPath: null,
88
+ packageRoot: resolved.packageRoot
89
+ };
90
+ }
91
+ try {
92
+ const manifest = JSON.parse(fs.readFileSync(resolved.manifestPath, "utf8"));
93
+ const validation = options.validateManifest(manifest);
94
+ return {
95
+ manifest: validation.ok ? /** @type {T} */ (manifest) : null,
96
+ errors: validation.errors,
97
+ manifestPath: resolved.manifestPath,
98
+ packageRoot: resolved.packageRoot
99
+ };
100
+ } catch (error) {
101
+ return {
102
+ manifest: null,
103
+ errors: [`${options.packageLabel} '${options.packageName}' manifest could not be read: ${error instanceof Error ? error.message : String(error)}`],
104
+ manifestPath: resolved.manifestPath,
105
+ packageRoot: resolved.packageRoot
106
+ };
107
+ }
108
+ }
@@ -0,0 +1,81 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * @typedef {Object} PackagePolicy
5
+ * @property {string} version
6
+ * @property {string[]} allowedPackageScopes
7
+ * @property {string[]} allowedPackages
8
+ * @property {Record<string, string>} pinnedVersions
9
+ */
10
+
11
+ /**
12
+ * @param {unknown} value
13
+ * @param {string} fieldName
14
+ * @param {string} policyPath
15
+ * @returns {string[]}
16
+ */
17
+ export function optionalStringArray(value, fieldName, policyPath) {
18
+ if (value == null) {
19
+ return [];
20
+ }
21
+ if (!Array.isArray(value)) {
22
+ throw new Error(`${policyPath} ${fieldName} must be an array of strings.`);
23
+ }
24
+ return value.map((item) => {
25
+ if (typeof item !== "string" || item.length === 0) {
26
+ throw new Error(`${policyPath} ${fieldName} must contain only non-empty strings.`);
27
+ }
28
+ return item;
29
+ });
30
+ }
31
+
32
+ /**
33
+ * @param {unknown} value
34
+ * @param {string} policyPath
35
+ * @param {string} [pinnedLabel]
36
+ * @returns {Record<string, string>}
37
+ */
38
+ export function optionalStringRecord(value, policyPath, pinnedLabel = "package ids") {
39
+ if (value == null) {
40
+ return {};
41
+ }
42
+ if (typeof value !== "object" || Array.isArray(value)) {
43
+ throw new Error(`${policyPath} pinnedVersions must be an object of ${pinnedLabel} to versions.`);
44
+ }
45
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => {
46
+ if (typeof item !== "string" || item.length === 0) {
47
+ throw new Error(`${policyPath} pinnedVersions['${key}'] must be a non-empty string.`);
48
+ }
49
+ return [key, item];
50
+ }));
51
+ }
52
+
53
+ /**
54
+ * @param {string} packageName
55
+ * @returns {string|null}
56
+ */
57
+ export function packageScopeFromName(packageName) {
58
+ return packageName.startsWith("@") ? packageName.split("/")[0] || null : null;
59
+ }
60
+
61
+ /**
62
+ * @param {string} allowed
63
+ * @param {string|null} scope
64
+ * @returns {boolean}
65
+ */
66
+ function packageScopeMatches(allowed, scope) {
67
+ return Boolean(scope && (allowed === scope || allowed === `${scope}/*`));
68
+ }
69
+
70
+ /**
71
+ * @param {PackagePolicy} policy
72
+ * @param {string} packageName
73
+ * @returns {boolean}
74
+ */
75
+ export function packageAllowedByPolicy(policy, packageName) {
76
+ if (policy.allowedPackages.includes(packageName)) {
77
+ return true;
78
+ }
79
+ const scope = packageScopeFromName(packageName);
80
+ return policy.allowedPackageScopes.some((allowed) => packageScopeMatches(allowed, scope));
81
+ }