@wairon/cli 5.1.1-dev.42 → 5.1.1-dev.44
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/dist/cli/index.js +245 -60
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +225 -49
- package/dist/index.js.map +1 -1
- package/dist/templates/skills/sdd-narrative.md +1 -0
- package/package.json +1 -1
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.
|
|
68
|
+
WAIRON_VERSION = "5.1.1-dev.44";
|
|
69
69
|
GITHUB_REPO = "SYW-Apps/Waffle-AIron";
|
|
70
70
|
SUPPORTED_ALIASES = ["wai"];
|
|
71
71
|
SCAN_EXCLUDE_DIRS = /* @__PURE__ */ new Set([
|
|
@@ -890,6 +890,31 @@ var init_project = __esm({
|
|
|
890
890
|
kind: CarriedDebtKindSchema,
|
|
891
891
|
/** The reason itself, in the author's own words — what is true here, and what paying it would take. */
|
|
892
892
|
why: import_zod4.z.string().min(1),
|
|
893
|
+
/**
|
|
894
|
+
* This classification is PROVISIONAL, and this is what would settle it.
|
|
895
|
+
*
|
|
896
|
+
* A confident-sounding `why` that is wrong is worse than a missing one: it
|
|
897
|
+
* reads as settled, so nobody looks again, and the register keeps counting
|
|
898
|
+
* the finding under a kind that was never true. Nothing can check a reason
|
|
899
|
+
* for truth — `kind` and `why` are prose, and STALE_CARRIED_FINDING only
|
|
900
|
+
* ever catches "this stopped applying", never "this still applies for a
|
|
901
|
+
* reason that has become false". What a register CAN do is let an author say
|
|
902
|
+
* out loud that they are not sure yet, and then keep saying it: every run
|
|
903
|
+
* counts the groups marked here beside the kind totals, so the uncertainty
|
|
904
|
+
* is as loud as the debt.
|
|
905
|
+
*
|
|
906
|
+
* It is deliberately a SENTENCE and not a flag, and deliberately on the
|
|
907
|
+
* reason GROUP rather than the finding: what is provisional is the reason,
|
|
908
|
+
* and a reader deciding whether to pick this up needs to know what to
|
|
909
|
+
* measure — "is the adapter really realized by the file that consumes it, or
|
|
910
|
+
* is that a modelling error?" — not merely that somebody once hesitated. A
|
|
911
|
+
* finding whose own classification is uncertain while its neighbours' is not
|
|
912
|
+
* is a different reason, and belongs in its own group.
|
|
913
|
+
*
|
|
914
|
+
* There is no counterpart on `lint.allow`, and that is a decision, not an
|
|
915
|
+
* omission: see the note on ConformanceRuleConfig.carried.
|
|
916
|
+
*/
|
|
917
|
+
revisit: import_zod4.z.string().min(1).optional(),
|
|
893
918
|
/** The findings this reason explains. */
|
|
894
919
|
findings: import_zod4.z.array(CarriedFindingSchema)
|
|
895
920
|
});
|
|
@@ -935,6 +960,17 @@ var init_project = __esm({
|
|
|
935
960
|
* • every entry states its kind and its reason, and `wairon validate`
|
|
936
961
|
* prints the running total, so the debt is loud where a suppression is
|
|
937
962
|
* silent.
|
|
963
|
+
*
|
|
964
|
+
* A reason group may also declare itself PROVISIONAL (`revisit`), and the
|
|
965
|
+
* total says how many did. There is no such marker on a `lint.allow`, and
|
|
966
|
+
* the asymmetry is the point: an entry here does not silence anything — the
|
|
967
|
+
* finding is counted out loud on every run — so marking one uncertain adds a
|
|
968
|
+
* second dial to something already visible. An allow DOES silence, and a
|
|
969
|
+
* "provisional allow" would buy the silence and defer the decision, which is
|
|
970
|
+
* the one combination that cannot be reviewed: the finding is gone and the
|
|
971
|
+
* doubt is in a field nobody opens. The mechanism for a decision nobody has
|
|
972
|
+
* taken is to not take it — leave the warning firing — or, where the code is
|
|
973
|
+
* carryable, an entry of kind `undecided`, which is exactly that sentence.
|
|
938
974
|
*/
|
|
939
975
|
carried: import_zod4.z.array(CarriedDebtSchema).optional()
|
|
940
976
|
});
|
|
@@ -1251,6 +1287,11 @@ function passesIntentFloor(text2, methodName) {
|
|
|
1251
1287
|
if (norm(t) === norm(methodName)) return false;
|
|
1252
1288
|
return true;
|
|
1253
1289
|
}
|
|
1290
|
+
function parseDeclaredCall(ref) {
|
|
1291
|
+
const at = ref.lastIndexOf(".");
|
|
1292
|
+
if (at <= 0 || at === ref.length - 1) return null;
|
|
1293
|
+
return { compId: ref.slice(0, at), methodName: ref.slice(at + 1) };
|
|
1294
|
+
}
|
|
1254
1295
|
function methodSourceFile(method2, implementationSourcePath) {
|
|
1255
1296
|
return method2.sourcePath || implementationSourcePath || void 0;
|
|
1256
1297
|
}
|
|
@@ -1411,8 +1452,28 @@ var init_specs = __esm({
|
|
|
1411
1452
|
LintAllowSchema = import_zod7.z.object({
|
|
1412
1453
|
/** The issue code being allowed (see `wairon rules list`). */
|
|
1413
1454
|
code: import_zod7.z.string(),
|
|
1455
|
+
/**
|
|
1456
|
+
* The SITE inside this spec the allow covers, named exactly as the finding
|
|
1457
|
+
* names it (a contract method, an import edge "from -> to", a declared edge
|
|
1458
|
+
* "component -> target"). A finding that names a site is covered ONLY by an
|
|
1459
|
+
* allow naming that same site; a finding that names none is covered only by
|
|
1460
|
+
* an allow that names none either.
|
|
1461
|
+
*/
|
|
1462
|
+
at: import_zod7.z.string().min(1).optional(),
|
|
1463
|
+
/**
|
|
1464
|
+
* The units of an AGGREGATING finding this allow covers — the steps of one
|
|
1465
|
+
* CALL_STEP_UNREALIZED, the crossings of one UNDECLARED_COLOCATED_CALL —
|
|
1466
|
+
* each named the way the finding's own message names it. The allow silences
|
|
1467
|
+
* the finding only when it lists EVERY unit reported, so a unit nobody
|
|
1468
|
+
* decided on surfaces on the day it appears instead of inheriting a decision
|
|
1469
|
+
* taken about its neighbours. Meaningless without `at`, and refused there.
|
|
1470
|
+
*/
|
|
1471
|
+
covers: import_zod7.z.array(import_zod7.z.string()).optional(),
|
|
1414
1472
|
/** Why this finding is acceptable here (e.g. "dispatcher — fan-out is the point"). */
|
|
1415
1473
|
reason: import_zod7.z.string().min(1)
|
|
1474
|
+
}).refine((a) => !a.covers?.length || !!a.at, {
|
|
1475
|
+
message: "`covers` names the units of ONE finding, so it needs the `at` that says which finding \u2014 add the site, or drop covers.",
|
|
1476
|
+
path: ["covers"]
|
|
1416
1477
|
});
|
|
1417
1478
|
LintConfigSchema = import_zod7.z.object({
|
|
1418
1479
|
allow: import_zod7.z.array(LintAllowSchema).default([])
|
|
@@ -1861,6 +1922,31 @@ var init_specs = __esm({
|
|
|
1861
1922
|
intent: import_zod7.z.string().optional(),
|
|
1862
1923
|
/** Conformance tier for THIS method (overrides the spec-level default). */
|
|
1863
1924
|
conformance: ConformanceTierSchema.optional(),
|
|
1925
|
+
/**
|
|
1926
|
+
* The calls this method makes, when its narrative does not show them.
|
|
1927
|
+
*
|
|
1928
|
+
* The mirror of the contract's `invokedBy`: that declares the caller
|
|
1929
|
+
* OUTSIDE the modeled graph, this declares the callees INSIDE it. It lives
|
|
1930
|
+
* here and not on the L3 method because what a method CALLS is a property
|
|
1931
|
+
* of the realization — the detail dial that hides the steps is here, and two
|
|
1932
|
+
* implementations of one contract may reach different collaborators —
|
|
1933
|
+
* whereas an external caller is a fact about the contract's role that must
|
|
1934
|
+
* hold for every realization.
|
|
1935
|
+
*
|
|
1936
|
+
* Each entry names one target the way the debt register and a lint allow
|
|
1937
|
+
* name a unit: `<component>.<method>` (a `covers` entry is the same
|
|
1938
|
+
* spelling with the step number a declaration has no equivalent of). A
|
|
1939
|
+
* third vocabulary for "which method" would be the worse outcome.
|
|
1940
|
+
*
|
|
1941
|
+
* The narrative graph walk takes exactly these edges for a method with no
|
|
1942
|
+
* narrative, and NOTHING else: an undeclared collaborator is not reached,
|
|
1943
|
+
* so a lower detail dial no longer vouches for everything the component
|
|
1944
|
+
* declares. Each entry gets the same target validation a `call` step gets
|
|
1945
|
+
* (the component resolves, the caller declares it, the method is on its
|
|
1946
|
+
* contract). Refused beside a non-empty narrative, where the steps already
|
|
1947
|
+
* say what is called and a second spelling could only disagree.
|
|
1948
|
+
*/
|
|
1949
|
+
calls: import_zod7.z.array(import_zod7.z.string().min(1)).optional(),
|
|
1864
1950
|
/**
|
|
1865
1951
|
* The code-level name realizing this contract method in the sourcePath
|
|
1866
1952
|
* file, when it legitimately differs from the intent-language contract
|
|
@@ -1897,7 +1983,16 @@ var init_specs = __esm({
|
|
|
1897
1983
|
* stereotypes should bind tech directly (TECH_ON_LOGIC_COMPONENT).
|
|
1898
1984
|
*/
|
|
1899
1985
|
technologies: import_zod7.z.array(import_zod7.z.string()).optional(),
|
|
1900
|
-
methods: import_zod7.z.array(MethodImplementationSchema).default([]),
|
|
1986
|
+
methods: import_zod7.z.array(MethodImplementationSchema).default([]).superRefine((methods, ctx) => {
|
|
1987
|
+
methods.forEach((m, i) => {
|
|
1988
|
+
if (!m.calls?.length || m.narrative.length === 0) return;
|
|
1989
|
+
ctx.addIssue({
|
|
1990
|
+
code: import_zod7.z.ZodIssueCode.custom,
|
|
1991
|
+
path: [i, "calls"],
|
|
1992
|
+
message: `Method "${m.name}" declares calls AND a narrative \u2014 \`calls\` says what a method calls when its narrative does not show it, and this one has ${m.narrative.length} step(s) that already do. Add the call step, or drop the declaration.`
|
|
1993
|
+
});
|
|
1994
|
+
});
|
|
1995
|
+
}),
|
|
1901
1996
|
/** Spec-level narrative detail default for all methods (each may override). */
|
|
1902
1997
|
detail: NarrativeDetailSchema.optional(),
|
|
1903
1998
|
/** Spec-level conformance tier default (each method may override). */
|
|
@@ -9694,10 +9789,10 @@ var init_lint_allows = __esm({
|
|
|
9694
9789
|
"use strict";
|
|
9695
9790
|
lintAllowsRule = {
|
|
9696
9791
|
name: "lint-allows",
|
|
9697
|
-
description: "Per-spec lint suppressions (lint.allow) must name real issue codes and actually suppress a finding \u2014 unknown codes and stale allows are flagged. Allows silence warnings only; errors always surface.",
|
|
9792
|
+
description: "Per-spec lint suppressions (lint.allow) must name real issue codes and actually suppress a finding \u2014 unknown codes and stale allows are flagged. An allow covers exactly the occurrence it names: a finding that reports a site is silenced only by an allow whose `at` is that site, a finding that reports none only by an allow that names none, and an aggregating finding only by an allow whose `covers` lists every unit it reports \u2014 a unit nobody listed is named back as new instead of inheriting a decision taken about its neighbours. So a coarse allow left on a rule that names sites, and an allow whose site the run no longer reports, are both UNUSED_LINT_ALLOW, and the finding names the sites that did fire. Allows silence warnings only; errors always surface.",
|
|
9698
9793
|
codes: [
|
|
9699
9794
|
{ code: "UNKNOWN_LINT_ALLOW_CODE", defaultSeverity: "warning", summary: "lint.allow names an issue code no registered rule emits" },
|
|
9700
|
-
{ code: "UNUSED_LINT_ALLOW", defaultSeverity: "warning", summary: "lint.allow entry matched no finding this run \u2014
|
|
9795
|
+
{ code: "UNUSED_LINT_ALLOW", defaultSeverity: "warning", summary: "lint.allow entry matched no finding this run \u2014 the code never fired, or it fired at sites this allow does not name" }
|
|
9701
9796
|
],
|
|
9702
9797
|
check(ctx) {
|
|
9703
9798
|
for (const a of ctx.lintAllows) {
|
|
@@ -9710,14 +9805,24 @@ var init_lint_allows = __esm({
|
|
|
9710
9805
|
);
|
|
9711
9806
|
continue;
|
|
9712
9807
|
}
|
|
9713
|
-
if (
|
|
9714
|
-
|
|
9715
|
-
|
|
9716
|
-
|
|
9717
|
-
|
|
9718
|
-
|
|
9719
|
-
|
|
9808
|
+
if (a.used) continue;
|
|
9809
|
+
const reported = ctx.sitesReported(a.specId, a.code);
|
|
9810
|
+
const at = a.at ? ` at "${a.at}"` : "";
|
|
9811
|
+
let why;
|
|
9812
|
+
if (a.at && reported.unsited && reported.sites.length === 0) {
|
|
9813
|
+
why = `findings of "${a.code}" on this spec name no site at all, so this allow must not name one \u2014 drop the \`at\``;
|
|
9814
|
+
} else if (reported.sites.length > 0) {
|
|
9815
|
+
const sites = reported.sites.map((s) => `"${s}"`).join("; ");
|
|
9816
|
+
why = a.at ? `that code fired at ${sites} instead \u2014 retarget the allow, or remove it` : `that code fired at ${sites}, and a sited finding is covered only by an allow naming its site \u2014 give this allow an \`at\` (one per site, each with its own reason), or remove it`;
|
|
9817
|
+
} else {
|
|
9818
|
+
why = "no such finding fired this run \u2014 remove the stale allow";
|
|
9720
9819
|
}
|
|
9820
|
+
ctx.addIssue(
|
|
9821
|
+
"warning",
|
|
9822
|
+
"UNUSED_LINT_ALLOW",
|
|
9823
|
+
`Spec "${a.specId}" allows "${a.code}"${at} (reason: ${a.reason}), but ${why}.`,
|
|
9824
|
+
a.specId
|
|
9825
|
+
);
|
|
9721
9826
|
}
|
|
9722
9827
|
}
|
|
9723
9828
|
};
|
|
@@ -9772,16 +9877,36 @@ var init_contract_symmetry = __esm({
|
|
|
9772
9877
|
});
|
|
9773
9878
|
|
|
9774
9879
|
// src/core/rules/narrative/narrative-target-references.ts
|
|
9775
|
-
var narrativeTargetReferencesRule;
|
|
9880
|
+
var targetsOf, narrativeTargetReferencesRule;
|
|
9776
9881
|
var init_narrative_target_references = __esm({
|
|
9777
9882
|
"src/core/rules/narrative/narrative-target-references.ts"() {
|
|
9778
9883
|
"use strict";
|
|
9884
|
+
init_models();
|
|
9885
|
+
targetsOf = (implMethod) => {
|
|
9886
|
+
const entries = [];
|
|
9887
|
+
for (const step of implMethod.narrative) {
|
|
9888
|
+
if (step.type !== "call" && step.type !== "dispatch" && step.type !== "register") continue;
|
|
9889
|
+
entries.push({
|
|
9890
|
+
kind: step.type,
|
|
9891
|
+
stepNumber: step.stepNumber,
|
|
9892
|
+
targetComponent: step.targetComponent,
|
|
9893
|
+
targetMethod: step.targetMethod,
|
|
9894
|
+
assertsGuarantees: step.assertsGuarantees
|
|
9895
|
+
});
|
|
9896
|
+
}
|
|
9897
|
+
for (const ref of implMethod.calls ?? []) {
|
|
9898
|
+
const parsed = parseDeclaredCall(ref);
|
|
9899
|
+
entries.push(parsed ? { kind: "declared", targetComponent: parsed.compId, targetMethod: parsed.methodName } : { kind: "declared", unparsed: ref });
|
|
9900
|
+
}
|
|
9901
|
+
return entries;
|
|
9902
|
+
};
|
|
9779
9903
|
narrativeTargetReferencesRule = {
|
|
9780
9904
|
name: "narrative-target-references",
|
|
9781
|
-
description:
|
|
9905
|
+
description: `A narrative call, dispatch or register step must name a target component, and a target method unless it dispatches; a method's declared calls name the same pair as one "<component>.<method>" reference, the spelling the conformance debt register and a lint allow name a unit with. A target this tree contains must be a collaborator the calling component declares, must carry the named method on one of its interfaces, and that method must declare every semantic guarantee the entry asserts.`,
|
|
9782
9906
|
codes: [
|
|
9783
9907
|
{ code: "MISSING_TARGET_COMPONENT", defaultSeverity: "error", summary: "Call/register step missing targetComponent" },
|
|
9784
9908
|
{ code: "MISSING_TARGET_METHOD", defaultSeverity: "error", summary: "Call/register step missing targetMethod" },
|
|
9909
|
+
{ code: "MALFORMED_DECLARED_CALL", defaultSeverity: "error", summary: 'Declared call that is not a "<component>.<method>" reference' },
|
|
9785
9910
|
{ code: "UNDECLARED_DEPENDENCY_CALL", defaultSeverity: "error", summary: "Call step targets a component the caller does not depend on or own" },
|
|
9786
9911
|
{ code: "INVALID_TARGET_METHOD_REFERENCE", defaultSeverity: "error", summary: "Call step targets a method not on any target interface" },
|
|
9787
9912
|
{ code: "NARRATIVE_SEMANTIC_UNBACKED", defaultSeverity: "warning", summary: "Narrative asserts a guarantee the called contract does not declare" }
|
|
@@ -9793,25 +9918,35 @@ var init_narrative_target_references = __esm({
|
|
|
9793
9918
|
const isDraftCtx = ctx.isImplementationDraft(impl);
|
|
9794
9919
|
const caller = ctx.componentMap.get(contract.component);
|
|
9795
9920
|
for (const implMethod of impl.methods) {
|
|
9796
|
-
for (const
|
|
9797
|
-
|
|
9798
|
-
if (
|
|
9921
|
+
for (const entry of targetsOf(implMethod)) {
|
|
9922
|
+
const where = entry.stepNumber === void 0 ? "" : ` (step ${entry.stepNumber})`;
|
|
9923
|
+
if (entry.unparsed !== void 0) {
|
|
9924
|
+
ctx.addIssue(
|
|
9925
|
+
"error",
|
|
9926
|
+
"MALFORMED_DECLARED_CALL",
|
|
9927
|
+
`Method "${implMethod.name}" in implementation "${impl.id}" declares the call "${entry.unparsed}", which is not a "<component>.<method>" reference \u2014 the spelling the conformance debt register and a lint allow name a unit with.`,
|
|
9928
|
+
impl.id,
|
|
9929
|
+
isDraftCtx
|
|
9930
|
+
);
|
|
9931
|
+
continue;
|
|
9932
|
+
}
|
|
9933
|
+
if (!entry.targetComponent) {
|
|
9799
9934
|
ctx.addIssue(
|
|
9800
9935
|
"error",
|
|
9801
9936
|
"MISSING_TARGET_COMPONENT",
|
|
9802
|
-
`Method "${implMethod.name}" in implementation "${impl.id}" has a ${
|
|
9937
|
+
`Method "${implMethod.name}" in implementation "${impl.id}" has a ${entry.kind} step (${entry.stepNumber}) missing "targetComponent".`,
|
|
9803
9938
|
impl.id,
|
|
9804
9939
|
isDraftCtx
|
|
9805
9940
|
);
|
|
9806
9941
|
continue;
|
|
9807
9942
|
}
|
|
9808
|
-
const target =
|
|
9809
|
-
const verb =
|
|
9810
|
-
if (
|
|
9943
|
+
const target = entry.targetComponent;
|
|
9944
|
+
const verb = entry.kind === "dispatch" ? "dispatches through" : entry.kind === "register" ? "registers callback" : entry.kind === "declared" ? "declares a call to" : "calls";
|
|
9945
|
+
if (entry.kind !== "dispatch" && !entry.targetMethod) {
|
|
9811
9946
|
ctx.addIssue(
|
|
9812
9947
|
"error",
|
|
9813
9948
|
"MISSING_TARGET_METHOD",
|
|
9814
|
-
`Method "${implMethod.name}" in implementation "${impl.id}" has a ${
|
|
9949
|
+
`Method "${implMethod.name}" in implementation "${impl.id}" has a ${entry.kind} step (${entry.stepNumber}) missing "targetMethod".`,
|
|
9815
9950
|
impl.id,
|
|
9816
9951
|
isDraftCtx
|
|
9817
9952
|
);
|
|
@@ -9822,16 +9957,16 @@ var init_narrative_target_references = __esm({
|
|
|
9822
9957
|
ctx.addIssue(
|
|
9823
9958
|
"error",
|
|
9824
9959
|
"UNDECLARED_DEPENDENCY_CALL",
|
|
9825
|
-
`Method "${implMethod.name}" in implementation "${impl.id}" (component "${caller.id}") ${verb} component "${target}"
|
|
9960
|
+
`Method "${implMethod.name}" in implementation "${impl.id}" (component "${caller.id}") ${verb} component "${target}"${where} but component "${caller.id}" does not list "${target}" as a dependency.`,
|
|
9826
9961
|
impl.id,
|
|
9827
9962
|
isDraftCtx || ctx.isComponentDraft(caller.id)
|
|
9828
9963
|
);
|
|
9829
9964
|
}
|
|
9830
|
-
if (
|
|
9965
|
+
if (entry.kind === "dispatch") continue;
|
|
9831
9966
|
const targetInterfaces = ctx.interfacesByComponent.get(target) ?? [];
|
|
9832
9967
|
let targetMethodSpec;
|
|
9833
9968
|
for (const targetIntf of targetInterfaces) {
|
|
9834
|
-
const found = targetIntf.methods.find((m) => m.name ===
|
|
9969
|
+
const found = targetIntf.methods.find((m) => m.name === entry.targetMethod);
|
|
9835
9970
|
if (found) {
|
|
9836
9971
|
targetMethodSpec = found;
|
|
9837
9972
|
break;
|
|
@@ -9841,19 +9976,19 @@ var init_narrative_target_references = __esm({
|
|
|
9841
9976
|
ctx.addIssue(
|
|
9842
9977
|
"error",
|
|
9843
9978
|
"INVALID_TARGET_METHOD_REFERENCE",
|
|
9844
|
-
`Method "${implMethod.name}" in implementation "${impl.id}" ${verb} method "${
|
|
9979
|
+
`Method "${implMethod.name}" in implementation "${impl.id}" ${verb} method "${entry.targetMethod}" on component "${target}" which is not defined on any of its interfaces${where}.`,
|
|
9845
9980
|
impl.id,
|
|
9846
9981
|
isDraftCtx || ctx.isComponentDraft(target)
|
|
9847
9982
|
);
|
|
9848
9983
|
continue;
|
|
9849
9984
|
}
|
|
9850
9985
|
const declared = new Set(targetMethodSpec.guarantees ?? []);
|
|
9851
|
-
for (const g of
|
|
9986
|
+
for (const g of entry.assertsGuarantees ?? []) {
|
|
9852
9987
|
if (!declared.has(g)) {
|
|
9853
9988
|
ctx.addIssue(
|
|
9854
9989
|
"warning",
|
|
9855
9990
|
"NARRATIVE_SEMANTIC_UNBACKED",
|
|
9856
|
-
`Step ${
|
|
9991
|
+
`Step ${entry.stepNumber} of "${implMethod.name}" in implementation "${impl.id}" explicitly asserts guarantee "${g}", but the method it calls \u2014 "${entry.targetMethod}" on "${target}" \u2014 does not list "${g}" among its L3 contract guarantees. Declare it on that method (and ensure its shape can deliver it), or revise the narrative.`,
|
|
9857
9992
|
impl.id,
|
|
9858
9993
|
isDraftCtx || ctx.isComponentDraft(target)
|
|
9859
9994
|
);
|
|
@@ -12544,15 +12679,11 @@ function walk(ctx, seeds, options = {}) {
|
|
|
12544
12679
|
if (edge.methodName) enqueueMethod(edge.compId, edge.methodName);
|
|
12545
12680
|
}
|
|
12546
12681
|
}
|
|
12547
|
-
|
|
12548
|
-
const
|
|
12549
|
-
if (
|
|
12550
|
-
|
|
12551
|
-
|
|
12552
|
-
reachComponent(depId);
|
|
12553
|
-
enqueueAllMethods(depId);
|
|
12554
|
-
}
|
|
12555
|
-
}
|
|
12682
|
+
for (const ref of methodImpl.calls ?? []) {
|
|
12683
|
+
const parsed = parseDeclaredCall(ref);
|
|
12684
|
+
if (!parsed || !ctx.componentMap.has(parsed.compId)) continue;
|
|
12685
|
+
reachComponent(parsed.compId);
|
|
12686
|
+
enqueueMethod(parsed.compId, parsed.methodName);
|
|
12556
12687
|
}
|
|
12557
12688
|
}
|
|
12558
12689
|
return {
|
|
@@ -12617,7 +12748,9 @@ var init_unused_detection = __esm({
|
|
|
12617
12748
|
"INVOKED_BY_REDUNDANT",
|
|
12618
12749
|
`Method "${m.name}" on component "${intf.component}" declares invokedBy (${m.invokedBy.kind}), but the internal narrative walk already reaches it \u2014 the declaration is stale; remove it.`,
|
|
12619
12750
|
intf.id,
|
|
12620
|
-
isDraftCtx
|
|
12751
|
+
isDraftCtx,
|
|
12752
|
+
void 0,
|
|
12753
|
+
{ at: m.name }
|
|
12621
12754
|
);
|
|
12622
12755
|
}
|
|
12623
12756
|
}
|
|
@@ -12645,7 +12778,11 @@ var init_unused_detection = __esm({
|
|
|
12645
12778
|
"UNUSED_METHOD",
|
|
12646
12779
|
`Method "${m.name}" on component "${comp.id}" is defined but never called by any narrative step.`,
|
|
12647
12780
|
intf.id,
|
|
12648
|
-
isDraftCtx
|
|
12781
|
+
isDraftCtx,
|
|
12782
|
+
void 0,
|
|
12783
|
+
// The site is the method, so an allow covers exactly the one it
|
|
12784
|
+
// names - never its neighbours on the same interface.
|
|
12785
|
+
{ at: m.name }
|
|
12649
12786
|
);
|
|
12650
12787
|
}
|
|
12651
12788
|
}
|
|
@@ -13736,7 +13873,13 @@ var init_dependency_conformance = __esm({
|
|
|
13736
13873
|
"UNDECLARED_DEPENDENCY",
|
|
13737
13874
|
`"${fromPath}" (realizing ${fromComponents.map((c) => c.id).join(", ")}) imports "${toPath}" (realizing ${toComponents.map((c) => c.id).join(", ")}) but no declared dependsOn/owns edge justifies it \u2014 declare the collaboration on the component that actually uses it, or route the cross-subsystem hop through the target's published surface.`,
|
|
13738
13875
|
realization.implementationsAt(fromPath)[0]?.id,
|
|
13739
|
-
draftAt(fromPath) || draftAt(toPath)
|
|
13876
|
+
draftAt(fromPath) || draftAt(toPath),
|
|
13877
|
+
void 0,
|
|
13878
|
+
// One import edge, one indivisible fact — but one implementation
|
|
13879
|
+
// maps many files and many edges, so the EDGE is the site, not the
|
|
13880
|
+
// spec. Without it a single allow on the spec covers every crossing
|
|
13881
|
+
// that file ever grows.
|
|
13882
|
+
{ at: `${fromPath} -> ${toPath}` }
|
|
13740
13883
|
);
|
|
13741
13884
|
}
|
|
13742
13885
|
}
|
|
@@ -13763,7 +13906,12 @@ var init_dependency_conformance = __esm({
|
|
|
13763
13906
|
"UNREALIZED_DEPENDENCY",
|
|
13764
13907
|
`Component "${component.id}" declares ${relation} "${targetId}", but no runtime import connects their source files (${fromFiles.join(", ")} \u219B ${crossSubsystem ? `subsystem ${target.subsystem}` : filesOf(targetId).join(", ")}) \u2014 either the collaboration is wired indirectly (DI) or the declared edge is stale.`,
|
|
13765
13908
|
impls[0]?.id ?? component.id,
|
|
13766
|
-
draft
|
|
13909
|
+
draft,
|
|
13910
|
+
void 0,
|
|
13911
|
+
// One declared edge, one indivisible fact. A component declares many,
|
|
13912
|
+
// and they all anchor on its one implementation spec, so the EDGE is
|
|
13913
|
+
// the site.
|
|
13914
|
+
{ at: `${component.id} -> ${targetId}` }
|
|
13767
13915
|
);
|
|
13768
13916
|
}
|
|
13769
13917
|
}
|
|
@@ -14122,7 +14270,7 @@ var init_carried_debt = __esm({
|
|
|
14122
14270
|
"use strict";
|
|
14123
14271
|
carriedDebtRule = {
|
|
14124
14272
|
name: "carried-debt",
|
|
14125
|
-
description: "The conformance debt register (`rules.conformance.carried`) holds the code\u2194spec findings this tree carries as declared, classified debt \u2014 `unclaimed`'s one-way shape, for findings about code a spec does name. It is not a second lint.allow: an allow says a finding is wrong here by design, an entry here says the finding is right and unpaid, and each entry states which of the three kinds it is (drift: spec and code disagree; undecided: the fix waits on a modelling decision nobody has taken; unreadable: the analysis cannot follow the shape the code is written in) and why. Only a code a rule declares CARRYABLE may appear, and naming any other is an error, so the register can never widen into general-purpose suppression. An entry carries a finding only when it lists EVERY unit that finding reports, so an aggregating finding cannot grow behind it; an entry that matched nothing, that duplicates another, that names a spec the tree does not hold, or that lists a unit no longer reported is STALE_CARRIED_FINDING, so the register only shrinks. The audit is silent in a run that could not have fired the findings at all: one that built no code model, the specs outside a scoped run, and a code the project switched off.",
|
|
14273
|
+
description: "The conformance debt register (`rules.conformance.carried`) holds the code\u2194spec findings this tree carries as declared, classified debt \u2014 `unclaimed`'s one-way shape, for findings about code a spec does name. It is not a second lint.allow: an allow says a finding is wrong here by design, an entry here says the finding is right and unpaid, and each entry states which of the three kinds it is (drift: spec and code disagree; undecided: the fix waits on a modelling decision nobody has taken; unreadable: the analysis cannot follow the shape the code is written in) and why. Only a code a rule declares CARRYABLE may appear, and naming any other is an error, so the register can never widen into general-purpose suppression. An entry carries a finding only when it lists EVERY unit that finding reports, so an aggregating finding cannot grow behind it; an entry that matched nothing, that duplicates another, that names a spec the tree does not hold, or that lists a unit no longer reported is STALE_CARRIED_FINDING, so the register only shrinks. The audit is silent in a run that could not have fired the findings at all: one that built no code model, the specs outside a scoped run, and a code the project switched off. Nothing can check a reason for TRUTH \u2014 a stale entry is one that stopped applying, never one that still applies for a reason that has become false \u2014 so a reason group may declare its own classification provisional (`revisit`: what would settle it), and every run counts those findings beside the kind totals.",
|
|
14126
14274
|
codes: [
|
|
14127
14275
|
{ code: "UNCARRYABLE_FINDING", defaultSeverity: "error", summary: "The conformance debt register names an issue code no rule emits, or one no rule declares carryable \u2014 the register holds measured code\u2194spec debt, never a design rule somebody wants quiet" },
|
|
14128
14276
|
{ code: "STALE_CARRIED_FINDING", defaultSeverity: "warning", summary: "An entry of the conformance debt register carries a finding that no longer fires, or lists a unit that finding no longer reports \u2014 delete it, the register only shrinks" }
|
|
@@ -16496,12 +16644,20 @@ function buildRuleContext(opts) {
|
|
|
16496
16644
|
const allowLookup = /* @__PURE__ */ new Map();
|
|
16497
16645
|
const collectAllows = (specId, lint) => {
|
|
16498
16646
|
for (const a of lint?.allow ?? []) {
|
|
16499
|
-
const entry = { specId, code: a.code, reason: a.reason, used: false };
|
|
16647
|
+
const entry = { specId, code: a.code, at: a.at, covers: a.covers, reason: a.reason, used: false };
|
|
16500
16648
|
lintAllows.push(entry);
|
|
16501
16649
|
if (!allowLookup.has(specId)) allowLookup.set(specId, /* @__PURE__ */ new Map());
|
|
16502
|
-
allowLookup.get(specId)
|
|
16650
|
+
const forSpec = allowLookup.get(specId);
|
|
16651
|
+
const forCode = forSpec.get(a.code) ?? [];
|
|
16652
|
+
forCode.push(entry);
|
|
16653
|
+
forSpec.set(a.code, forCode);
|
|
16503
16654
|
}
|
|
16504
16655
|
};
|
|
16656
|
+
const sitesSeen = /* @__PURE__ */ new Map();
|
|
16657
|
+
const sitesReported = (specId, code) => {
|
|
16658
|
+
const seen = sitesSeen.get(`${specId}\0${code}`);
|
|
16659
|
+
return { sites: [...seen?.sites ?? []], unsited: seen?.unsited ?? false };
|
|
16660
|
+
};
|
|
16505
16661
|
for (const s of subsystems) collectAllows(s.id, s.lint);
|
|
16506
16662
|
for (const c of components) collectAllows(c.id, c.lint);
|
|
16507
16663
|
for (const i of interfaces) collectAllows(i.id, i.lint);
|
|
@@ -16538,15 +16694,29 @@ function buildRuleContext(opts) {
|
|
|
16538
16694
|
}
|
|
16539
16695
|
const severity = getRuleSeverity(code, defaultSeverity, isDraftContext, owner);
|
|
16540
16696
|
if (severity === "off") return;
|
|
16697
|
+
let text2 = message;
|
|
16698
|
+
if (specId) {
|
|
16699
|
+
const key = `${specId}\0${code}`;
|
|
16700
|
+
let seen = sitesSeen.get(key);
|
|
16701
|
+
if (!seen) sitesSeen.set(key, seen = { sites: /* @__PURE__ */ new Set(), unsited: false });
|
|
16702
|
+
if (parts) seen.sites.add(parts.at);
|
|
16703
|
+
else seen.unsited = true;
|
|
16704
|
+
}
|
|
16705
|
+
let allowClaimed = false;
|
|
16541
16706
|
if (specId) {
|
|
16542
|
-
const allow = allowLookup.get(specId)?.get(code);
|
|
16707
|
+
const allow = (allowLookup.get(specId)?.get(code) ?? []).find((a) => parts ? a.at === parts.at : a.at === void 0);
|
|
16543
16708
|
if (allow) {
|
|
16544
16709
|
allow.used = true;
|
|
16545
|
-
|
|
16710
|
+
allowClaimed = true;
|
|
16711
|
+
const grew = (parts?.covers ?? []).filter((unit) => !(allow.covers ?? []).includes(unit));
|
|
16712
|
+
if (grew.length === 0) {
|
|
16713
|
+
if (severity === "warning") return;
|
|
16714
|
+
} else {
|
|
16715
|
+
text2 = `${text2} A lint.allow covers this site, but not ${grew.length} part(s) of it \u2014 ${grew.map((u) => `"${u}"`).join("; ")} ${grew.length === 1 ? "is" : "are"} new. Decide on them: add them to the allow's \`covers\` with a reason that is actually true, or fix them.`;
|
|
16716
|
+
}
|
|
16546
16717
|
}
|
|
16547
16718
|
}
|
|
16548
|
-
|
|
16549
|
-
if (specId && parts && severity === "warning") {
|
|
16719
|
+
if (specId && parts && severity === "warning" && !allowClaimed) {
|
|
16550
16720
|
const entry = carriedLookup.get(carriedKey(specId, code, parts.at));
|
|
16551
16721
|
if (entry) {
|
|
16552
16722
|
entry.fired = true;
|
|
@@ -16639,6 +16809,7 @@ function buildRuleContext(opts) {
|
|
|
16639
16809
|
codeModel: opts.codeModel ?? emptyCodeModel(),
|
|
16640
16810
|
roundTripIssues: opts.roundTripIssues,
|
|
16641
16811
|
lintAllows,
|
|
16812
|
+
sitesReported,
|
|
16642
16813
|
knownIssueCodes: opts.knownIssueCodes,
|
|
16643
16814
|
carriedFindings,
|
|
16644
16815
|
carryableIssueCodes: opts.carryableIssueCodes ?? /* @__PURE__ */ new Set(),
|
|
@@ -24400,9 +24571,11 @@ function identityKeyOf(field, item) {
|
|
|
24400
24571
|
return `${String(o.topic)} ${o.event === void 0 ? "" : String(o.event)}`;
|
|
24401
24572
|
case "trustedLinks":
|
|
24402
24573
|
return str(o.subsystem);
|
|
24574
|
+
// lint.allow: an allow is one decision about one OCCURRENCE, so several
|
|
24575
|
+
// may share a code on one spec, each naming its own site. Keyed by code
|
|
24576
|
+
// alone a delta for one site would overwrite its neighbours.
|
|
24403
24577
|
case "allow":
|
|
24404
|
-
return
|
|
24405
|
-
// lint.allow
|
|
24578
|
+
return `${String(o.code)} ${o.at === void 0 ? "" : String(o.at)}`;
|
|
24406
24579
|
case "findings":
|
|
24407
24580
|
return str(o.code);
|
|
24408
24581
|
// an interface method's findings
|
|
@@ -24490,6 +24663,7 @@ function identityFieldsOf(field) {
|
|
|
24490
24663
|
case "trustedLinks":
|
|
24491
24664
|
return ["subsystem"];
|
|
24492
24665
|
case "allow":
|
|
24666
|
+
return ["code", "at"];
|
|
24493
24667
|
case "findings":
|
|
24494
24668
|
return ["code"];
|
|
24495
24669
|
case "invariants":
|
|
@@ -28620,7 +28794,8 @@ function createMcpServer(options = {}) {
|
|
|
28620
28794
|
conformance: conformanceEnum.optional().describe("Conformance tier for this method (overrides the spec default)"),
|
|
28621
28795
|
symbol: import_zod11.z.string().optional().describe("Code-level name realizing this contract method in the sourcePath file, when it legitimately differs from the intent-language contract name (e.g. put realized by saveSnapshot)"),
|
|
28622
28796
|
ext: import_zod11.z.record(import_zod11.z.unknown()).optional().describe("Opaque pack/tool extension data for this method (namespaced keys) \u2014 preserved verbatim"),
|
|
28623
|
-
narrative: import_zod11.z.array(narrativeStepInput).optional()
|
|
28797
|
+
narrative: import_zod11.z.array(narrativeStepInput).optional(),
|
|
28798
|
+
calls: import_zod11.z.array(import_zod11.z.string().min(1)).optional().describe('The calls this method makes, when its narrative does not show them (detail: intent, or calls-only with no steps authored) \u2014 each as "<component>.<method>", the spelling the debt register and lint allows name a unit with. The narrative walk takes exactly these edges and no others, so an undeclared collaborator is NOT reached: this is what keeps unused-detection honest at a lower detail dial. Each entry is validated like a call step (the component resolves, this component declares it, the method is on its contract). Refused beside a non-empty narrative \u2014 add the call step there instead')
|
|
28624
28799
|
};
|
|
28625
28800
|
const implMethodInputFields = Object.keys(implMethodShape);
|
|
28626
28801
|
const implInput = {
|
|
@@ -28641,7 +28816,7 @@ function createMcpServer(options = {}) {
|
|
|
28641
28816
|
server,
|
|
28642
28817
|
"sdd_write_narrative",
|
|
28643
28818
|
{
|
|
28644
|
-
description: "Write L4 Concrete Implementation spec containing L5 method narratives. Narratives are a FLAT ordered step list; flow steps (branch/switch/loop/try/parallel/jump/return/throw) jump by step number \u2014 blocks are just skipped regions. Steps may declare a `label` anchor, and every jump field has a *Label twin (toLabel, onTrueLabel, endLabel, \u2026) resolved to step numbers at write time \u2014 prefer labels over hand-counted numbers; an unresolvable label rejects the write. Detail dial per method: full (narrative required) | calls-only (call choreography suffices) | intent (prose instead of steps); omitted = stereotype default (Portal/Observer/Adapter: calls-only, Store/Index/Registry: intent, else full). Conformance dial per method or spec: declared | anchored | off \u2014 how strictly structural conformance requires contract methods to be realized in their source file (omitted = Portal: anchored, else declared). A method whose body lives in its own file names it in the method's sourcePath; the implementation's sourcePath is the default for every method that names none. Re-authoring an existing id REPLACES the method list: a method left out of the input is REMOVED together with its narrative (and reported); spec-level lint/ext are carried forward, and the stored status is kept unless this input states a higher one. The answer carries a write receipt as structured content beside the sentence \u2014 the status written, whether a spec already held the id, and the notices a restatement raised, each as its own entry.",
|
|
28819
|
+
description: "Write L4 Concrete Implementation spec containing L5 method narratives. Narratives are a FLAT ordered step list; flow steps (branch/switch/loop/try/parallel/jump/return/throw) jump by step number \u2014 blocks are just skipped regions. Steps may declare a `label` anchor, and every jump field has a *Label twin (toLabel, onTrueLabel, endLabel, \u2026) resolved to step numbers at write time \u2014 prefer labels over hand-counted numbers; an unresolvable label rejects the write. Detail dial per method: full (narrative required) | calls-only (call choreography suffices) | intent (prose instead of steps); omitted = stereotype default (Portal/Observer/Adapter: calls-only, Store/Index/Registry: intent, else full). A method whose narrative shows no steps declares the calls it makes in `calls` (one \"<component>.<method>\" each): the reachability walk takes exactly those edges and no others, so an intent-level method that reaches a collaborator must name it or that collaborator is reported unused. Conformance dial per method or spec: declared | anchored | off \u2014 how strictly structural conformance requires contract methods to be realized in their source file (omitted = Portal: anchored, else declared). A method whose body lives in its own file names it in the method's sourcePath; the implementation's sourcePath is the default for every method that names none. Re-authoring an existing id REPLACES the method list: a method left out of the input is REMOVED together with its narrative (and reported); spec-level lint/ext are carried forward, and the stored status is kept unless this input states a higher one. The answer carries a write receipt as structured content beside the sentence \u2014 the status written, whether a spec already held the id, and the notices a restatement raised, each as its own entry.",
|
|
28645
28820
|
inputSchema: implInput,
|
|
28646
28821
|
outputSchema: specWriteReceiptOutput
|
|
28647
28822
|
},
|
|
@@ -28964,7 +29139,7 @@ function createMcpServer(options = {}) {
|
|
|
28964
29139
|
inputSchema: {
|
|
28965
29140
|
kind: import_zod11.z.enum(["system", "subsystem", "component", "interface", "implementation", "type"]).describe("The spec kind to update (system = the singleton L0 \u2014 vision, boundaries, globalRequirements, databases, and publicInterfaces: the project gateway surface, each entry {id, name, subsystem, component, type, details, audience: project|department|instance|partner|external}; id is informational)"),
|
|
28966
29141
|
id: import_zod11.z.string().describe("The ID of the spec to update (namespaced if needed)"),
|
|
28967
|
-
delta: import_zod11.z.record(import_zod11.z.any()).describe(`The partial fields to merge into the spec. ARRAYS UPSERT, they do not replace: an array whose elements carry an identity is merged element-by-element, so a delta naming ONE element leaves the others intact. Identity is "name" or "id" by default, and per field: dispatch by "capability", lifecycle by phase+component+method, emits/subscribesTo by topic+event, trustedLinks by "subsystem", invariants and patterns by "id", lint.allow by "code", an interface method's findings by "code", boundaries by "name", globalRequirements by "description", switch cases by "value", try catches by "error". Identity merging applies at EVERY depth, including an array INSIDE an element (a method's params, a step's catches). Add "action: 'delete'" (or "remove: true") alongside that identity to REMOVE an element \u2014 including a stale lint allow. Arrays of plain STRINGS (owns, dependsOn, guarantees) carry no per-element identity and are replaced wholesale; pass [] to clear any array outright. To REMOVE an optional field entirely, list it in "unset": e.g. {"unset": ["basePath", "variant"]} \u2014 passing null/undefined means "no change" (they are skipped), and writing "" would leave the field present but empty, which is a different and usually wrong spec. Unsetting a required field is refused by schema validation, which names it. For narrative steps, match by "stepNumber" and use "action: 'insert'" (shifts subsequent steps up) or "action: 'delete'" (shifts subsequent steps down and removes it). Step entries apply in ASCENDING stepNumber order, each against the numbering the earlier entries of the SAME delta left behind \u2014 delete step 3 and step 7 becomes step 6 \u2014 so prefer labels, and restate the step's "label" or "description" on a delete to have it checked against the step actually addressed. Renumbering RELOCATES every flow jump field (onTrueStep/onFalseStep/cases.step/defaultStep/endStep/catches.step/finallyStep/toStep) in the same narrative. A delete is REJECTED when the narrative has no such step, when a jump still targets it (retarget the referrers first), when a restated label/description does not match, or when it is a loop/try/parallel header whose body would be left standing (retype the header first to dissolve the region, then delete it). Changing a step's "type" REBUILDS it for the new type: its description and label are kept and every field the new type cannot carry is dropped (returned as a NOTICE); a delta that retypes AND sets such a field is refused. A step delta is also refused when it carries a marker the merge does not recognise: a non-boolean "remove", an "action" that is neither "insert" nor "delete", a "captureJumps" outside an insert, or no "stepNumber" to address. Inserting AT a jump target relocates those jumps past the inserted step by default (a NOTICE is returned) \u2014 add "captureJumps": true on the inserted step to retarget entry jumps onto it (loop/try endStep region tails always relocate with the body and are never captured). Every jump field has a "*Label" twin (toLabel, onFalseLabel, endLabel, \u2026, and "label" on a cases/catches entry) resolved against step labels AFTER the merge, so a delta may anchor on a label only pre-existing steps carry; a label the delta supplies REPLACES the stored number it twins, while setting the number and its label together in one delta is refused as a contradiction. Reference ids in deltas may use LOCAL names \u2014 they are qualified against the spec's namespace exactly as the loader would. Per-spec lint suppression: set "lint: { allow: [{ code, reason }] }" to silence a WARNING code on this spec only (errors always surface; stale allows are flagged). This delta is deliberately OPEN below its top level \u2014 the shapes nest further than a schema here should restate \u2014 so a key that is not a field at its depth is not refused, it is NAMED BACK under NO EFFECT in the answer, together with any value the spec already held and any "unset" that removed nothing. Read that list: it is where a nested typo shows up.`),
|
|
29142
|
+
delta: import_zod11.z.record(import_zod11.z.any()).describe(`The partial fields to merge into the spec. ARRAYS UPSERT, they do not replace: an array whose elements carry an identity is merged element-by-element, so a delta naming ONE element leaves the others intact. Identity is "name" or "id" by default, and per field: dispatch by "capability", lifecycle by phase+component+method, emits/subscribesTo by topic+event, trustedLinks by "subsystem", invariants and patterns by "id", lint.allow by "code" AND "at" (an allow covers one occurrence, so several may share a code on one spec), an interface method's findings by "code", boundaries by "name", globalRequirements by "description", switch cases by "value", try catches by "error". Identity merging applies at EVERY depth, including an array INSIDE an element (a method's params, a step's catches). Add "action: 'delete'" (or "remove: true") alongside that identity to REMOVE an element \u2014 including a stale lint allow. Arrays of plain STRINGS (owns, dependsOn, guarantees) carry no per-element identity and are replaced wholesale; pass [] to clear any array outright. To REMOVE an optional field entirely, list it in "unset": e.g. {"unset": ["basePath", "variant"]} \u2014 passing null/undefined means "no change" (they are skipped), and writing "" would leave the field present but empty, which is a different and usually wrong spec. Unsetting a required field is refused by schema validation, which names it. For narrative steps, match by "stepNumber" and use "action: 'insert'" (shifts subsequent steps up) or "action: 'delete'" (shifts subsequent steps down and removes it). Step entries apply in ASCENDING stepNumber order, each against the numbering the earlier entries of the SAME delta left behind \u2014 delete step 3 and step 7 becomes step 6 \u2014 so prefer labels, and restate the step's "label" or "description" on a delete to have it checked against the step actually addressed. Renumbering RELOCATES every flow jump field (onTrueStep/onFalseStep/cases.step/defaultStep/endStep/catches.step/finallyStep/toStep) in the same narrative. A delete is REJECTED when the narrative has no such step, when a jump still targets it (retarget the referrers first), when a restated label/description does not match, or when it is a loop/try/parallel header whose body would be left standing (retype the header first to dissolve the region, then delete it). Changing a step's "type" REBUILDS it for the new type: its description and label are kept and every field the new type cannot carry is dropped (returned as a NOTICE); a delta that retypes AND sets such a field is refused. A step delta is also refused when it carries a marker the merge does not recognise: a non-boolean "remove", an "action" that is neither "insert" nor "delete", a "captureJumps" outside an insert, or no "stepNumber" to address. Inserting AT a jump target relocates those jumps past the inserted step by default (a NOTICE is returned) \u2014 add "captureJumps": true on the inserted step to retarget entry jumps onto it (loop/try endStep region tails always relocate with the body and are never captured). Every jump field has a "*Label" twin (toLabel, onFalseLabel, endLabel, \u2026, and "label" on a cases/catches entry) resolved against step labels AFTER the merge, so a delta may anchor on a label only pre-existing steps carry; a label the delta supplies REPLACES the stored number it twins, while setting the number and its label together in one delta is refused as a contradiction. Reference ids in deltas may use LOCAL names \u2014 they are qualified against the spec's namespace exactly as the loader would. Per-spec lint suppression: set "lint: { allow: [{ code, at, covers, reason }] }" to silence a WARNING code on this spec only (errors always surface; stale allows are flagged). An allow covers EXACTLY the occurrence it names: "at" is the site the finding names (a contract method, an import edge "from -> to", a declared edge "component -> target") and is REQUIRED for a code whose findings report one, while a finding that reports no site is covered only by an allow that names none; "covers" lists the units an aggregating finding reports, and the allow silences it only when every one is listed. This delta is deliberately OPEN below its top level \u2014 the shapes nest further than a schema here should restate \u2014 so a key that is not a field at its depth is not refused, it is NAMED BACK under NO EFFECT in the answer, together with any value the spec already held and any "unset" that removed nothing. Read that list: it is where a nested typo shows up.`),
|
|
28968
29143
|
dryRun: import_zod11.z.boolean().optional().describe("Ask what this delta WOULD do instead of doing it. The whole write runs, the candidate gate included, and the answer is the change report it would have produced \u2014 marked DRY RUN, with nothing stamped and not one byte of the stored file moved. Use it before a delta that renumbers a long narrative.")
|
|
28969
29144
|
},
|
|
28970
29145
|
outputSchema: specChangeReportOutput
|
|
@@ -31008,6 +31183,20 @@ init_loader();
|
|
|
31008
31183
|
init_errors();
|
|
31009
31184
|
init_core();
|
|
31010
31185
|
init_validation();
|
|
31186
|
+
function carriedDebtSummary(carried) {
|
|
31187
|
+
if (!carried || carried.length === 0) return null;
|
|
31188
|
+
const findings = carried.flatMap((g) => g.findings ?? []);
|
|
31189
|
+
const units = findings.reduce((n, f) => n + (f.covers?.length ?? 1), 0);
|
|
31190
|
+
const perKind = (kind) => carried.filter((g) => g.kind === kind).reduce((n, g) => n + (g.findings?.length ?? 0), 0);
|
|
31191
|
+
const provisional = carried.filter((g) => g.revisit);
|
|
31192
|
+
const unsettled = provisional.reduce((n, g) => n + (g.findings?.length ?? 0), 0);
|
|
31193
|
+
return {
|
|
31194
|
+
line: `Conformance debt register: ${findings.length} finding(s) over ${units} unit(s) carried \u2014 ${perKind("drift")} drift, ${perKind("undecided")} undecided, ${perKind("unreadable")} unreadable (\`rules.conformance.carried\`). Drift and undecided are owed; unreadable is what the analysis cannot follow.` + (unsettled > 0 ? ` ${unsettled} finding(s) in ${provisional.length} group(s) are marked for re-evaluation \u2014 the classification is provisional, not the debt.` : ""),
|
|
31195
|
+
revisits: provisional.map(
|
|
31196
|
+
(g) => ` re-evaluate (${g.kind}, ${g.findings?.length ?? 0} finding(s)): ${g.revisit}`
|
|
31197
|
+
)
|
|
31198
|
+
};
|
|
31199
|
+
}
|
|
31011
31200
|
function isCiDraftWaivable(issue2) {
|
|
31012
31201
|
if (issue2.severity !== "warning") return false;
|
|
31013
31202
|
if (issue2.code === "DRAFT_SUBSYSTEM_WARNING") return true;
|
|
@@ -31123,15 +31312,11 @@ async function runValidate(options = {}) {
|
|
|
31123
31312
|
}
|
|
31124
31313
|
}
|
|
31125
31314
|
}
|
|
31126
|
-
const
|
|
31127
|
-
if (
|
|
31128
|
-
const findings = carried.flatMap((g) => g.findings ?? []);
|
|
31129
|
-
const units = findings.reduce((n, f) => n + (f.covers?.length ?? 1), 0);
|
|
31130
|
-
const perKind = (kind) => carried.filter((g) => g.kind === kind).reduce((n, g) => n + (g.findings?.length ?? 0), 0);
|
|
31315
|
+
const summary = carriedDebtSummary(projectConfig.rules?.conformance?.carried);
|
|
31316
|
+
if (summary) {
|
|
31131
31317
|
logger.blank();
|
|
31132
|
-
logger.info(import_chalk7.default.yellow(
|
|
31133
|
-
|
|
31134
|
-
));
|
|
31318
|
+
logger.info(import_chalk7.default.yellow(summary.line));
|
|
31319
|
+
for (const note of summary.revisits) logger.info(import_chalk7.default.gray(note));
|
|
31135
31320
|
}
|
|
31136
31321
|
logger.blank();
|
|
31137
31322
|
if (options.ci && waivedWarnings > 0) {
|