@nimiplatform/nimi-coding 0.1.0
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/LICENSE +21 -0
- package/README.md +348 -0
- package/adapters/README.md +25 -0
- package/adapters/claude/README.md +89 -0
- package/adapters/claude/profile.yaml +70 -0
- package/adapters/codex/README.md +53 -0
- package/adapters/codex/profile.yaml +78 -0
- package/adapters/oh-my-codex/README.md +185 -0
- package/adapters/oh-my-codex/profile.yaml +46 -0
- package/bin/nimicoding.mjs +6 -0
- package/cli/commands/admit-high-risk-decision.mjs +108 -0
- package/cli/commands/audit-sweep.mjs +341 -0
- package/cli/commands/blueprint-audit.mjs +91 -0
- package/cli/commands/clear.mjs +168 -0
- package/cli/commands/closeout.mjs +183 -0
- package/cli/commands/decide-high-risk-execution.mjs +124 -0
- package/cli/commands/doctor.mjs +53 -0
- package/cli/commands/generate-spec-derived-docs.mjs +131 -0
- package/cli/commands/handoff.mjs +123 -0
- package/cli/commands/ingest-high-risk-execution.mjs +95 -0
- package/cli/commands/review-high-risk-execution.mjs +95 -0
- package/cli/commands/start.mjs +717 -0
- package/cli/commands/topic-formatters.mjs +382 -0
- package/cli/commands/topic-goal.mjs +33 -0
- package/cli/commands/topic-options-shared.mjs +27 -0
- package/cli/commands/topic-options-workflow.mjs +767 -0
- package/cli/commands/topic-options.mjs +626 -0
- package/cli/commands/topic-runner.mjs +169 -0
- package/cli/commands/topic.mjs +795 -0
- package/cli/commands/validate-acceptance.mjs +5 -0
- package/cli/commands/validate-ai-governance.mjs +214 -0
- package/cli/commands/validate-execution-packet.mjs +5 -0
- package/cli/commands/validate-orchestration-state.mjs +5 -0
- package/cli/commands/validate-prompt.mjs +5 -0
- package/cli/commands/validate-spec-audit.mjs +27 -0
- package/cli/commands/validate-spec-governance.mjs +124 -0
- package/cli/commands/validate-spec-tree.mjs +27 -0
- package/cli/commands/validate-worker-output.mjs +5 -0
- package/cli/constants.mjs +489 -0
- package/cli/help.mjs +134 -0
- package/cli/index.mjs +103 -0
- package/cli/lib/adapter-profiles.mjs +403 -0
- package/cli/lib/audit-execution.mjs +52 -0
- package/cli/lib/audit-sweep-runtime/admissions.mjs +381 -0
- package/cli/lib/audit-sweep-runtime/audit-validity.mjs +333 -0
- package/cli/lib/audit-sweep-runtime/chunks.mjs +697 -0
- package/cli/lib/audit-sweep-runtime/closeout.mjs +144 -0
- package/cli/lib/audit-sweep-runtime/codex-auditor-evidence.mjs +639 -0
- package/cli/lib/audit-sweep-runtime/codex-auditor.mjs +515 -0
- package/cli/lib/audit-sweep-runtime/common.mjs +329 -0
- package/cli/lib/audit-sweep-runtime/coverage-quality.mjs +172 -0
- package/cli/lib/audit-sweep-runtime/evidence-assignment.mjs +152 -0
- package/cli/lib/audit-sweep-runtime/format.mjs +57 -0
- package/cli/lib/audit-sweep-runtime/ingest.mjs +486 -0
- package/cli/lib/audit-sweep-runtime/inventory-spec-chunks.mjs +198 -0
- package/cli/lib/audit-sweep-runtime/inventory.mjs +728 -0
- package/cli/lib/audit-sweep-runtime/ledger.mjs +315 -0
- package/cli/lib/audit-sweep-runtime/p0p1-profile.mjs +101 -0
- package/cli/lib/audit-sweep-runtime/remediation.mjs +349 -0
- package/cli/lib/audit-sweep-runtime/rerun.mjs +129 -0
- package/cli/lib/audit-sweep-runtime/risk-budget.mjs +300 -0
- package/cli/lib/audit-sweep-runtime/status.mjs +62 -0
- package/cli/lib/audit-sweep-runtime/validators-ledger.mjs +215 -0
- package/cli/lib/audit-sweep-runtime/validators.mjs +758 -0
- package/cli/lib/audit-sweep.mjs +18 -0
- package/cli/lib/authority-convergence.mjs +309 -0
- package/cli/lib/blueprint-audit.mjs +370 -0
- package/cli/lib/bootstrap.mjs +228 -0
- package/cli/lib/closeout.mjs +623 -0
- package/cli/lib/codex-sdk-runner.mjs +76 -0
- package/cli/lib/contracts.mjs +180 -0
- package/cli/lib/doctor.mjs +18 -0
- package/cli/lib/entrypoints.mjs +274 -0
- package/cli/lib/external-execution.mjs +101 -0
- package/cli/lib/fs-helpers.mjs +33 -0
- package/cli/lib/handoff.mjs +785 -0
- package/cli/lib/high-risk-admission.mjs +442 -0
- package/cli/lib/high-risk-decision.mjs +324 -0
- package/cli/lib/high-risk-ingest.mjs +317 -0
- package/cli/lib/high-risk-review.mjs +263 -0
- package/cli/lib/internal/contracts-loaders.mjs +132 -0
- package/cli/lib/internal/contracts-parse-high-risk.mjs +131 -0
- package/cli/lib/internal/contracts-parse.mjs +457 -0
- package/cli/lib/internal/contracts-validators.mjs +398 -0
- package/cli/lib/internal/doctor-bootstrap-surface.mjs +359 -0
- package/cli/lib/internal/doctor-delegated-surface.mjs +256 -0
- package/cli/lib/internal/doctor-finalize.mjs +385 -0
- package/cli/lib/internal/doctor-format.mjs +286 -0
- package/cli/lib/internal/doctor-inspectors.mjs +294 -0
- package/cli/lib/internal/doctor-state.mjs +205 -0
- package/cli/lib/internal/governance/ai/ai-context-budget-core.mjs +315 -0
- package/cli/lib/internal/governance/ai/ai-structure-budget-core.mjs +358 -0
- package/cli/lib/internal/governance/ai/check-agents-freshness.mjs +155 -0
- package/cli/lib/internal/governance/ai/check-high-risk-doc-metadata-core.mjs +173 -0
- package/cli/lib/internal/governance/config.mjs +150 -0
- package/cli/lib/internal/governance/runner.mjs +35 -0
- package/cli/lib/internal/governance/shared/read-yaml-with-fragments.mjs +49 -0
- package/cli/lib/internal/validators-artifacts.mjs +515 -0
- package/cli/lib/internal/validators-shared.mjs +28 -0
- package/cli/lib/internal/validators-spec-helpers.mjs +186 -0
- package/cli/lib/internal/validators-spec.mjs +410 -0
- package/cli/lib/shared.mjs +83 -0
- package/cli/lib/topic-draft-packets.mjs +48 -0
- package/cli/lib/topic-goal.mjs +361 -0
- package/cli/lib/topic-runner.mjs +772 -0
- package/cli/lib/topic.mjs +93 -0
- package/cli/lib/ui.mjs +178 -0
- package/cli/lib/validators.mjs +78 -0
- package/cli/lib/value-helpers.mjs +24 -0
- package/cli/lib/yaml-helpers.mjs +133 -0
- package/cli/nimicoding.mjs +1 -0
- package/cli/seeds/bootstrap.mjs +47 -0
- package/config/audit-execution-artifacts.yaml +20 -0
- package/config/bootstrap.yaml +6 -0
- package/config/external-execution-artifacts.yaml +16 -0
- package/config/host-adapter.yaml +30 -0
- package/config/host-profile.yaml +29 -0
- package/config/installer-evidence.yaml +31 -0
- package/config/skill-installer.yaml +23 -0
- package/config/skill-manifest.yaml +46 -0
- package/config/skills.yaml +30 -0
- package/config/spec-generation-inputs.yaml +25 -0
- package/contracts/acceptance.schema.yaml +16 -0
- package/contracts/admission-checklist.schema.yaml +15 -0
- package/contracts/audit-chunk.schema.yaml +110 -0
- package/contracts/audit-closeout.schema.yaml +51 -0
- package/contracts/audit-finding.schema.yaml +61 -0
- package/contracts/audit-ledger.schema.yaml +138 -0
- package/contracts/audit-plan.schema.yaml +123 -0
- package/contracts/audit-remediation-map.schema.yaml +51 -0
- package/contracts/audit-rerun.schema.yaml +31 -0
- package/contracts/audit-sweep-result.yaml +49 -0
- package/contracts/authority-convergence-audit.schema.yaml +19 -0
- package/contracts/closeout.schema.yaml +25 -0
- package/contracts/decision-review.schema.yaml +16 -0
- package/contracts/doc-spec-audit-result.yaml +19 -0
- package/contracts/execution-packet.schema.yaml +49 -0
- package/contracts/external-host-compatibility.yaml +22 -0
- package/contracts/forbidden-shortcuts.catalog.yaml +23 -0
- package/contracts/high-risk-admission.schema.yaml +23 -0
- package/contracts/high-risk-execution-result.yaml +20 -0
- package/contracts/orchestration-state.schema.yaml +41 -0
- package/contracts/overflow-continuation.schema.yaml +12 -0
- package/contracts/packet.schema.yaml +30 -0
- package/contracts/pending-note.schema.yaml +17 -0
- package/contracts/prompt.schema.yaml +12 -0
- package/contracts/remediation.schema.yaml +16 -0
- package/contracts/result.schema.yaml +24 -0
- package/contracts/spec-generation-audit.schema.yaml +31 -0
- package/contracts/spec-generation-inputs.schema.yaml +39 -0
- package/contracts/spec-reconstruction-result.yaml +37 -0
- package/contracts/topic-goal.schema.yaml +78 -0
- package/contracts/topic-run-ledger.schema.yaml +72 -0
- package/contracts/topic-step-decision.schema.yaml +45 -0
- package/contracts/topic.schema.yaml +65 -0
- package/contracts/true-close.schema.yaml +15 -0
- package/contracts/wave.schema.yaml +29 -0
- package/contracts/worker-output.schema.yaml +15 -0
- package/methodology/audit-sweep-p0p1-recall.yaml +45 -0
- package/methodology/authority-convergence-policy.yaml +42 -0
- package/methodology/core.yaml +25 -0
- package/methodology/four-closure-policy.yaml +28 -0
- package/methodology/overflow-continuation-policy.yaml +14 -0
- package/methodology/role-separation-policy.yaml +28 -0
- package/methodology/skill-exchange-projection.yaml +114 -0
- package/methodology/skill-handoff.yaml +34 -0
- package/methodology/skill-installer-result.yaml +27 -0
- package/methodology/skill-installer-summary-projection.yaml +181 -0
- package/methodology/skill-runtime.yaml +23 -0
- package/methodology/spec-reconstruction.yaml +63 -0
- package/methodology/spec-target-truth-profile.yaml +53 -0
- package/methodology/topic-lifecycle-report.yaml +144 -0
- package/methodology/topic-lifecycle.yaml +37 -0
- package/methodology/topic-naming-ontology.yaml +21 -0
- package/methodology/topic-ontology.yaml +38 -0
- package/methodology/topic-validation-policy.yaml +9 -0
- package/methodology/wave-dag-policy.yaml +14 -0
- package/package.json +50 -0
- package/spec/_meta/command-gating-matrix.yaml +110 -0
- package/spec/_meta/generate-drift-migration-checklist.yaml +155 -0
- package/spec/_meta/governance-routing-cutover-checklist.yaml +35 -0
- package/spec/_meta/phase2-impacted-surface-matrix.yaml +44 -0
- package/spec/_meta/spec-authority-cutover-readiness.yaml +104 -0
- package/spec/_meta/spec-tree-model.yaml +72 -0
- package/spec/bootstrap-state.yaml +99 -0
- package/spec/product-scope.yaml +56 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { readdir } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { readTextIfFile } from "./fs-helpers.mjs";
|
|
5
|
+
import { parseYamlText } from "./yaml-helpers.mjs";
|
|
6
|
+
|
|
7
|
+
function toPortableRelativePath(filePath) {
|
|
8
|
+
return filePath.split(path.sep).join("/");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function parsePacketDraft(text) {
|
|
12
|
+
if (!text) return null;
|
|
13
|
+
if (text.startsWith("---\n")) {
|
|
14
|
+
const closing = text.indexOf("\n---\n", 4);
|
|
15
|
+
if (closing !== -1) {
|
|
16
|
+
return parseYamlText(text.slice(4, closing));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return parseYamlText(text);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function findUniqueFreezableDraftPacket(projectRoot, loaded, wave, authority) {
|
|
23
|
+
const matches = [];
|
|
24
|
+
for (const entry of await readdir(loaded.topicDir, { withFileTypes: true })) {
|
|
25
|
+
if (!entry.isFile() || !/^draft.*\.(ya?ml|md)$/u.test(entry.name.toLowerCase())) continue;
|
|
26
|
+
const draftPath = path.join(loaded.topicDir, entry.name);
|
|
27
|
+
const packet = parsePacketDraft(await readTextIfFile(draftPath) ?? "");
|
|
28
|
+
if (!packet || typeof packet !== "object") continue;
|
|
29
|
+
if (packet.topic_id !== loaded.topicId || packet.wave_id !== wave.wave_id) continue;
|
|
30
|
+
if (!authority.packetFreezeAllowedStatuses.includes(packet.status)) continue;
|
|
31
|
+
if (authority.packetRequiredFields.some((field) => {
|
|
32
|
+
const value = packet[field];
|
|
33
|
+
return value == null || value === "" || (Array.isArray(value) && value.length === 0);
|
|
34
|
+
})) continue;
|
|
35
|
+
matches.push({
|
|
36
|
+
packet,
|
|
37
|
+
draftRef: toPortableRelativePath(path.relative(projectRoot, draftPath)),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return matches.length === 1
|
|
41
|
+
? { ok: true, ...matches[0] }
|
|
42
|
+
: {
|
|
43
|
+
ok: false,
|
|
44
|
+
reasonCode: matches.length === 0
|
|
45
|
+
? "admitted_wave_requires_packet"
|
|
46
|
+
: "admitted_wave_has_ambiguous_draft_packets",
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readdir } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
import { pathExists, readTextIfFile } from "./fs-helpers.mjs";
|
|
7
|
+
import { loadTopicReport, validateTopicGraph, validateTopicRoot } from "./topic.mjs";
|
|
8
|
+
import { parseYamlText } from "./yaml-helpers.mjs";
|
|
9
|
+
import { loadGovernanceConfig } from "./internal/governance/config.mjs";
|
|
10
|
+
|
|
11
|
+
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
12
|
+
const TOPIC_GOAL_CONTRACT_REF = "nimi-coding/contracts/topic-goal.schema.yaml";
|
|
13
|
+
const HOST_TOPIC_GOAL_CONTRACT_REF = ".nimi/contracts/topic-goal.schema.yaml";
|
|
14
|
+
const FORBIDDEN_SHORTCUTS_REF = "nimi-coding/contracts/forbidden-shortcuts.catalog.yaml";
|
|
15
|
+
const HOST_FORBIDDEN_SHORTCUTS_REF = ".nimi/contracts/forbidden-shortcuts.catalog.yaml";
|
|
16
|
+
const GOAL_COMMAND_MAX_CHARS = 1500;
|
|
17
|
+
|
|
18
|
+
const REQUIRED_ARTIFACTS = [
|
|
19
|
+
"topic.yaml",
|
|
20
|
+
"design.md",
|
|
21
|
+
"waves.md",
|
|
22
|
+
"candidate-wave-plan.md",
|
|
23
|
+
"admission-checklists.md",
|
|
24
|
+
"preflight.md",
|
|
25
|
+
"implementation-doctrine.md",
|
|
26
|
+
"manager-session-protocol.md",
|
|
27
|
+
"manager-prompts.md",
|
|
28
|
+
"closeout.md",
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const REQUIRED_STOP_KEYS = [
|
|
32
|
+
"mvp_subset_contract",
|
|
33
|
+
"legacy_alias",
|
|
34
|
+
"compat_shim",
|
|
35
|
+
"dual_read",
|
|
36
|
+
"dual_write",
|
|
37
|
+
"placeholder_success",
|
|
38
|
+
"happy_path_only_closure",
|
|
39
|
+
"time_phased_layering",
|
|
40
|
+
"app_local_shadow_truth",
|
|
41
|
+
"silent_owner_cut_reopen",
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const EXECUTABLE_WAVE_STATES = new Set(["implementation_admitted", "implementation_active"]);
|
|
45
|
+
const TERMINAL_WAVE_STATES = new Set(["closed", "retired", "superseded"]);
|
|
46
|
+
const WAVE_ID_PATTERN = /^wave-[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
47
|
+
|
|
48
|
+
const CHECK_REASON = {
|
|
49
|
+
topic_validate_passed: "topic_validate_failed",
|
|
50
|
+
topic_graph_validate_passed: "topic_graph_validate_failed",
|
|
51
|
+
topic_lifecycle_ongoing: "topic_not_ongoing",
|
|
52
|
+
true_close_not_started: "true_close_not_started_required",
|
|
53
|
+
strict_policy_active: "strict_policy_ignored",
|
|
54
|
+
parallel_truth_forbidden: "parallel_truth_not_forbidden",
|
|
55
|
+
profile_resolves: "unknown_profile",
|
|
56
|
+
selected_target_wave_resolves: "selected_target_not_wave",
|
|
57
|
+
selected_wave_single_source: "selected_wave_mismatch",
|
|
58
|
+
wave_option_matches_selected: "wave_override_forbidden",
|
|
59
|
+
selected_wave_executable: "selected_wave_not_executable",
|
|
60
|
+
selected_wave_dependencies_terminal: "dependency_not_terminal",
|
|
61
|
+
selected_wave_goal_present: "missing_primary_closure_goal",
|
|
62
|
+
forbidden_shortcuts_present: "forbidden_shortcuts_incomplete",
|
|
63
|
+
forbidden_shortcuts_catalog_aligned: "forbidden_shortcuts_catalog_drift",
|
|
64
|
+
required_artifacts_present: "required_artifact_missing",
|
|
65
|
+
no_unresolved_placeholders: "unresolved_placeholder",
|
|
66
|
+
stop_line_declared: "stop_line_missing",
|
|
67
|
+
human_gates_declared: "human_gates_missing",
|
|
68
|
+
validation_commands_declared: "validation_commands_missing",
|
|
69
|
+
closeout_criteria_declared: "closeout_criteria_missing",
|
|
70
|
+
authority_owner_declared: "authority_owner_missing",
|
|
71
|
+
work_type_declared: "work_type_missing",
|
|
72
|
+
authority_change_admitted: "authority_alignment_missing",
|
|
73
|
+
host_projection_aligned: "host_projection_drift",
|
|
74
|
+
goal_size_within_limit: "goal_too_large",
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
function toPortableRelativePath(filePath) {
|
|
78
|
+
return filePath.split(path.sep).join("/");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function projectRef(projectRoot, absolutePath) {
|
|
82
|
+
return toPortableRelativePath(path.relative(projectRoot, absolutePath));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function packagePath(relativePath) {
|
|
86
|
+
return path.join(PACKAGE_ROOT, relativePath.replace(/^nimi-coding\//, ""));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function check(id, ok, message, extra = {}) {
|
|
90
|
+
return {
|
|
91
|
+
id,
|
|
92
|
+
status: ok ? "pass" : "fail",
|
|
93
|
+
severity: ok ? "info" : "blocking",
|
|
94
|
+
message,
|
|
95
|
+
...extra,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function sectionText(markdown, sectionTitle) {
|
|
100
|
+
const escaped = sectionTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
101
|
+
const match = markdown.match(new RegExp(`^##\\s+${escaped}\\s*$([\\s\\S]*?)(?=^##\\s+|$(?![\\s\\S]))`, "im"));
|
|
102
|
+
return match ? match[1].trim() : "";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function hasSectionBullet(markdown, sectionTitle) {
|
|
106
|
+
return /^[-*]\s+\S/m.test(sectionText(markdown, sectionTitle));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function hasUnresolvedPlaceholder(text) {
|
|
110
|
+
return /(?<![A-Za-z-])(?:TODO|TBD|FIXME|XXX)(?![A-Za-z-])/i.test(text)
|
|
111
|
+
|| /<\s*(?:placeholder|place-holder|fill-me|missing)\s*>/i.test(text)
|
|
112
|
+
|| /\?\?\?/.test(text);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function parseValidationCommands(artifactTexts) {
|
|
116
|
+
const commands = [];
|
|
117
|
+
const commandPattern = /\b(?:pnpm|npm|npx|node|go|cargo)\s+[^\n`]+/g;
|
|
118
|
+
for (const { text } of artifactTexts) {
|
|
119
|
+
for (const match of text.matchAll(commandPattern)) {
|
|
120
|
+
const command = match[0].replace(/[.)\]]+$/u, "").trim();
|
|
121
|
+
if (command.includes("<") || command.includes("topic goal")) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (command.length > 0 && !commands.includes(command)) {
|
|
125
|
+
commands.push(command);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return commands.map((command) => ({
|
|
130
|
+
command,
|
|
131
|
+
cwd: ".",
|
|
132
|
+
profile: null,
|
|
133
|
+
scope: command.includes("topic validate graph")
|
|
134
|
+
? "graph"
|
|
135
|
+
: command.includes("topic validate")
|
|
136
|
+
? "topic"
|
|
137
|
+
: command.includes("test")
|
|
138
|
+
? "test"
|
|
139
|
+
: "selected wave",
|
|
140
|
+
required: true,
|
|
141
|
+
expected_exit_code: 0,
|
|
142
|
+
}));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function parseHumanGates(preflightText) {
|
|
146
|
+
return sectionText(preflightText, "Human Gates")
|
|
147
|
+
.split("\n")
|
|
148
|
+
.map((line) => line.match(/^[-*]\s+(.+)$/)?.[1]?.trim())
|
|
149
|
+
.filter(Boolean);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function resolveProfile(projectRoot, requestedProfile) {
|
|
153
|
+
const governance = await loadGovernanceConfig(projectRoot);
|
|
154
|
+
const hostText = await readTextIfFile(path.join(projectRoot, ".nimi", "config", "host-profile.yaml"))
|
|
155
|
+
?? await readTextIfFile(path.join(PACKAGE_ROOT, "config", "host-profile.yaml"));
|
|
156
|
+
const hostProfile = parseYamlText(hostText)?.host_profile?.id ?? null;
|
|
157
|
+
const defaultProfile = governance.ok ? governance.config.profileId : hostProfile;
|
|
158
|
+
const effectiveProfile = requestedProfile ?? defaultProfile;
|
|
159
|
+
return {
|
|
160
|
+
ok: typeof effectiveProfile === "string"
|
|
161
|
+
&& effectiveProfile.length > 0
|
|
162
|
+
&& effectiveProfile === defaultProfile,
|
|
163
|
+
profile: effectiveProfile,
|
|
164
|
+
defaultProfile,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function loadForbiddenShortcutsCatalog(projectRoot) {
|
|
169
|
+
const canonicalText = await readTextIfFile(packagePath(FORBIDDEN_SHORTCUTS_REF));
|
|
170
|
+
const hostText = await readTextIfFile(path.join(projectRoot, HOST_FORBIDDEN_SHORTCUTS_REF));
|
|
171
|
+
const catalog = parseYamlText(canonicalText);
|
|
172
|
+
const keys = Array.isArray(catalog?.entries)
|
|
173
|
+
? catalog.entries.map((entry) => entry?.key).filter((key) => typeof key === "string")
|
|
174
|
+
: [];
|
|
175
|
+
return {
|
|
176
|
+
keys,
|
|
177
|
+
aligned: hostText === null || hostText === canonicalText,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function loadSourceArtifacts(topicDir) {
|
|
182
|
+
const artifactTexts = [];
|
|
183
|
+
const missing = [];
|
|
184
|
+
for (const fileName of REQUIRED_ARTIFACTS) {
|
|
185
|
+
const artifactPath = path.join(topicDir, fileName);
|
|
186
|
+
const text = await readTextIfFile(artifactPath);
|
|
187
|
+
if (text === null) {
|
|
188
|
+
missing.push(fileName);
|
|
189
|
+
} else {
|
|
190
|
+
artifactTexts.push({ ref: fileName, text });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return { artifactTexts, missing };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function fileReferencesWave(fileName, waveId) {
|
|
197
|
+
return fileName.includes(waveId) || fileName.includes(waveId.replace(/^wave-/, "wave-"));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function collectLineageArtifacts(topicDir, waveIds) {
|
|
201
|
+
const entries = await readdir(topicDir, { withFileTypes: true });
|
|
202
|
+
return entries
|
|
203
|
+
.filter((entry) => entry.isFile())
|
|
204
|
+
.map((entry) => entry.name)
|
|
205
|
+
.filter((name) => /^(packet|result|closeout)-/.test(name))
|
|
206
|
+
.filter((name) => waveIds.some((waveId) => fileReferencesWave(name, waveId)))
|
|
207
|
+
.sort();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function selectedWaveResolution(topic) {
|
|
211
|
+
const waves = Array.isArray(topic.waves) ? topic.waves : [];
|
|
212
|
+
const selectedTarget = typeof topic.selected_next_target === "string" ? topic.selected_next_target : null;
|
|
213
|
+
const matchingWaves = selectedTarget === null ? [] : waves.filter((wave) => wave.wave_id === selectedTarget);
|
|
214
|
+
const selectedWaves = waves.filter((wave) => wave.selected === true);
|
|
215
|
+
return {
|
|
216
|
+
waves,
|
|
217
|
+
selectedTarget,
|
|
218
|
+
selectedWave: matchingWaves.length === 1 ? matchingWaves[0] : null,
|
|
219
|
+
matchingWaveCount: matchingWaves.length,
|
|
220
|
+
selectedWaves,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function dependencyEvidence(lineageRefs, depId) {
|
|
225
|
+
return lineageRefs.some((ref) => fileReferencesWave(ref, depId) && /^(result|closeout)-/.test(ref));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function buildGoalCommand(topicId, waveId, sourceArtifacts) {
|
|
229
|
+
const artifactList = sourceArtifacts.map((ref) => path.basename(ref)).join(", ");
|
|
230
|
+
return `/goal Execute topic ${topicId} from selected admitted wave ${waveId}. Treat ${artifactList} as the execution contract. Implement only the admitted scope. Do not reinterpret scope, change authority ownership, lower readiness gates, mutate topic state during goal generation, invoke Codex automatically, delete evidence, skip admitted coverage, or emit fallback goals. After meaningful implementation steps, run topic validate, topic validate graph, and focused tests. Stop only for declared human gates, authority/scope changes, lowered gates, destructive evidence deletion, or blockers requiring contract changes. Complete by writing required result/closeout artifacts and reporting validation evidence, blockers, and residual risk.`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function checkHostProjection(projectRoot) {
|
|
234
|
+
const canonicalText = await readTextIfFile(packagePath(TOPIC_GOAL_CONTRACT_REF));
|
|
235
|
+
const hostPath = path.join(projectRoot, HOST_TOPIC_GOAL_CONTRACT_REF);
|
|
236
|
+
const hostInfo = await pathExists(hostPath);
|
|
237
|
+
if (!hostInfo) {
|
|
238
|
+
return { ok: true, message: "host topic-goal projection is absent" };
|
|
239
|
+
}
|
|
240
|
+
const hostText = await readTextIfFile(hostPath);
|
|
241
|
+
return {
|
|
242
|
+
ok: canonicalText !== null && hostText === canonicalText,
|
|
243
|
+
message: canonicalText !== null && hostText === canonicalText
|
|
244
|
+
? "host topic-goal schema projection matches the package contract"
|
|
245
|
+
: "host topic-goal schema projection differs from the package contract",
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function buildStateHash(artifactTexts, extraTexts) {
|
|
250
|
+
const hash = createHash("sha256");
|
|
251
|
+
for (const artifact of [...artifactTexts, ...extraTexts].sort((a, b) => a.ref.localeCompare(b.ref))) {
|
|
252
|
+
hash.update(artifact.ref);
|
|
253
|
+
hash.update("\0");
|
|
254
|
+
hash.update(artifact.text ?? "");
|
|
255
|
+
hash.update("\0");
|
|
256
|
+
}
|
|
257
|
+
return hash.digest("hex");
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export async function buildTopicGoal(projectRoot, options) {
|
|
261
|
+
const loaded = await loadTopicReport(projectRoot, options.topicInput);
|
|
262
|
+
if (!loaded.ok) {
|
|
263
|
+
return {
|
|
264
|
+
inputError: true,
|
|
265
|
+
ok: false,
|
|
266
|
+
error: loaded.error,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const topicReport = await validateTopicRoot(projectRoot, options.topicInput);
|
|
271
|
+
const graphReport = await validateTopicGraph(projectRoot, options.topicInput);
|
|
272
|
+
const profile = await resolveProfile(projectRoot, options.profile);
|
|
273
|
+
const { artifactTexts, missing } = await loadSourceArtifacts(loaded.topicDir);
|
|
274
|
+
const textByRef = new Map(artifactTexts.map((artifact) => [artifact.ref, artifact.text]));
|
|
275
|
+
const resolution = selectedWaveResolution(loaded.topic);
|
|
276
|
+
const lineageWaveIds = [
|
|
277
|
+
...new Set([
|
|
278
|
+
resolution.selectedTarget,
|
|
279
|
+
...resolution.waves.flatMap((wave) => Array.isArray(wave.deps) ? wave.deps : []),
|
|
280
|
+
].filter(Boolean)),
|
|
281
|
+
];
|
|
282
|
+
const lineageRefs = await collectLineageArtifacts(loaded.topicDir, lineageWaveIds);
|
|
283
|
+
const lineageTexts = [];
|
|
284
|
+
for (const ref of lineageRefs) {
|
|
285
|
+
const text = await readTextIfFile(path.join(loaded.topicDir, ref));
|
|
286
|
+
lineageTexts.push({ ref, text: text ?? "" });
|
|
287
|
+
}
|
|
288
|
+
const sourceArtifacts = [...REQUIRED_ARTIFACTS, ...lineageRefs];
|
|
289
|
+
const sourceArtifactTexts = [...artifactTexts, ...lineageTexts];
|
|
290
|
+
const validationCommands = parseValidationCommands(sourceArtifactTexts);
|
|
291
|
+
const humanGates = parseHumanGates(textByRef.get("preflight.md") ?? "");
|
|
292
|
+
const forbiddenCatalog = await loadForbiddenShortcutsCatalog(projectRoot);
|
|
293
|
+
const hostProjection = await checkHostProjection(projectRoot);
|
|
294
|
+
const selectedWave = resolution.selectedWave;
|
|
295
|
+
const deps = Array.isArray(selectedWave?.deps) ? selectedWave.deps : [];
|
|
296
|
+
const depFailures = deps.filter((depId) => {
|
|
297
|
+
const depWave = resolution.waves.find((wave) => wave.wave_id === depId);
|
|
298
|
+
return !depWave || !TERMINAL_WAVE_STATES.has(depWave.state) || !dependencyEvidence(lineageRefs, depId);
|
|
299
|
+
});
|
|
300
|
+
const commands = validationCommands.map((entry) => entry.command);
|
|
301
|
+
|
|
302
|
+
const checks = [
|
|
303
|
+
check("topic_validate_passed", topicReport.ok === true, topicReport.ok ? "topic validate passes" : topicReport.error ?? "topic validate failed"),
|
|
304
|
+
check("topic_graph_validate_passed", graphReport.ok === true, graphReport.ok ? "topic validate graph passes" : graphReport.error ?? "topic validate graph failed"),
|
|
305
|
+
check("topic_lifecycle_ongoing", loaded.topic.state === "ongoing", `topic state is ${loaded.topic.state ?? "missing"}`),
|
|
306
|
+
check("true_close_not_started", loaded.topic.current_true_close_status === "not_started", `current_true_close_status is ${loaded.topic.current_true_close_status ?? "missing"}`),
|
|
307
|
+
check("strict_policy_active", topicReport.ignoredByPolicy !== true, topicReport.ignoredByPolicy ? "topic root is ignored by strict validation policy" : "topic root is under strict validation policy"),
|
|
308
|
+
check("parallel_truth_forbidden", loaded.topic.parallel_truth === "forbidden", `parallel_truth is ${loaded.topic.parallel_truth ?? "missing"}`),
|
|
309
|
+
check("profile_resolves", profile.ok, profile.ok ? `profile resolves as ${profile.profile}` : `unknown or mismatched profile ${profile.profile ?? "missing"}`),
|
|
310
|
+
check("selected_target_wave_resolves", resolution.matchingWaveCount === 1 && WAVE_ID_PATTERN.test(resolution.selectedTarget ?? ""), `selected_next_target is ${resolution.selectedTarget ?? "missing"}`),
|
|
311
|
+
check("selected_wave_single_source", resolution.selectedWaves.length === 1 && resolution.selectedWaves[0]?.wave_id === resolution.selectedTarget, `selected waves: ${resolution.selectedWaves.map((wave) => wave.wave_id).join(", ") || "none"}`),
|
|
312
|
+
check("wave_option_matches_selected", options.wave === null || options.wave === resolution.selectedTarget, options.wave === null ? "no wave assertion provided" : `--wave is ${options.wave}`),
|
|
313
|
+
check("selected_wave_executable", selectedWave ? EXECUTABLE_WAVE_STATES.has(selectedWave.state) : false, selectedWave ? `selected wave state is ${selectedWave.state}` : "selected wave does not resolve"),
|
|
314
|
+
check("selected_wave_dependencies_terminal", depFailures.length === 0, depFailures.length === 0 ? "selected wave dependencies are terminal by lifecycle evidence" : `dependencies are not terminal by evidence: ${depFailures.join(", ")}`),
|
|
315
|
+
check("selected_wave_goal_present", typeof selectedWave?.primary_closure_goal === "string" && selectedWave.primary_closure_goal.trim().length > 0, selectedWave?.primary_closure_goal ? "selected wave declares primary_closure_goal" : "selected wave is missing primary_closure_goal"),
|
|
316
|
+
check("forbidden_shortcuts_present", REQUIRED_STOP_KEYS.every((key) => (loaded.topic.forbidden_shortcuts ?? []).includes(key)) && REQUIRED_STOP_KEYS.every((key) => forbiddenCatalog.keys.includes(key)), "topic forbidden_shortcuts include required package catalog keys"),
|
|
317
|
+
check("forbidden_shortcuts_catalog_aligned", forbiddenCatalog.aligned, forbiddenCatalog.aligned ? "host forbidden-shortcuts projection is aligned or absent" : "host forbidden-shortcuts projection differs from package catalog"),
|
|
318
|
+
check("required_artifacts_present", missing.length === 0, missing.length === 0 ? "all required artifacts are present" : `missing artifacts: ${missing.join(", ")}`),
|
|
319
|
+
check("no_unresolved_placeholders", sourceArtifactTexts.every(({ text }) => !hasUnresolvedPlaceholder(text)), "required artifacts contain no frozen placeholder markers"),
|
|
320
|
+
check("stop_line_declared", hasSectionBullet(textByRef.get("preflight.md") ?? "", "Stop Line"), "preflight.md declares Stop Line bullets"),
|
|
321
|
+
check("human_gates_declared", humanGates.length > 0, humanGates.length > 0 ? "preflight.md declares Human Gates bullets" : "preflight.md does not declare Human Gates bullets"),
|
|
322
|
+
check("validation_commands_declared", commands.some((command) => command.includes("topic validate ")) && commands.some((command) => command.includes("topic validate graph")), "validation command evidence includes topic validate and topic validate graph"),
|
|
323
|
+
check("closeout_criteria_declared", /Wave-1 Closeout Requirements/i.test(textByRef.get("closeout.md") ?? "") && /\bcomplete\b/i.test(textByRef.get("closeout.md") ?? "") && /\bpartial\b/i.test(textByRef.get("closeout.md") ?? "") && (/\bblocked\b/i.test(textByRef.get("closeout.md") ?? "") || /Non-Closure Conditions/i.test(textByRef.get("closeout.md") ?? "") || /Do not close if/i.test(textByRef.get("closeout.md") ?? "")) && /\bpending\b/i.test(textByRef.get("closeout.md") ?? ""), "closeout.md declares selected-wave criteria and complete/partial/blocked/pending states"),
|
|
324
|
+
check("authority_owner_declared", sectionText(textByRef.get("preflight.md") ?? "", "Authority Owner").length > 0, "preflight.md declares Authority Owner"),
|
|
325
|
+
check("work_type_declared", sectionText(textByRef.get("preflight.md") ?? "", "Work Type").length > 0, "preflight.md declares Work Type"),
|
|
326
|
+
check("authority_change_admitted", !/authority ownership changes? (?:are )?in scope/i.test(textByRef.get("preflight.md") ?? "") || /\.nimi\/spec\//.test(textByRef.get("preflight.md") ?? ""), "authority-changing work is either out of scope or cites .nimi/spec alignment"),
|
|
327
|
+
check("host_projection_aligned", hostProjection.ok, hostProjection.message),
|
|
328
|
+
];
|
|
329
|
+
|
|
330
|
+
const preliminaryOk = checks.every((entry) => entry.status === "pass");
|
|
331
|
+
const preliminaryGoal = preliminaryOk && selectedWave ? buildGoalCommand(loaded.topicId, selectedWave.wave_id, sourceArtifacts) : null;
|
|
332
|
+
checks.push(check("goal_size_within_limit", preliminaryGoal === null || preliminaryGoal.length <= GOAL_COMMAND_MAX_CHARS, preliminaryGoal === null ? "goal size check skipped until readiness passes" : `goal command length is ${preliminaryGoal.length}`));
|
|
333
|
+
|
|
334
|
+
const readinessOk = checks.every((entry) => entry.status === "pass");
|
|
335
|
+
const goalCommand = readinessOk && selectedWave ? preliminaryGoal : null;
|
|
336
|
+
return {
|
|
337
|
+
ok: readinessOk,
|
|
338
|
+
topic_id: loaded.topicId,
|
|
339
|
+
topic_ref: projectRef(projectRoot, loaded.topicDir),
|
|
340
|
+
topic_state: loaded.topic.state ?? null,
|
|
341
|
+
true_close_status: loaded.topic.current_true_close_status ?? null,
|
|
342
|
+
profile: profile.profile ?? null,
|
|
343
|
+
selected_next_target: loaded.topic.selected_next_target ?? null,
|
|
344
|
+
selected_wave_id: selectedWave?.wave_id ?? null,
|
|
345
|
+
topic_state_hash: buildStateHash(sourceArtifactTexts, [
|
|
346
|
+
{ ref: TOPIC_GOAL_CONTRACT_REF, text: await readTextIfFile(packagePath(TOPIC_GOAL_CONTRACT_REF)) ?? "" },
|
|
347
|
+
]),
|
|
348
|
+
readiness: {
|
|
349
|
+
ok: readinessOk,
|
|
350
|
+
checks,
|
|
351
|
+
},
|
|
352
|
+
goal_command: goalCommand,
|
|
353
|
+
source_artifacts: sourceArtifacts,
|
|
354
|
+
validation_commands: validationCommands,
|
|
355
|
+
human_gates: humanGates,
|
|
356
|
+
refusal_reasons: checks
|
|
357
|
+
.filter((entry) => entry.status === "fail")
|
|
358
|
+
.map((entry) => CHECK_REASON[entry.id])
|
|
359
|
+
.filter(Boolean),
|
|
360
|
+
};
|
|
361
|
+
}
|