aienvmp 0.1.40 → 0.1.42

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.
package/BUGFIXES.md CHANGED
@@ -34,6 +34,12 @@ Short record of bugs, fixes, and follow-up checks.
34
34
  - Fix: npm and Python security summaries now include fix versions and advisory references when scanners provide them.
35
35
  - Verification: parser tests cover npm remediation objects and pip-audit advisory ids; Windows and macOS tarball tests completed `sync --security`.
36
36
 
37
+ ### Advisory doctor behavior needed clearer verification
38
+
39
+ - Issue: `doctor` warnings can look like failures to AI/CI consumers even though local operation should stay non-blocking by default.
40
+ - Fix: `doctor --json` now exposes `exitBehavior`, and enforcement gate metadata explains when strict mode sets a failure exit code.
41
+ - Verification: Windows and macOS candidate smoke checks confirmed default `doctor --json` exits successfully while `doctor --strict policy --json` fails on matching policy warnings.
42
+
37
43
  ## Template
38
44
 
39
45
  ### Title
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.42
4
+
5
+ - Added explicit enforcement gate metadata so AI and CI consumers know local checks are warn-only unless `--strict` or `--ci` is requested.
6
+ - Added `doctor --json` exit behavior metadata to distinguish advisory warnings from strict failure conditions.
7
+ - Surfaced enforcement gate details in context, plan, and dashboard outputs.
8
+ - Extended the GitHub Action to write `.aienvmp/schema.json` and `.aienvmp/doctor.json` artifacts by default.
9
+ - Clarified README guidance for advisory-by-default behavior and opt-in strict/CI failure.
10
+ - Documented advisory doctor exit behavior and strict verification steps in troubleshooting and bugfix notes.
11
+
12
+ ## 0.1.41
13
+
14
+ - Added preflight contract metadata so AI and CI consumers can rely on stable entry fields while ignoring additive changes.
15
+ - Added `aienvmp schema --json` to print the stable AI-readable output contract without scanning a workspace.
16
+ - Exposed coordination summaries at the root of `context --json` and `handoff --json`, with a human-readable handoff section.
17
+ - Added light SBOM source, confidence, and limitation hints so agents know what the lightweight snapshot does and does not prove.
18
+ - Updated the README with the schema command and light SBOM verification boundary while keeping the quick-start compact.
19
+ - Added an AI Contract dashboard card so humans can review the same stable fields that agents consume.
20
+
3
21
  ## 0.1.40
4
22
 
5
23
  - Added dependency handoff summaries so the next AI receives dependency read-set and protocol guidance directly in `handoff`.
package/README.md CHANGED
@@ -16,6 +16,7 @@ It helps Codex, Claude, Gemini, and humans avoid silent runtime, package manager
16
16
  npx aienvmp sync
17
17
  npx aienvmp status
18
18
  npx aienvmp context --json
19
+ npx aienvmp schema --json
19
20
  ```
20
21
 
21
22
  Before environment changes:
@@ -27,6 +28,7 @@ npx aienvmp handoff --record --actor agent:codex
27
28
  ```
28
29
 
29
30
  Use `--dir <workspace>` when AI or CI runs outside the target project.
31
+ Warnings are advisory by default. Use `doctor --strict <scope>` only when you want CI-style failure.
30
32
 
31
33
  ## What It Creates
32
34
 
@@ -43,11 +45,14 @@ AIENV.md # Markdown env map for AI agents
43
45
  ## AI Contract
44
46
 
45
47
  - `status`, `context`, `plan`, and `handoff` share one preflight contract.
48
+ - `schema --json` prints the stable AI-readable output contract without scanning.
46
49
  - `status.json.nextAgent` tells the next AI what to read and whether to review first.
47
50
  - `dependencyReadSet` lists manifests and lockfiles before package or security changes.
48
51
  - `coordination.conflictTargets` shows where multiple agents are planning changes.
49
52
  - `handoff` carries dependency read-set and protocol guidance for the next AI.
50
- - Everything is advisory by default; strict failure is opt-in with `doctor --strict`.
53
+ - Light SBOM includes source/confidence hints; verify security claims with dedicated scanners.
54
+ - `enforcementProfile.gate` explains when checks warn, fail, and set exit codes.
55
+ - Everything is advisory by default; strict failure is opt-in with `doctor --strict` or `--ci`.
51
56
 
52
57
  ## Agent Files
53
58
 
@@ -65,6 +70,7 @@ npx aienvmp snippet gemini
65
70
  aienvmp sync # update env map, status, ledger, dashboard
66
71
  aienvmp status --write # refresh compact AI status
67
72
  aienvmp context --json # AI decision contract
73
+ aienvmp schema --json # stable output contract for AI/CI consumers
68
74
  aienvmp plan --write # read-only action plan
69
75
  aienvmp handoff --record # next-agent summary
70
76
  aienvmp intent # record planned env change
@@ -74,6 +80,8 @@ aienvmp doctor --strict security|policy|coordination|all
74
80
 
75
81
  ## CI
76
82
 
83
+ The GitHub Action writes status, schema, doctor, plan, and dashboard artifacts. `strict: "off"` reports warnings without failing the job.
84
+
77
85
  ```yaml
78
86
  - uses: soovwv/aienvmp@main
79
87
  with:
@@ -101,3 +101,33 @@ Then retry:
101
101
  ```bash
102
102
  npx aienvmp context
103
103
  ```
104
+
105
+ ## Doctor shows warnings but exits successfully
106
+
107
+ Symptom:
108
+
109
+ ```text
110
+ npx aienvmp doctor --json
111
+ # status: warning
112
+ # exit code: 0
113
+ ```
114
+
115
+ Cause:
116
+
117
+ - Local checks are advisory by default so shared machines are not blocked unexpectedly.
118
+
119
+ Fail CI explicitly:
120
+
121
+ ```bash
122
+ npx aienvmp doctor --strict policy
123
+ npx aienvmp doctor --strict security
124
+ npx aienvmp doctor --strict coordination
125
+ ```
126
+
127
+ Check the machine-readable rule:
128
+
129
+ ```bash
130
+ npx aienvmp doctor --json
131
+ ```
132
+
133
+ Read `exitBehavior` and `strict.gate` to see when a failure exit code will be set.
package/action.yml CHANGED
@@ -19,6 +19,14 @@ inputs:
19
19
  description: Write compact AI status artifact
20
20
  required: false
21
21
  default: "true"
22
+ write-schema:
23
+ description: Write stable AI output contract artifact
24
+ required: false
25
+ default: "true"
26
+ write-doctor-json:
27
+ description: Write doctor JSON with advisory/strict exit behavior
28
+ required: false
29
+ default: "true"
22
30
 
23
31
  runs:
24
32
  using: composite
@@ -41,6 +49,22 @@ runs:
41
49
  node "$GITHUB_ACTION_PATH/bin/aienvmp.js" status --dir "${{ inputs.directory }}" --write --quiet
42
50
  fi
43
51
 
52
+ - name: Write AI schema
53
+ shell: bash
54
+ run: |
55
+ if [ "${{ inputs.write-schema }}" = "true" ]; then
56
+ mkdir -p "${{ inputs.directory }}/.aienvmp"
57
+ node "$GITHUB_ACTION_PATH/bin/aienvmp.js" schema --json > "${{ inputs.directory }}/.aienvmp/schema.json"
58
+ fi
59
+
60
+ - name: Write doctor advisory JSON
61
+ shell: bash
62
+ run: |
63
+ if [ "${{ inputs.write-doctor-json }}" = "true" ]; then
64
+ mkdir -p "${{ inputs.directory }}/.aienvmp"
65
+ node "$GITHUB_ACTION_PATH/bin/aienvmp.js" doctor --dir "${{ inputs.directory }}" --json > "${{ inputs.directory }}/.aienvmp/doctor.json"
66
+ fi
67
+
44
68
  - name: Doctor
45
69
  shell: bash
46
70
  run: |
@@ -14,6 +14,8 @@ jobs:
14
14
  with:
15
15
  directory: "."
16
16
  write-plan: "true"
17
+ write-schema: "true"
18
+ write-doctor-json: "true"
17
19
  strict: "off" # security, policy, coordination, all, or off
18
20
 
19
21
  - uses: actions/upload-artifact@v4
@@ -24,6 +26,8 @@ jobs:
24
26
  AIENV.md
25
27
  .aienvmp/manifest.json
26
28
  .aienvmp/status.json
29
+ .aienvmp/schema.json
30
+ .aienvmp/doctor.json
27
31
  .aienvmp/plan.json
28
32
  .aienvmp/plan.md
29
33
  .aienvmp/dashboard.html
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aienvmp",
3
- "version": "0.1.40",
3
+ "version": "0.1.42",
4
4
  "description": "AI-first environment maps and lightweight runtime SBOMs for shared development machines.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -13,6 +13,7 @@ import { snippetWorkspace } from "./commands/snippet.js";
13
13
  import { handoffWorkspace } from "./commands/handoff.js";
14
14
  import { planWorkspace } from "./commands/plan.js";
15
15
  import { statusWorkspace } from "./commands/status.js";
16
+ import { schemaWorkspace } from "./commands/schema.js";
16
17
  import { readFileSync } from "node:fs";
17
18
 
18
19
  const commands = new Map([
@@ -30,7 +31,8 @@ const commands = new Map([
30
31
  ["snippet", snippetWorkspace],
31
32
  ["handoff", handoffWorkspace],
32
33
  ["plan", planWorkspace],
33
- ["status", statusWorkspace]
34
+ ["status", statusWorkspace],
35
+ ["schema", schemaWorkspace]
34
36
  ]);
35
37
 
36
38
  const version = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
@@ -107,6 +109,7 @@ Usage:
107
109
  aienvmp status [--dir .] [--json] [--write] [--quiet]
108
110
  aienvmp handoff [--dir .] [--json] [--record --actor agent:id]
109
111
  aienvmp plan [--dir .] [--json] [--write]
112
+ aienvmp schema [--json]
110
113
 
111
114
  Common:
112
115
  aienvmp sync update AIENV.md, manifest, status, ledger, intents, and dashboard
@@ -114,6 +117,7 @@ Common:
114
117
  aienvmp context print the AI preflight brief
115
118
  aienvmp handoff print the next-agent handoff summary
116
119
  aienvmp plan print a read-only AI environment action plan
120
+ aienvmp schema print the stable AI-readable output contract
117
121
  aienvmp snippet print an AGENTS.md pointer snippet
118
122
  aienvmp dash regenerate/open the lightweight dashboard
119
123
 
@@ -26,6 +26,7 @@ export async function contextWorkspace(args) {
26
26
  console.log(JSON.stringify({
27
27
  status: warnings.length ? "review-required" : "clear",
28
28
  preflight,
29
+ coordination: preflight.coordination,
29
30
  decision,
30
31
  enforcement: enforcementAdvice(warnings),
31
32
  recommendedActions: actions,
@@ -49,7 +50,7 @@ export async function contextWorkspace(args) {
49
50
  }, null, 2));
50
51
  return;
51
52
  }
52
- console.log(renderContext(manifest, timeline, warnings, intents, policy, actions));
53
+ console.log(renderContext({ ...manifest, preflight }, timeline, warnings, intents, policy, actions));
53
54
  }
54
55
 
55
56
  function lightSbomSummary(lightSbom = {}) {
@@ -73,6 +74,9 @@ function lightSbomSummary(lightSbom = {}) {
73
74
  guidance: "No lockfile policy detected."
74
75
  },
75
76
  dependencyChangeHints: (lightSbom.dependencyChangeHints || []).slice(0, 8),
77
+ source: lightSbom.source || {},
78
+ confidence: lightSbom.confidence || {},
79
+ limitations: lightSbom.limitations || [],
76
80
  aiUse: lightSbom.aiUse || {}
77
81
  };
78
82
  }
@@ -18,9 +18,18 @@ export async function doctorWorkspace(args) {
18
18
  const warnings = [...diagnose(manifest, { timeline, intents }), ...policyWarnings(manifest, policy)];
19
19
  const actions = recommendedActions(manifest, { warnings, intents });
20
20
  const strict = strictResult(warnings, args);
21
+ const exitBehavior = {
22
+ mode: strict.enabled ? "strict" : "advisory",
23
+ willSetFailureExitCode: strict.fail,
24
+ reason: strict.enabled
25
+ ? `strict scope ${strict.scope} is enabled`
26
+ : "strict mode is off; warnings are reported without failing local operation",
27
+ gate: strict.gate
28
+ };
21
29
  if (args.json) {
22
30
  console.log(JSON.stringify({
23
31
  status: strict.fail ? "fail" : warnings.length ? "warning" : "ok",
32
+ exitBehavior,
24
33
  trust: manifest.trust || {},
25
34
  policy,
26
35
  openIntentCount: intents.length,
@@ -62,6 +62,7 @@ export function buildHandoff(manifest, timeline = [], warnings = [], intents = [
62
62
  inventory: inventorySummary(manifest.inventory),
63
63
  security: securitySummary(manifest.security),
64
64
  dependencyHandoff: dependencyHandoffSummary(preflight),
65
+ coordination: preflight.coordination,
65
66
  policy: {
66
67
  node: policy.node || "not set",
67
68
  python: policy.python || "not set",
@@ -0,0 +1,16 @@
1
+ import { schemaContract } from "../contract.js";
2
+
3
+ export async function schemaWorkspace(args = {}) {
4
+ const schema = schemaContract();
5
+ if (args.json) {
6
+ console.log(JSON.stringify(schema, null, 2));
7
+ } else {
8
+ console.log("aienvmp contract");
9
+ console.log(`schema: ${schema.name} v${schema.schemaVersion}`);
10
+ console.log(`status: ${schema.outputs.status.command}`);
11
+ console.log(`context: ${schema.outputs.context.command}`);
12
+ console.log(`handoff: ${schema.outputs.handoff.command}`);
13
+ console.log(`rule: ${schema.compatibility.consumerRule}`);
14
+ }
15
+ return schema;
16
+ }
@@ -0,0 +1,42 @@
1
+ export function preflightContract() {
2
+ return {
3
+ name: "aienvmp-preflight",
4
+ version: 1,
5
+ stability: "additive",
6
+ requiredFields: ["schemaVersion", "state", "decision", "quickstart", "commands", "artifacts"],
7
+ aiEntryFields: ["state", "summary", "nextAgent", "coordination", "dependencyReadSet", "dependencyChangeProtocol"],
8
+ rule: "Consumers should ignore unknown fields and treat missing optional fields as unavailable."
9
+ };
10
+ }
11
+
12
+ export function schemaContract() {
13
+ return {
14
+ schemaVersion: 1,
15
+ name: "aienvmp-contract",
16
+ purpose: "Stable AI-readable contract for aienvmp outputs.",
17
+ outputs: {
18
+ status: {
19
+ file: ".aienvmp/status.json",
20
+ command: "aienvmp status --json",
21
+ contract: preflightContract()
22
+ },
23
+ context: {
24
+ command: "aienvmp context --json",
25
+ rootFields: ["status", "preflight", "coordination", "decision", "recommendedActions", "workspace", "dependencySnapshot", "lightSbom", "warnings"]
26
+ },
27
+ handoff: {
28
+ command: "aienvmp handoff --json",
29
+ rootFields: ["status", "decision", "preflight", "coordination", "dependencyHandoff", "openIntents", "warnings", "recommendedActions", "recentChanges"]
30
+ },
31
+ manifest: {
32
+ file: ".aienvmp/manifest.json",
33
+ rootFields: ["schemaVersion", "workspace", "runtimes", "packageManagers", "dependencySnapshot", "lightSbom", "security", "trust"]
34
+ }
35
+ },
36
+ compatibility: {
37
+ stability: "additive",
38
+ consumerRule: "Ignore unknown fields. Do not require optional fields unless listed in requiredFields.",
39
+ localBehavior: "read-only; this command does not scan, install, update, or lock anything."
40
+ }
41
+ };
42
+ }
@@ -79,6 +79,23 @@ export function buildLightSbom(snapshot = {}, security = {}) {
79
79
  schemaVersion: 1,
80
80
  mode: "light-sbom",
81
81
  note: "AI-ready package and vulnerability summary from read-only project files and optional scanners.",
82
+ source: {
83
+ dependencies: "project manifests",
84
+ lockfiles: "file presence only",
85
+ vulnerabilities: security.enabled ? "optional scanner summary" : "not scanned",
86
+ resolver: "not run"
87
+ },
88
+ confidence: {
89
+ directDependencies: "high",
90
+ lockfileManager: snapshot.lockfiles?.length ? "medium" : "none",
91
+ transitiveDependencies: "not-resolved",
92
+ vulnerabilities: security.enabled ? "scanner-provided" : "not-scanned"
93
+ },
94
+ limitations: [
95
+ "Does not install packages.",
96
+ "Does not resolve full transitive dependency graphs.",
97
+ "Does not replace CycloneDX, SPDX, Syft, Trivy, npm audit, or pip-audit outputs."
98
+ ],
82
99
  summary: {
83
100
  ecosystems: countBy(packages, "ecosystem"),
84
101
  managers: countBy(packages, "manager"),
@@ -96,7 +113,8 @@ export function buildLightSbom(snapshot = {}, security = {}) {
96
113
  aiUse: {
97
114
  beforeDependencyChanges: "Read lightSbom.summary and lightSbom.topRisk before changing dependencies.",
98
115
  securityMode: security.enabled ? "scanner-summary" : "scanner-off",
99
- dependencySource: "project manifests only; no install or resolver is run"
116
+ dependencySource: "project manifests only; no install or resolver is run",
117
+ trustRule: "Use lightSbom as a fast AI planning map; verify with dedicated scanners before security claims."
100
118
  }
101
119
  };
102
120
  }
@@ -3,12 +3,14 @@ const STRICT_SCOPES = ["security", "policy", "coordination", "all"];
3
3
  export function strictResult(warnings = [], args = {}) {
4
4
  const scope = normalizeStrictScope(args.strict || (args.ci ? "all" : ""));
5
5
  const matchedWarnings = scope ? warnings.filter((warning) => warningMatchesScope(warning, scope)) : [];
6
+ const gate = enforcementGate(scope);
6
7
  return {
7
8
  enabled: Boolean(scope),
8
9
  scope: scope || "off",
9
10
  fail: matchedWarnings.length > 0,
10
11
  matchedWarningCodes: matchedWarnings.map((warning) => warning.code),
11
- availableScopes: STRICT_SCOPES
12
+ availableScopes: STRICT_SCOPES,
13
+ gate
12
14
  };
13
15
  }
14
16
 
@@ -28,6 +30,7 @@ export function enforcementAdvice(warnings = []) {
28
30
  mode: "advisory-by-default",
29
31
  localBehavior: "non-blocking",
30
32
  ciBehavior: "strict-only-when-requested",
33
+ gate: enforcementGate(""),
31
34
  suggestedStrictScopes,
32
35
  scopes: scopeResults,
33
36
  recommendedCommand: suggestedStrictScopes.length
@@ -37,6 +40,18 @@ export function enforcementAdvice(warnings = []) {
37
40
  };
38
41
  }
39
42
 
43
+ export function enforcementGate(scope = "") {
44
+ const strictScope = normalizeStrictScope(scope);
45
+ return {
46
+ defaultMode: "advisory",
47
+ strictMode: strictScope || "off",
48
+ localDefault: "warn-only",
49
+ failCondition: strictScope ? `matching warnings in ${strictScope}` : "never in default mode",
50
+ exitCode: strictScope ? "1 when matching warnings exist" : "0 unless the command itself errors",
51
+ rule: "Do not block local or shared machine operation unless --strict or --ci is explicitly requested."
52
+ };
53
+ }
54
+
40
55
  function normalizeStrictScope(value) {
41
56
  if (value === true) return "all";
42
57
  const scope = String(value || "").trim().toLowerCase();
package/src/preflight.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { recommendedActions } from "./actions.js";
2
2
  import { aiDecision } from "./decision.js";
3
- import { enforcementAdvice } from "./enforcement.js";
3
+ import { enforcementAdvice, enforcementGate } from "./enforcement.js";
4
+ import { preflightContract } from "./contract.js";
4
5
 
5
6
  export function buildPreflight(manifest = {}, warnings = [], intents = []) {
6
7
  const decision = aiDecision(warnings, intents);
@@ -14,6 +15,7 @@ export function buildPreflight(manifest = {}, warnings = [], intents = []) {
14
15
  const coordination = coordinationSummary(intents);
15
16
  return {
16
17
  schemaVersion: 1,
18
+ contract: preflightContract(),
17
19
  state,
18
20
  summary: state === "clear"
19
21
  ? "Project-local work can continue. Record intent before environment changes."
@@ -25,6 +27,7 @@ export function buildPreflight(manifest = {}, warnings = [], intents = []) {
25
27
  localOperation: "non-blocking",
26
28
  strictMode: "optional",
27
29
  strictUse: "CI or explicit human-requested checks only",
30
+ gate: enforcementGate(""),
28
31
  reason: "Avoid disrupting shared servers or developer machines while still making drift visible.",
29
32
  recommendedStrictCommand: enforcement.recommendedCommand,
30
33
  strictCommands: [
package/src/render.js CHANGED
@@ -170,6 +170,9 @@ export function renderContext(manifest, timeline = [], warnings = [], intents =
170
170
  "Recommended actions:",
171
171
  ...(recommendedActions.length ? recommendedActions.map((item) => `- [${item.priority}] ${item.summary}${item.command ? ` (${item.command})` : ""}`) : ["- none"]),
172
172
  "",
173
+ "Enforcement gate:",
174
+ ...enforcementGateLines(manifest.preflight?.enforcementProfile?.gate),
175
+ "",
173
176
  "Open intents:",
174
177
  ...(intents.length ? intents.slice(-5).reverse().map((i) => `- ${i.actor}: ${i.action}`) : ["- none"]),
175
178
  "",
@@ -199,6 +202,9 @@ export function renderHandoff(handoff) {
199
202
  "Open intents:",
200
203
  ...(handoff.openIntents.length ? handoff.openIntents.map((i) => `- ${i.actor}: ${i.action}${i.target ? ` (${i.target})` : ""}`) : ["- none"]),
201
204
  "",
205
+ "Coordination:",
206
+ ...coordinationHandoffLines(handoff.coordination),
207
+ "",
202
208
  "Warnings:",
203
209
  ...(handoff.warnings.length ? handoff.warnings.map((w) => `- ${w.message}`) : ["- none"]),
204
210
  "",
@@ -220,6 +226,12 @@ export function renderHandoff(handoff) {
220
226
  return lines.join("\n");
221
227
  }
222
228
 
229
+ function coordinationHandoffLines(coordination = {}) {
230
+ const conflicts = coordination.conflictTargets || [];
231
+ if (conflicts.length) return [`- Conflicts: ${conflicts.join(", ")}`, `- Next: ${coordination.next || "review open intents"}`];
232
+ return [`- Open intents: ${coordination.openIntentCount || 0}`, `- Next: ${coordination.next || "no open environment intents"}`];
233
+ }
234
+
223
235
  function dependencyHandoffLines(dependencyHandoff = {}) {
224
236
  const readSet = dependencyHandoff.readSet || [];
225
237
  const protocol = dependencyHandoff.protocol || {};
@@ -258,6 +270,9 @@ export function renderPlan(plan) {
258
270
  "Review gates:",
259
271
  ...plan.reviewGates.map((item) => `- ${item}`),
260
272
  "",
273
+ "Enforcement gate:",
274
+ ...enforcementGateLines(plan.preflight?.enforcementProfile?.gate),
275
+ "",
261
276
  "Dependency protocol:",
262
277
  ...dependencyProtocolPlanLines(plan.preflight?.dependencyChangeProtocol),
263
278
  "",
@@ -285,6 +300,15 @@ function dependencyProtocolPlanLines(protocol = {}) {
285
300
  ];
286
301
  }
287
302
 
303
+ function enforcementGateLines(gate = {}) {
304
+ return [
305
+ `- Default: ${gate.defaultMode || "advisory"} (${gate.localDefault || "warn-only"})`,
306
+ `- Strict: ${gate.strictMode || "off"}`,
307
+ `- Fail condition: ${gate.failCondition || "never in default mode"}`,
308
+ `- Exit code: ${gate.exitCode || "0 unless the command itself errors"}`
309
+ ];
310
+ }
311
+
288
312
  function environmentLines(item) {
289
313
  return [
290
314
  `- ${item.category}: ${item.summary}`,
@@ -405,7 +429,10 @@ const ciHasFailure=ciReadiness.some(s=>s.status==='fail');
405
429
  const ciReadinessHtml=ciReadiness.length?'<table>'+ciReadiness.map(s=>\`<tr><th>\${esc(s.scope)}</th><td><code>\${esc(s.status)}</code>\${s.matchedWarningCodes?.length?\` \${esc(s.matchedWarningCodes.join(', '))}\`:''}</td></tr>\`).join('')+'</table>':'<div class="okline">Run <code>aienvmp doctor --strict security|policy|coordination|all</code> to choose CI enforcement scope.</div>';
406
430
  const enforcementProfile=manifest.preflight?.enforcementProfile||{};
407
431
  const strictCommands=enforcementProfile.strictCommands||[];
408
- const enforcementHtml=\`<table><tr><th>Default</th><td><code>\${esc(enforcementProfile.defaultMode||'advisory')}</code></td></tr><tr><th>Local</th><td>\${esc(enforcementProfile.localOperation||'non-blocking')}</td></tr><tr><th>Strict</th><td>\${esc(enforcementProfile.strictUse||'CI or explicit checks only')}</td></tr><tr><th>Recommended</th><td><code>\${esc(enforcementProfile.recommendedStrictCommand||'aienvmp doctor --strict all')}</code></td></tr></table><div class="timeline">\${strictCommands.slice(0,4).map(cmd=>\`<div class="event"><time>CI</time><div><code>\${esc(cmd)}</code></div></div>\`).join('')}</div><div class="path">\${esc(enforcementProfile.reason||'Warnings stay advisory unless strict mode is requested.')}</div>\`;
432
+ const gate=enforcementProfile.gate||{};
433
+ const enforcementHtml=\`<table><tr><th>Default</th><td><code>\${esc(gate.defaultMode||enforcementProfile.defaultMode||'advisory')}</code> \${esc(gate.localDefault||'warn-only')}</td></tr><tr><th>Strict</th><td><code>\${esc(gate.strictMode||'off')}</code></td></tr><tr><th>Fail</th><td>\${esc(gate.failCondition||'never in default mode')}</td></tr><tr><th>Exit</th><td>\${esc(gate.exitCode||'0 unless the command itself errors')}</td></tr><tr><th>Recommended</th><td><code>\${esc(enforcementProfile.recommendedStrictCommand||'aienvmp doctor --strict all')}</code></td></tr></table><div class="timeline">\${strictCommands.slice(0,4).map(cmd=>\`<div class="event"><time>CI</time><div><code>\${esc(cmd)}</code></div></div>\`).join('')}</div><div class="path">\${esc(gate.rule||enforcementProfile.reason||'Warnings stay advisory unless strict mode is requested.')}</div>\`;
434
+ const contract=manifest.preflight?.contract||{};
435
+ const contractHtml=contract.name?\`<table><tr><th>Name</th><td><code>\${esc(contract.name)}</code></td></tr><tr><th>Version</th><td><code>\${esc(contract.version||1)}</code></td></tr><tr><th>Stability</th><td><code>\${esc(contract.stability||'additive')}</code></td></tr><tr><th>AI fields</th><td>\${esc((contract.aiEntryFields||[]).join(', ')||'none')}</td></tr></table><div class="path">\${esc(contract.rule||'Ignore unknown fields.')}</div>\`:'<div class="okline">Run <code>aienvmp status --write</code> to include AI contract metadata.</div>';
409
436
  const intentTargets=manifest.preflight?.intentTargets||[];
410
437
  const intentTargetsHtml=intentTargets.length?'<div class="timeline">'+intentTargets.slice(0,5).map(t=>\`<div class="event"><time>\${esc(t.target)}</time><div><b>\${esc(t.target)}</b> \${esc(t.reason||'Record this target before environment changes.')}\${t.command?\`<div class="path">\${esc(t.command)}</div>\`:''}</div></div>\`).join('')+'</div>':'<div class="okline">No specific target recommendation. Use <code>aienvmp intent --actor agent:id --action planned-change</code>.</div>';
411
438
  const dependencyReadSet=manifest.preflight?.dependencyReadSet||[];
@@ -462,6 +489,8 @@ document.getElementById('app').innerHTML=\`
462
489
  <div style="height:14px"></div>
463
490
  \${card('AI Intent Targets','<span class="pill">'+intentTargets.length+' targets</span>',intentTargetsHtml)}
464
491
  <div style="height:14px"></div>
492
+ \${card('AI Contract','<span class="pill">'+(contract.stability||'additive')+'</span>',contractHtml)}
493
+ <div style="height:14px"></div>
465
494
  \${card('Dependency Read Set','<span class="pill">'+dependencyReadSet.length+' files</span>',dependencyReadSetHtml)}
466
495
  <div style="height:14px"></div>
467
496
  \${card('Dependency Protocol','<span class="pill">'+(dependencyProtocol.mode||'advisory')+'</span>',dependencyProtocolHtml)}
@@ -569,6 +598,10 @@ function lightSbomLines(lightSbom = {}) {
569
598
  `- Transitive or unmatched vulnerable packages: ${summary.transitiveOrUnmatchedVulnerablePackages || 0}`,
570
599
  `- Lockfiles: ${(summary.lockfiles || []).map((item) => item.file).join(", ") || "none"}`
571
600
  ];
601
+ if (lightSbom.source || lightSbom.confidence) {
602
+ lines.push(`- Source: ${lightSbom.source?.dependencies || "project manifests"}; vulnerabilities: ${lightSbom.source?.vulnerabilities || "not scanned"}`);
603
+ lines.push(`- Confidence: direct ${lightSbom.confidence?.directDependencies || "unknown"}; transitive ${lightSbom.confidence?.transitiveDependencies || "unknown"}`);
604
+ }
572
605
  if (lightSbom.packageManagerPolicy) {
573
606
  lines.push(`- Package manager policy: ${lightSbom.packageManagerPolicy.status}`);
574
607
  lines.push(`- Package manager guidance: ${lightSbom.packageManagerPolicy.guidance}`);