nexarch 0.12.9 → 0.12.11

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.
@@ -477,27 +477,46 @@ function injectTrustAttestationUnavailableBlock(path, reason) {
477
477
  }
478
478
  function injectInitProjectReportingContract(path) {
479
479
  const section = [
480
- "## Nexarch Init-Project Reporting Contract",
480
+ "## Nexarch Init-Project Completion Contract",
481
481
  "",
482
- "When you run `npx nexarch@latest init-project`, treat it as skeleton registration only.",
483
- "Do not claim full completion until enrichment commands are actually executed.",
482
+ "`init-project` is the first step of registration, not the whole task. It returns",
483
+ "`registrationStatus`, `enrichmentCompleted` and a `nextSteps` array. While",
484
+ "`enrichmentCompleted` is `false`, the work is unfinished.",
484
485
  "",
485
- "Useful status fields to report after init-project:",
486
- '- `registration_status`: `"skeleton_only"` or `"enriched"`',
487
- '- `enrichment_completed`: `true` or `false`',
486
+ "**Run the commands in `nextSteps` before you report back.** They are the step that",
487
+ "puts real content in the graph. Reporting `skeleton_only` and stopping is not a",
488
+ "completed task it is a description of a task you have not finished.",
489
+ "",
490
+ "For an infrastructure repository, `nextSteps` holds one `ingest-infra` command per",
491
+ "Terraform root module. Until they run, the graph knows the repository exists but",
492
+ "nothing it provisions.",
488
493
  "",
489
- "Notes:",
490
- '- While `enrichment_completed=false`, the project is a skeleton; describing it as fully registered would overstate what is in the graph.',
491
- '- Enrichment uses explicit per-entity `update-entity` runs rather than a bulk shortcut, so each entity gets evidence-based content.',
494
+ "Stop early only when a step genuinely cannot run — missing cloud credentials, a root",
495
+ "module that needs `terraform init`, or an environment you should not touch without",
496
+ "asking. When that happens, say which step stopped you and why, then ask. Do not",
497
+ "silently return a skeleton.",
492
498
  "",
493
- "You may set `registration_status=\"enriched\"` only after executing enrichment actions (e.g. update-entity for project/sub-packages, aliases, and any chosen gap wiring), with evidence-based per-entity descriptions/subtypes.",
499
+ "When reporting:",
500
+ '- `registration_status`: `"skeleton_only"` or `"enriched"` — take it from the command output, do not infer it',
501
+ '- `enrichment_completed`: `true` or `false`',
502
+ '- Describing a skeleton as fully registered overstates what is in the graph; so does reporting success without saying what remains.',
503
+ "",
504
+ "Enrichment uses explicit per-entity `update-entity` runs rather than a bulk shortcut,",
505
+ "so each entity gets evidence-based content.",
494
506
  "",
495
507
  ].join("\n");
496
508
  const existing = existsSync(path) ? readFileSync(path, "utf8") : "";
497
509
  const managed = wrapManagedSection("init-project-reporting-contract", section.trim());
498
510
  let replaced = replaceManagedSection(existing, "init-project-reporting-contract", section.trim());
499
511
  if (replaced === existing) {
500
- replaced = replaceInjectedSection(existing, "## Nexarch Init-Project Reporting Contract", managed);
512
+ // The section was renamed from "Reporting Contract" to "Completion Contract".
513
+ // Files written before managed markers existed carry the old heading, so both
514
+ // are matched — otherwise the rename would leave the superseded contract in
515
+ // place alongside the new one, and the old one tells the agent to stop.
516
+ replaced = replaceInjectedSection(existing, "## Nexarch Init-Project Completion Contract", managed);
517
+ if (replaced === existing) {
518
+ replaced = replaceInjectedSection(existing, "## Nexarch Init-Project Reporting Contract", managed);
519
+ }
501
520
  }
502
521
  writeFileSync(path, replaced !== existing ? replaced : `${existing}${existing.endsWith("\n") ? "" : "\n"}${managed}\n`, "utf8");
503
522
  }
@@ -25,6 +25,36 @@ async function confirm(question) {
25
25
  rl.close();
26
26
  }
27
27
  }
28
+ /**
29
+ * The ingest runs that still have to happen, one per root module.
30
+ *
31
+ * Registration records the repository; it does not record the estate. Only
32
+ * state does that, and state is per root module — seven of them here, each
33
+ * needing its own Terraform run. Naming this as a list of commands rather than
34
+ * a paragraph of advice is the difference between an agent finishing the job
35
+ * and an agent reporting "skeleton_only" and stopping, which is exactly what
36
+ * happened when the handoff lived only in prose at the end of a real run.
37
+ */
38
+ function nextStepsFor(projectRef, detection) {
39
+ return detection.rootModules.map((root) => ({
40
+ rootModule: root.relative,
41
+ environment: root.environmentHint,
42
+ initialised: root.initialised,
43
+ command: `nexarch ingest-infra --dir ${root.relative} --project-ref ${projectRef}`,
44
+ }));
45
+ }
46
+ /** Prints the per-root ingest commands, the part that actually populates the graph. */
47
+ function printNextSteps(steps, registered) {
48
+ if (steps.length === 0)
49
+ return;
50
+ console.log(`\n${registered ? "Registered" : "Would register"} the repository — the estate itself is not in the graph yet.`);
51
+ console.log(`Each of the ${steps.length} root modules holds its own state and needs its own ingest:\n`);
52
+ for (const step of steps) {
53
+ const notes = [step.environment ? `${step.environment} environment` : "shared stack", step.initialised ? "initialised" : "run `terraform init` here first"].join(", ");
54
+ console.log(` # ${step.rootModule} — ${notes}`);
55
+ console.log(` ${step.command}`);
56
+ }
57
+ }
28
58
  /**
29
59
  * Shows what a real run would write.
30
60
  *
@@ -35,8 +65,19 @@ async function confirm(question) {
35
65
  */
36
66
  function reportDryRun(params) {
37
67
  const { asJson, projectRef, detection, entities, relationships } = params;
68
+ const nextSteps = nextStepsFor(projectRef, detection);
38
69
  if (asJson) {
39
- process.stdout.write(`${JSON.stringify({ ok: true, dryRun: true, wrote: false, projectRef, detection, plan: { entities, relationships } }, null, 2)}\n`);
70
+ process.stdout.write(`${JSON.stringify({
71
+ ok: true,
72
+ dryRun: true,
73
+ wrote: false,
74
+ projectRef,
75
+ registrationStatus: "skeleton_only",
76
+ enrichmentCompleted: false,
77
+ detection,
78
+ plan: { entities, relationships },
79
+ nextSteps,
80
+ }, null, 2)}\n`);
40
81
  return;
41
82
  }
42
83
  console.log(`\nWould write ${entities.length} entit${entities.length === 1 ? "y" : "ies"}:`);
@@ -47,7 +88,11 @@ function reportDryRun(params) {
47
88
  for (const rel of relationships)
48
89
  console.log(` ${rel.fromEntityRef} --${rel.relationshipTypeCode}--> ${rel.toEntityRef}`);
49
90
  }
50
- console.log(`\nNothing was written. Re-run without --dry-run to register.`);
91
+ // The dry run used to stop here, which made registration look like the whole
92
+ // job: a reader saw four entities and no hint that seven ingest runs were the
93
+ // actual work. A preview has to preview the handoff too.
94
+ printNextSteps(nextSteps, false);
95
+ console.log(`\nNothing was written. Re-run without --dry-run to register, then run the commands above.`);
51
96
  }
52
97
  export async function runInfrastructureOnboarding(options, detection) {
53
98
  const { dir, nameOverride, asJson, nonInteractive, skipIngest, dryRun } = options;
@@ -132,15 +177,9 @@ export async function runInfrastructureOnboarding(options, detection) {
132
177
  // the map up front — rather than letting the caller discover it through a
133
178
  // failure — is what lets an agent finish the job unaided.
134
179
  const roots = detection.rootModules;
135
- if (!asJson && roots.length > 1) {
136
- console.log(`\nThis repository has ${roots.length} root modules, each with its own state.`);
137
- console.log("Ingest them one at a time — every root module is a separate estate:\n");
138
- for (const root of roots) {
139
- const notes = [root.environmentHint ? `${root.environmentHint} environment` : "shared stack", root.initialised ? "initialised" : "run `terraform init` here first"].join(", ");
140
- console.log(` # ${root.relative} — ${notes}`);
141
- console.log(` nexarch ingest-infra --dir ${root.relative} --project-ref ${projectRef}`);
142
- }
143
- }
180
+ const nextSteps = nextStepsFor(projectRef, detection);
181
+ if (!asJson && roots.length > 1)
182
+ printNextSteps(nextSteps, true);
144
183
  let ingested = false;
145
184
  if (!skipIngest && roots.length <= 1) {
146
185
  const shouldIngest = nonInteractive ? false : await confirm("\nRead the current state and populate the graph now?");
@@ -156,9 +195,23 @@ export async function runInfrastructureOnboarding(options, detection) {
156
195
  }
157
196
  }
158
197
  }
198
+ // Registration alone is a skeleton, and saying so in the payload keeps the
199
+ // caller's report honest: the numbers it needs to decide "am I finished?"
200
+ // come from the command that did the work, not from a convention it half
201
+ // remembers.
202
+ const outstanding = ingested ? nextSteps.filter((step) => step.rootModule !== "." && roots.length > 1) : nextSteps;
159
203
  const { file, snippet } = ciSnippetFor(detection.ciSystem, detection.ciFile);
160
204
  if (asJson) {
161
- process.stdout.write(`${JSON.stringify({ ok: true, projectRef, detection, ingested, ci: { file, snippet } }, null, 2)}\n`);
205
+ process.stdout.write(`${JSON.stringify({
206
+ ok: true,
207
+ projectRef,
208
+ registrationStatus: outstanding.length > 0 ? "skeleton_only" : "enriched",
209
+ enrichmentCompleted: outstanding.length === 0,
210
+ detection,
211
+ ingested,
212
+ nextSteps: outstanding,
213
+ ci: { file, snippet },
214
+ }, null, 2)}\n`);
162
215
  return;
163
216
  }
164
217
  console.log(`\nInfrastructure changes continuously, so ingestion belongs in the pipeline, not in someone's memory.`);
@@ -166,5 +219,9 @@ export async function runInfrastructureOnboarding(options, detection) {
166
219
  console.log(snippet);
167
220
  console.log(`\nNEXARCH_TOKEN is a service credential — create one in the workspace under`);
168
221
  console.log(`Discovery → Agents, then store it as a masked CI variable.`);
222
+ if (outstanding.length > 0) {
223
+ console.log(`\nThis run registered the repository only. Until the ${outstanding.length} ingest command${outstanding.length === 1 ? "" : "s"} above`);
224
+ console.log(`have run, the graph knows the repo exists but nothing it provisions.`);
225
+ }
169
226
  }
170
227
  export { detectInfrastructureProject };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexarch",
3
- "version": "0.12.9",
3
+ "version": "0.12.11",
4
4
  "description": "Your architecture workspace for AI delivery.",
5
5
  "keywords": [
6
6
  "nexarch",