@wairon/cli 5.1.1-dev.40 → 5.1.1-dev.41

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/README.md CHANGED
@@ -175,6 +175,7 @@ See [docs/cli.md](docs/cli.md). Summary:
175
175
  | `wairon domains list \| scan \| add \| remove` | Domains (subsystem-derived + free-standing) |
176
176
  | `wairon skills list \| install` | Manage the SDD skills installed into your tools |
177
177
  | `wairon lock [-y]` | Validate the tree as-complete and freeze it (generates the implementer agents) |
178
+ | `wairon lock-check [--strict]` | Merge gate: is the design in this tree the design that was approved? Importable as a [reusable workflow](.github/workflows/lock-check.yml) |
178
179
  | `wairon mcp serve \| install \| status` | The wairon MCP server (`sdd_*` tools) |
179
180
  | `wairon serve [--port] [--data-dir] [--no-auth]` | Self-host: HTTP MCP for many isolated projects + admin plane |
180
181
  | `wairon host project \| key \| lock \| promote` | Administer the hosting server (projects, keys, state-scoped lock/promote) |
package/dist/cli/index.js CHANGED
@@ -65,7 +65,7 @@ var init_defaults = __esm({
65
65
  copilot: ".github/prompts",
66
66
  codex: ".codex/agents"
67
67
  };
68
- WAIRON_VERSION = "5.1.1-dev.40";
68
+ WAIRON_VERSION = "5.1.1-dev.41";
69
69
  GITHUB_REPO = "SYW-Apps/Waffle-AIron";
70
70
  SUPPORTED_ALIASES = ["wai"];
71
71
  SCAN_EXCLUDE_DIRS = /* @__PURE__ */ new Set([
@@ -3136,6 +3136,10 @@ var init_budget_policy = __esm({
3136
3136
  function canonicalize(value) {
3137
3137
  return JSON.stringify(sortKeys(value));
3138
3138
  }
3139
+ function compareOrdinal(a, b) {
3140
+ if (a < b) return -1;
3141
+ return a > b ? 1 : 0;
3142
+ }
3139
3143
  function sortKeys(v) {
3140
3144
  if (Array.isArray(v)) return v.map(sortKeys);
3141
3145
  if (v && typeof v === "object") {
@@ -3156,14 +3160,17 @@ var init_canonical_json = __esm({
3156
3160
  });
3157
3161
 
3158
3162
  // src/core/statehash.ts
3163
+ function inIdentityOrder(specs) {
3164
+ return specs.map((spec) => ({ spec, content: canonicalize(spec) })).sort((a, b) => compareOrdinal(a.spec.id, b.spec.id) || compareOrdinal(a.content, b.content)).map((entry) => entry.spec);
3165
+ }
3159
3166
  function computeStateId() {
3160
3167
  const tree = {
3161
3168
  system: loadSystemSpec(),
3162
- subsystems: loadSubsystemSpecs(),
3163
- components: loadComponentSpecs(),
3164
- interfaces: loadInterfaceSpecs(),
3165
- implementations: loadImplementationSpecs(),
3166
- types: loadTypeSpecs()
3169
+ subsystems: inIdentityOrder(loadSubsystemSpecs()),
3170
+ components: inIdentityOrder(loadComponentSpecs()),
3171
+ interfaces: inIdentityOrder(loadInterfaceSpecs()),
3172
+ implementations: inIdentityOrder(loadImplementationSpecs()),
3173
+ types: inIdentityOrder(loadTypeSpecs())
3167
3174
  };
3168
3175
  const digest2 = crypto.createHash("sha256").update(canonicalize(tree)).digest("hex");
3169
3176
  return { algorithm: CONTENT_ALGORITHM, digest: digest2 };
@@ -8426,7 +8433,7 @@ function discoverPacks(dir) {
8426
8433
  return [];
8427
8434
  }
8428
8435
  const refs = [];
8429
- for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {
8436
+ for (const e of entries.sort((a, b) => compareOrdinal(a.name, b.name))) {
8430
8437
  const full = path12.join(dir, e.name);
8431
8438
  if (e.isDirectory()) {
8432
8439
  if (packDirEntry(full)) refs.push(full);
@@ -8716,6 +8723,7 @@ var init_extensions = __esm({
8716
8723
  init_fs();
8717
8724
  init_project_config();
8718
8725
  init_version();
8726
+ init_canonical_json();
8719
8727
  init_project();
8720
8728
  init_packstore();
8721
8729
  ProfileDefSchema = import_zod9.z.object({
@@ -22436,7 +22444,7 @@ var init_core = __esm({
22436
22444
 
22437
22445
  // src/core/rules/gate-identity.ts
22438
22446
  function doctrineIdentity(doctrine, builtinRules, gate) {
22439
- const byKey = (items, key) => [...items].sort((a, b) => key(a).localeCompare(key(b)));
22447
+ const byKey = (items, key) => [...items].sort((a, b) => compareOrdinal(key(a), key(b)));
22440
22448
  return {
22441
22449
  /**
22442
22450
  * The BUILTIN rule set, by identity rather than by wairon version.
@@ -30445,6 +30453,37 @@ var path35 = __toESM(require("path"));
30445
30453
  init_core();
30446
30454
  init_fs();
30447
30455
  init_validation();
30456
+ function checkApproval(strict) {
30457
+ if (!loadSystemSpec()) {
30458
+ return {
30459
+ state: "no-tree",
30460
+ approved: !strict,
30461
+ message: strict ? "No SDD spec tree here (.wai/specs) \u2014 and --strict asks for an approved design, so this is a failure. Run `wairon init` to start one, or drop --strict." : "No SDD spec tree here (.wai/specs) \u2014 there is no design to approve, so nothing is gated."
30462
+ };
30463
+ }
30464
+ const lock = readLockState(computeGateStateId());
30465
+ const record2 = lock.record;
30466
+ switch (lock.state) {
30467
+ case "locked":
30468
+ return {
30469
+ state: "locked",
30470
+ approved: true,
30471
+ message: `The design in this tree is the approved design \u2014 approved ${record2.lockedAt} by ${describeApprover(record2.lockedBy)}.`
30472
+ };
30473
+ case "stale":
30474
+ return {
30475
+ state: "stale",
30476
+ approved: false,
30477
+ message: `The design changed after it was approved (the approval on record is ${record2.lockedAt} by ${describeApprover(record2.lockedBy)}). Nothing says a human has seen what is about to merge. Fix: run \`wairon lock\` on this branch and commit the updated .wai/lock.json.`
30478
+ };
30479
+ default:
30480
+ return {
30481
+ state: "unlocked",
30482
+ approved: !strict,
30483
+ message: strict ? "No approval on record (.wai/lock.json is absent), and --strict asks for one. Run `wairon lock` and commit the record." : "No approval on record (.wai/lock.json is absent) \u2014 this project has not opted into the approval gate, so nothing is gated. Run `wairon lock` and commit the record to turn it on, or pass --strict to fail here instead."
30484
+ };
30485
+ }
30486
+ }
30448
30487
  async function runLock(options = {}, gate) {
30449
30488
  const diff = diffAgainstApproval();
30450
30489
  if (!diff) {
@@ -47147,6 +47186,17 @@ async function statusCommand(opts) {
47147
47186
  }
47148
47187
  await runStatus({ subsystem: opts.subsystem, recursive: opts.recursive });
47149
47188
  }
47189
+ async function lockCheckCommand(opts) {
47190
+ const verdict = checkApproval(opts.strict === true);
47191
+ if (!verdict.approved) logger.error(verdict.message);
47192
+ else if (verdict.state === "locked") logger.success(verdict.message);
47193
+ else logger.info(verdict.message);
47194
+ if (verdict.approved) return;
47195
+ process.exit(1);
47196
+ }
47197
+ program.command("lock-check").description("Merge gate: is the design in this working tree the design that was approved? Compares the tree's gate identity against the committed .wai/lock.json. Exits 1 when the design moved past its approval; passes with a notice when the project never locked, unless --strict.").option("--strict", "also fail when nothing was ever approved, or when there is no spec tree at all").action(async (opts) => {
47198
+ await lockCheckCommand(opts);
47199
+ });
47150
47200
  program.command("lock").description("Final check before implementation: validate the spec tree as complete, freeze all specs to complete, and (re)generate the agent topology \u2014 only if it validates. In an attached checkout, locks the hosted project instead.").option("-y, --yes", "skip the confirmation prompt (for scripts / CI)").option("--subsystem <id>", "only lock specs in the specified subsystem").option("--no-recursive", "do not recursively validate subprojects").action(async (opts) => {
47151
47201
  await lockCommand(opts);
47152
47202
  });