hcifootprint 0.2.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.
Files changed (87) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +524 -0
  3. package/dist/atom/types.d.ts +618 -0
  4. package/dist/atom/types.d.ts.map +1 -0
  5. package/dist/atom/types.js +2 -0
  6. package/dist/atom/types.js.map +1 -0
  7. package/dist/graph/builder.d.ts +31 -0
  8. package/dist/graph/builder.d.ts.map +1 -0
  9. package/dist/graph/builder.js +165 -0
  10. package/dist/graph/builder.js.map +1 -0
  11. package/dist/graph/guards.d.ts +31 -0
  12. package/dist/graph/guards.d.ts.map +1 -0
  13. package/dist/graph/guards.js +94 -0
  14. package/dist/graph/guards.js.map +1 -0
  15. package/dist/graph/skill-deps.d.ts +20 -0
  16. package/dist/graph/skill-deps.d.ts.map +1 -0
  17. package/dist/graph/skill-deps.js +15 -0
  18. package/dist/graph/skill-deps.js.map +1 -0
  19. package/dist/index.d.ts +49 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +42 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/mcp.d.ts +16 -0
  24. package/dist/mcp.d.ts.map +1 -0
  25. package/dist/mcp.js +15 -0
  26. package/dist/mcp.js.map +1 -0
  27. package/dist/presence/presence.d.ts +50 -0
  28. package/dist/presence/presence.d.ts.map +1 -0
  29. package/dist/presence/presence.js +126 -0
  30. package/dist/presence/presence.js.map +1 -0
  31. package/dist/registry/registry.d.ts +53 -0
  32. package/dist/registry/registry.d.ts.map +1 -0
  33. package/dist/registry/registry.js +76 -0
  34. package/dist/registry/registry.js.map +1 -0
  35. package/dist/serve/mcp-server.d.ts +40 -0
  36. package/dist/serve/mcp-server.d.ts.map +1 -0
  37. package/dist/serve/mcp-server.js +72 -0
  38. package/dist/serve/mcp-server.js.map +1 -0
  39. package/dist/serve/mcp.d.ts +12 -0
  40. package/dist/serve/mcp.d.ts.map +1 -0
  41. package/dist/serve/mcp.js +62 -0
  42. package/dist/serve/mcp.js.map +1 -0
  43. package/dist/serve/modes.d.ts +66 -0
  44. package/dist/serve/modes.d.ts.map +1 -0
  45. package/dist/serve/modes.js +347 -0
  46. package/dist/serve/modes.js.map +1 -0
  47. package/dist/testing/harness.d.ts +156 -0
  48. package/dist/testing/harness.d.ts.map +1 -0
  49. package/dist/testing/harness.js +416 -0
  50. package/dist/testing/harness.js.map +1 -0
  51. package/dist/testing/index.d.ts +35 -0
  52. package/dist/testing/index.d.ts.map +1 -0
  53. package/dist/testing/index.js +32 -0
  54. package/dist/testing/index.js.map +1 -0
  55. package/dist/testing/lint.d.ts +20 -0
  56. package/dist/testing/lint.d.ts.map +1 -0
  57. package/dist/testing/lint.js +18 -0
  58. package/dist/testing/lint.js.map +1 -0
  59. package/dist/testing/model/check.d.ts +30 -0
  60. package/dist/testing/model/check.d.ts.map +1 -0
  61. package/dist/testing/model/check.js +89 -0
  62. package/dist/testing/model/check.js.map +1 -0
  63. package/dist/testing/model/lint.d.ts +54 -0
  64. package/dist/testing/model/lint.d.ts.map +1 -0
  65. package/dist/testing/model/lint.js +235 -0
  66. package/dist/testing/model/lint.js.map +1 -0
  67. package/dist/testing/model/satisfiable.d.ts +25 -0
  68. package/dist/testing/model/satisfiable.d.ts.map +1 -0
  69. package/dist/testing/model/satisfiable.js +101 -0
  70. package/dist/testing/model/satisfiable.js.map +1 -0
  71. package/dist/traverse/nav-session.d.ts +108 -0
  72. package/dist/traverse/nav-session.d.ts.map +1 -0
  73. package/dist/traverse/nav-session.js +637 -0
  74. package/dist/traverse/nav-session.js.map +1 -0
  75. package/dist/traverse/session.d.ts +239 -0
  76. package/dist/traverse/session.d.ts.map +1 -0
  77. package/dist/traverse/session.js +1305 -0
  78. package/dist/traverse/session.js.map +1 -0
  79. package/dist/tree/appmap.d.ts +8 -0
  80. package/dist/tree/appmap.d.ts.map +1 -0
  81. package/dist/tree/appmap.js +259 -0
  82. package/dist/tree/appmap.js.map +1 -0
  83. package/dist/tree/types.d.ts +151 -0
  84. package/dist/tree/types.d.ts.map +1 -0
  85. package/dist/tree/types.js +2 -0
  86. package/dist/tree/types.js.map +1 -0
  87. package/package.json +90 -0
@@ -0,0 +1,89 @@
1
+ /**
2
+ * checkGraph — the one-call health verdict.
3
+ *
4
+ * `lintGraph` returns a flat list of findings; `checkGraph` rolls them into a
5
+ * single answer a consumer can act on without iterating: a boolean `ok`, counts,
6
+ * findings grouped by the kind of drift a frontend dev recognises (control /
7
+ * page / flow), a per-skill feasibility list, and a ready-to-print `summary`.
8
+ * The whole health gate is one line:
9
+ *
10
+ * const health = checkGraph(graph, { initialState });
11
+ * if (!health.ok) { console.error(health.summary); process.exit(1); }
12
+ *
13
+ * It is STATIC and pure (same engine-free module graph as lintGraph — no
14
+ * Session, no footprint engine), so it runs in any CI step. It answers "does the
15
+ * graph hang together?" — NOT "do the real handlers match?" That behavioral
16
+ * question needs your handlers, so it lives in testApp (effectVerified). Keeping
17
+ * them apart is deliberate: a one-call auto-driver cannot know the mid-flow state
18
+ * a real flow sets up, so it would raise false alarms.
19
+ */
20
+ import { lintGraph } from './lint.js';
21
+ const TYPE_OF = {
22
+ 'dangling-guard-key': 'control',
23
+ 'unsatisfiable-guard': 'control',
24
+ 'unreachable-page': 'page',
25
+ 'dead-end-page': 'page',
26
+ 'uncompletable-skill': 'flow',
27
+ 'skill-step-order': 'flow',
28
+ 'skill-step-cycle': 'flow',
29
+ 'unconsumed-write': 'note',
30
+ };
31
+ const TYPE_LABEL = {
32
+ control: 'Control drift (buttons / inputs)',
33
+ page: 'Page drift',
34
+ flow: 'Flow drift (skills)',
35
+ note: 'Advisory notes',
36
+ };
37
+ /** One-call health check: lint + group by drift type + per-skill rollup + a printable summary. */
38
+ export function checkGraph(graph, opts) {
39
+ const findings = lintGraph(graph, opts);
40
+ const byType = { control: [], page: [], flow: [], note: [] };
41
+ for (const finding of findings)
42
+ byType[TYPE_OF[finding.code] ?? 'note'].push(finding);
43
+ const errors = findings.filter((f) => f.severity === 'error').length;
44
+ const warnings = findings.filter((f) => f.severity === 'warning').length;
45
+ const skills = Object.values(graph.spec.skills).map((skill) => {
46
+ const blocking = findings.filter((f) => f.skill === skill.id && f.severity === 'error');
47
+ return {
48
+ id: skill.id,
49
+ feasible: blocking.length === 0,
50
+ blockedOn: [...new Set(blocking.flatMap((f) => f.keys ?? []))],
51
+ };
52
+ });
53
+ const unreachablePages = findings
54
+ .filter((f) => f.code === 'unreachable-page' && f.page)
55
+ .map((f) => f.page);
56
+ const ok = errors === 0;
57
+ return {
58
+ ok,
59
+ errors,
60
+ warnings,
61
+ findings,
62
+ byType,
63
+ skills,
64
+ unreachablePages,
65
+ summary: formatHealth(graph.id, findings, byType, errors, warnings),
66
+ };
67
+ }
68
+ const DRIFT_ORDER = ['control', 'page', 'flow'];
69
+ function formatHealth(graphId, findings, byType, errors, warnings) {
70
+ const notes = byType.note.length;
71
+ const lines = [`Graph health — ${graphId}`];
72
+ if (errors === 0 && warnings === 0) {
73
+ lines.push(` ✓ healthy — no drift (${notes} advisory note${notes === 1 ? '' : 's'})`);
74
+ return lines.join('\n');
75
+ }
76
+ lines.push(` ✗ ${errors} error${errors === 1 ? '' : 's'}, ${warnings} warning${warnings === 1 ? '' : 's'}`);
77
+ for (const type of DRIFT_ORDER) {
78
+ const group = byType[type].filter((f) => f.severity !== 'info');
79
+ if (group.length === 0)
80
+ continue;
81
+ lines.push('', ` ▸ ${TYPE_LABEL[type]}`);
82
+ for (const f of group) {
83
+ const where = f.affordance ?? f.skill ?? f.page ?? '';
84
+ lines.push(` • [${f.severity.toUpperCase()}] ${where}`, ` ${f.message}`, ` → ${f.remedy}`);
85
+ }
86
+ }
87
+ return lines.join('\n');
88
+ }
89
+ //# sourceMappingURL=check.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"check.js","sourceRoot":"","sources":["../../../src/testing/model/check.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,54 @@
1
+ import type { NavigationGraph } from '../../tree/types.js';
2
+ export type LintSeverity = 'error' | 'warning' | 'info';
3
+ export type LintCode = 'dangling-guard-key' | 'unsatisfiable-guard' | 'uncompletable-skill' | 'skill-step-order' | 'skill-step-cycle' | 'unreachable-page' | 'dead-end-page' | 'unconsumed-write';
4
+ export interface LintFinding {
5
+ code: LintCode;
6
+ severity: LintSeverity;
7
+ /** Plain-language statement of what drifted or is wrong. */
8
+ message: string;
9
+ /** The two consumer remedies — surfaced, never dictated. */
10
+ remedy: string;
11
+ /** The action (affordance) the finding is about, when it is action-scoped. */
12
+ affordance?: string;
13
+ /** The skill the finding is about, when skill-scoped. */
14
+ skill?: string;
15
+ /** The page the finding is about, when page-scoped. */
16
+ page?: string;
17
+ /** The state key(s) implicated. */
18
+ keys?: string[];
19
+ }
20
+ export interface LintOptions {
21
+ /**
22
+ * The keys (or a sample object) the app guarantees before any action runs —
23
+ * the initial projected state. Supplying it lets the linter PROMOTE
24
+ * "gated on a key nothing produces" from a warning to an error: with the
25
+ * initial world known, an unproducible key is provably dead.
26
+ */
27
+ initialState?: string[] | Record<string, unknown>;
28
+ /**
29
+ * Keys supplied from OUTSIDE the graph's own writes (a server push, a store
30
+ * the app seeds, a parent app). Listed here they count as producible, so a
31
+ * guard over them is not flagged as dangling.
32
+ */
33
+ externalKeys?: string[];
34
+ /** Which page the app starts on (default: the first declared page). */
35
+ startPage?: string;
36
+ }
37
+ /**
38
+ * Report every stale-logic drift provable from the graph alone. Returns an
39
+ * empty array for a clean graph. Advisory by default; pass initialState +
40
+ * externalKeys to promote provably-dead findings to errors.
41
+ */
42
+ export declare function lintGraph(graph: NavigationGraph, opts?: LintOptions): LintFinding[];
43
+ /** A plain-text report of the findings, most severe first. Empty string when clean. */
44
+ export declare function formatFindings(findings: LintFinding[]): string;
45
+ /**
46
+ * Throw if the graph has stale-logic findings at or above `failOn` (default
47
+ * 'error'). The opt-in CI gate: `expectNoStaleLogic(graph, { initialState })`
48
+ * fails a commit that drifts the graph. Report-by-default stays the norm —
49
+ * call lintGraph directly to inspect without failing.
50
+ */
51
+ export declare function expectNoStaleLogic(graph: NavigationGraph, opts?: LintOptions & {
52
+ failOn?: LintSeverity;
53
+ }): void;
54
+ //# sourceMappingURL=lint.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lint.d.ts","sourceRoot":"","sources":["../../../src/testing/model/lint.ts"],"names":[],"mappings":"AA+BA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAI3D,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;AAExD,MAAM,MAAM,QAAQ,GAChB,oBAAoB,GACpB,qBAAqB,GACrB,qBAAqB,GACrB,kBAAkB,GAClB,kBAAkB,GAClB,kBAAkB,GAClB,eAAe,GACf,kBAAkB,CAAC;AAEvB,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,QAAQ,CAAC;IACf,QAAQ,EAAE,YAAY,CAAC;IACvB,4DAA4D;IAC5D,OAAO,EAAE,MAAM,CAAC;IAChB,4DAA4D;IAC5D,MAAM,EAAE,MAAM,CAAC;IACf,8EAA8E;IAC9E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,yDAAyD;IACzD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uDAAuD;IACvD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mCAAmC;IACnC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClD;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,uEAAuE;IACvE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAmBD;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,eAAe,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,WAAW,EAAE,CAsLnF;AAID,uFAAuF;AACvF,wBAAgB,cAAc,CAAC,QAAQ,EAAE,WAAW,EAAE,GAAG,MAAM,CAS9D;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,eAAe,EACtB,IAAI,CAAC,EAAE,WAAW,GAAG;IAAE,MAAM,CAAC,EAAE,YAAY,CAAA;CAAE,GAC7C,IAAI,CAWN"}
@@ -0,0 +1,235 @@
1
+ import { stepDependencies } from '../../graph/skill-deps.js';
2
+ import { unsatisfiableKeys } from './satisfiable.js';
3
+ const REMEDY_ACTION = 'Two ways to resolve, your call: update the graph so it matches the app (adjust or drop this guard/effect), ' +
4
+ 'or treat it as a sign the app changed by mistake and revert that change. This check only surfaces the drift.';
5
+ const REMEDY_PAGE = 'Two ways to resolve, your call: add an action that reaches/leaves this page (or remove the page if the app dropped it), ' +
6
+ 'or confirm it is reached another way (a deep link the app syncs). This check only surfaces the drift.';
7
+ const REMEDY_WRITE = "If the app genuinely uses this write (for its own UI, not for gating), leave it. If it's a leftover from removed " +
8
+ 'logic, drop it from the graph. Either way, the choice is yours.';
9
+ /** Quote a key list for a message: `a`, `b` and `c`. */
10
+ function list(keys) {
11
+ const quoted = keys.map((k) => `“${k}”`);
12
+ if (quoted.length <= 1)
13
+ return quoted.join('');
14
+ return `${quoted.slice(0, -1).join(', ')} and ${quoted[quoted.length - 1]}`;
15
+ }
16
+ /**
17
+ * Report every stale-logic drift provable from the graph alone. Returns an
18
+ * empty array for a clean graph. Advisory by default; pass initialState +
19
+ * externalKeys to promote provably-dead findings to errors.
20
+ */
21
+ export function lintGraph(graph, opts) {
22
+ const spec = graph.spec;
23
+ const affordances = spec.affordances;
24
+ const affList = Object.values(affordances);
25
+ const findings = [];
26
+ const initialKeys = Array.isArray(opts?.initialState)
27
+ ? opts.initialState
28
+ : opts?.initialState
29
+ ? Object.keys(opts.initialState)
30
+ : [];
31
+ const externalKeys = opts?.externalKeys ?? [];
32
+ // "Grounded" = the consumer told us the world outside the graph's own writes,
33
+ // so an unproducible key is provably dead (error), not merely unproven (warn).
34
+ // Declaring initialState AT ALL grounds it — even `{}` says "nothing is seeded".
35
+ const grounded = opts?.initialState !== undefined || externalKeys.length > 0;
36
+ const writeKeys = new Set();
37
+ for (const aff of affList)
38
+ for (const key of aff.effect?.writes ?? [])
39
+ writeKeys.add(key);
40
+ const producible = new Set([...writeKeys, ...initialKeys, ...externalKeys]);
41
+ // --- 1. dangling guard key: gated on state nothing produces ----------------
42
+ for (const aff of affList) {
43
+ const dangling = Object.keys(aff.guard ?? {}).filter((key) => !producible.has(key));
44
+ if (dangling.length > 0) {
45
+ findings.push({
46
+ code: 'dangling-guard-key',
47
+ severity: grounded ? 'error' : 'warning',
48
+ affordance: aff.id,
49
+ keys: dangling,
50
+ message: grounded
51
+ ? `Action “${aff.id}” is gated on ${list(dangling)}, which nothing produces — no action writes it, and you did not list it in the initial or external state. This action can never become available.`
52
+ : `Action “${aff.id}” is gated on ${list(dangling)}, which no action in the graph writes. If the app seeds it from initial or external state that is fine — declare initialState/externalKeys to confirm; otherwise the gate can never pass.`,
53
+ remedy: REMEDY_ACTION,
54
+ });
55
+ }
56
+ }
57
+ // --- 2. unsatisfiable guard: operators that can never be jointly true ------
58
+ for (const aff of affList) {
59
+ for (const { key, reason } of unsatisfiableKeys(aff.guard)) {
60
+ findings.push({
61
+ code: 'unsatisfiable-guard',
62
+ severity: 'error',
63
+ affordance: aff.id,
64
+ keys: [key],
65
+ message: `Action “${aff.id}” has a guard on “${key}” that can never be true (${reason}) — the control it maps to can never light up.`,
66
+ remedy: REMEDY_ACTION,
67
+ });
68
+ }
69
+ }
70
+ // --- 3/4/5. skill completability, step order, and cycles -------------------
71
+ for (const skill of Object.values(spec.skills)) {
72
+ const steps = skill.steps;
73
+ const producedSoFar = new Set([...initialKeys, ...externalKeys]);
74
+ for (let i = 0; i < steps.length; i++) {
75
+ const stepId = steps[i];
76
+ const aff = affordances[stepId];
77
+ if (!aff)
78
+ continue; // build() guarantees step ids resolve; defensive only
79
+ for (const guardKey of Object.keys(aff.guard ?? {})) {
80
+ if (producedSoFar.has(guardKey))
81
+ continue;
82
+ const laterWriter = steps
83
+ .slice(i + 1)
84
+ .find((other) => (affordances[other]?.effect?.writes ?? []).includes(guardKey));
85
+ if (laterWriter) {
86
+ findings.push({
87
+ code: 'skill-step-order',
88
+ severity: 'warning',
89
+ skill: skill.id,
90
+ affordance: stepId,
91
+ keys: [guardKey],
92
+ message: `In skill “${skill.id}”, step “${stepId}” needs “${guardKey}”, but the step that produces it (“${laterWriter}”) is listed after it. As ordered, this step is blocked when the skill reaches it.`,
93
+ remedy: REMEDY_ACTION,
94
+ });
95
+ }
96
+ else if (!producible.has(guardKey)) {
97
+ findings.push({
98
+ code: 'uncompletable-skill',
99
+ severity: grounded ? 'error' : 'warning',
100
+ skill: skill.id,
101
+ affordance: stepId,
102
+ keys: [guardKey],
103
+ message: `Skill “${skill.id}” can never finish: step “${stepId}” is gated on “${guardKey}”, which nothing produces (no step writes it, and it is not in the initial/external state).`,
104
+ remedy: REMEDY_ACTION,
105
+ });
106
+ }
107
+ }
108
+ for (const written of aff.effect?.writes ?? [])
109
+ producedSoFar.add(written);
110
+ }
111
+ // Order-insensitive reachability from the grounded state: a step becomes
112
+ // runnable once all its guard keys are available, accumulating writes. A key
113
+ // supplied by initialState/externalKeys — or by an earlier step — breaks a
114
+ // structural "cycle" that is not actually a deadlock.
115
+ const runnable = new Set();
116
+ const reachable = new Set([...initialKeys, ...externalKeys]);
117
+ for (let changed = true; changed;) {
118
+ changed = false;
119
+ for (const stepId of steps) {
120
+ if (runnable.has(stepId))
121
+ continue;
122
+ if (Object.keys(affordances[stepId]?.guard ?? {}).every((key) => reachable.has(key))) {
123
+ runnable.add(stepId);
124
+ for (const written of affordances[stepId]?.effect?.writes ?? [])
125
+ reachable.add(written);
126
+ changed = true;
127
+ }
128
+ }
129
+ }
130
+ // A true deadlock: two steps each need state the other writes AND neither is
131
+ // reachable from grounded state on its own.
132
+ for (const stepId of steps) {
133
+ if (runnable.has(stepId))
134
+ continue;
135
+ const deps = stepDependencies(affordances, steps, stepId);
136
+ for (const dep of deps) {
137
+ if (!runnable.has(dep.affordanceId) &&
138
+ stepId < dep.affordanceId && // report each pair once
139
+ stepDependencies(affordances, steps, dep.affordanceId).some((d) => d.affordanceId === stepId)) {
140
+ findings.push({
141
+ code: 'skill-step-cycle',
142
+ severity: 'error',
143
+ skill: skill.id,
144
+ keys: [...new Set(dep.viaKeys)],
145
+ message: `Skill “${skill.id}” has steps “${stepId}” and “${dep.affordanceId}” that each need state the other writes — a dependency cycle, so neither can go first.`,
146
+ remedy: REMEDY_ACTION,
147
+ });
148
+ }
149
+ }
150
+ }
151
+ }
152
+ // --- 6/7. page reachability ------------------------------------------------
153
+ const pageIds = Object.keys(spec.pages);
154
+ const startPage = opts?.startPage ?? pageIds[0];
155
+ const navTargets = new Set();
156
+ for (const aff of affList)
157
+ if (aff.effect?.navigatesTo)
158
+ navTargets.add(aff.effect.navigatesTo);
159
+ for (const pageId of pageIds) {
160
+ if (pageId !== startPage && !navTargets.has(pageId)) {
161
+ findings.push({
162
+ code: 'unreachable-page',
163
+ severity: 'warning',
164
+ page: pageId,
165
+ message: `Page “${pageId}” is not the start page and no action navigates to it — nothing in the graph can reach it.`,
166
+ remedy: REMEDY_PAGE,
167
+ });
168
+ }
169
+ if (pageIds.length > 1) {
170
+ const leaves = affList.some((aff) => aff.on.includes(pageId) && aff.effect?.navigatesTo && aff.effect.navigatesTo !== pageId);
171
+ if (!leaves) {
172
+ findings.push({
173
+ code: 'dead-end-page',
174
+ severity: 'info',
175
+ page: pageId,
176
+ message: `Page “${pageId}” has no action that navigates away — once here, the graph offers no way out.`,
177
+ remedy: REMEDY_PAGE,
178
+ });
179
+ }
180
+ }
181
+ }
182
+ // --- 8. unconsumed write: a declared write no guard ever reads --------------
183
+ const readKeys = new Set();
184
+ for (const aff of affList)
185
+ for (const key of Object.keys(aff.guard ?? {}))
186
+ readKeys.add(key);
187
+ for (const skill of Object.values(spec.skills)) {
188
+ for (const key of Object.keys(skill.precondition ?? {}))
189
+ readKeys.add(key);
190
+ }
191
+ for (const aff of affList) {
192
+ const orphan = (aff.effect?.writes ?? []).filter((key) => !readKeys.has(key));
193
+ if (orphan.length > 0) {
194
+ findings.push({
195
+ code: 'unconsumed-write',
196
+ severity: 'info',
197
+ affordance: aff.id,
198
+ keys: orphan,
199
+ message: `Action “${aff.id}” declares it writes ${list(orphan)}, but no guard anywhere reads ${orphan.length > 1 ? 'them' : 'it'} — the write does nothing for planning (the app UI may still use it).`,
200
+ remedy: REMEDY_WRITE,
201
+ });
202
+ }
203
+ }
204
+ return findings;
205
+ }
206
+ const SEVERITY_RANK = { error: 3, warning: 2, info: 1 };
207
+ /** A plain-text report of the findings, most severe first. Empty string when clean. */
208
+ export function formatFindings(findings) {
209
+ if (findings.length === 0)
210
+ return '';
211
+ const ordered = [...findings].sort((a, b) => SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity]);
212
+ return ordered
213
+ .map((f) => {
214
+ const where = f.affordance ?? f.skill ?? f.page ?? '';
215
+ return `[${f.severity.toUpperCase()}] ${f.code}${where ? ` (${where})` : ''}\n ${f.message}\n → ${f.remedy}`;
216
+ })
217
+ .join('\n\n');
218
+ }
219
+ /**
220
+ * Throw if the graph has stale-logic findings at or above `failOn` (default
221
+ * 'error'). The opt-in CI gate: `expectNoStaleLogic(graph, { initialState })`
222
+ * fails a commit that drifts the graph. Report-by-default stays the norm —
223
+ * call lintGraph directly to inspect without failing.
224
+ */
225
+ export function expectNoStaleLogic(graph, opts) {
226
+ const failOn = opts?.failOn ?? 'error';
227
+ const threshold = SEVERITY_RANK[failOn];
228
+ const findings = lintGraph(graph, opts);
229
+ const failing = findings.filter((f) => SEVERITY_RANK[f.severity] >= threshold);
230
+ if (failing.length > 0) {
231
+ throw new Error(`hcifootprint: the navigation graph has ${failing.length} stale-logic finding(s) at or above ${failOn}:\n\n` +
232
+ formatFindings(failing));
233
+ }
234
+ }
235
+ //# sourceMappingURL=lint.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lint.js","sourceRoot":"","sources":["../../../src/testing/model/lint.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Pure guard-satisfiability reasoning — "can this guard EVER be true?"
3
+ *
4
+ * The graph compiler (composeGuards) already rejects a key whose SAME operator
5
+ * is declared twice with different values. What it does NOT catch is a key
6
+ * whose DIFFERENT operators contradict each other — { gt: 5, lt: 3 },
7
+ * { eq: 'paid', in: ['draft', 'open'] }, { in: [] }. A control gated on such a
8
+ * guard can never light up, so the linter flags it.
9
+ *
10
+ * This is literal reasoning only (no state, no engine) and deliberately
11
+ * CONSERVATIVE: it returns a contradiction only when one is provable from the
12
+ * operators alone. Anything open-ended (ne / notIn / a lone range) is left
13
+ * unflagged — the linter never cries "dead" over something it cannot prove.
14
+ */
15
+ import type { WhereFilter } from 'footprintjs';
16
+ type Ops = Record<string, unknown>;
17
+ /** A definite, provable contradiction among one key's operators, or null. */
18
+ export declare function unsatisfiableReason(ops: Ops): string | null;
19
+ /** Every key of a composed guard whose operators provably contradict. */
20
+ export declare function unsatisfiableKeys(guard: WhereFilter | undefined): {
21
+ key: string;
22
+ reason: string;
23
+ }[];
24
+ export {};
25
+ //# sourceMappingURL=satisfiable.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"satisfiable.d.ts","sourceRoot":"","sources":["../../../src/testing/model/satisfiable.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C,KAAK,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAuCnC,6EAA6E;AAC7E,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,GAAG,IAAI,CAwC3D;AAED,yEAAyE;AACzE,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,WAAW,GAAG,SAAS,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,EAAE,CAUnG"}
@@ -0,0 +1,101 @@
1
+ /** Whether a concrete candidate value satisfies EVERY operator on one key. */
2
+ function satisfies(value, ops) {
3
+ for (const [op, target] of Object.entries(ops)) {
4
+ switch (op) {
5
+ case 'eq':
6
+ if (value !== target)
7
+ return false;
8
+ break;
9
+ case 'ne':
10
+ if (value === target)
11
+ return false;
12
+ break;
13
+ case 'in':
14
+ if (!Array.isArray(target) || !target.includes(value))
15
+ return false;
16
+ break;
17
+ case 'notIn':
18
+ if (Array.isArray(target) && target.includes(value))
19
+ return false;
20
+ break;
21
+ // Use the SAME raw comparison footprint's evaluator uses (`a > t`, JS
22
+ // coercion and all) — so "no candidate satisfies" is a proof against the
23
+ // real runtime, never a false positive from a stricter type rule.
24
+ case 'gt':
25
+ if (!(value > target))
26
+ return false;
27
+ break;
28
+ case 'gte':
29
+ if (!(value >= target))
30
+ return false;
31
+ break;
32
+ case 'lt':
33
+ if (!(value < target))
34
+ return false;
35
+ break;
36
+ case 'lte':
37
+ if (!(value <= target))
38
+ return false;
39
+ break;
40
+ // Unknown operators are the compiler's job to reject — treat as neutral.
41
+ }
42
+ }
43
+ return true;
44
+ }
45
+ /** A definite, provable contradiction among one key's operators, or null. */
46
+ export function unsatisfiableReason(ops) {
47
+ // An empty allow-set can never match any value.
48
+ if (Array.isArray(ops['in']) && ops['in'].length === 0) {
49
+ return 'in: [] can never match any value';
50
+ }
51
+ // Numeric range inversion: lower bound above (or equal-and-exclusive to) upper.
52
+ const has = (k) => Object.hasOwn(ops, k);
53
+ const asNum = (v) => (typeof v === 'number' ? v : null);
54
+ const lo = has('gt')
55
+ ? { v: ops['gt'], inclusive: false }
56
+ : has('gte')
57
+ ? { v: ops['gte'], inclusive: true }
58
+ : null;
59
+ const hi = has('lt')
60
+ ? { v: ops['lt'], inclusive: false }
61
+ : has('lte')
62
+ ? { v: ops['lte'], inclusive: true }
63
+ : null;
64
+ if (lo && hi) {
65
+ const l = asNum(lo.v);
66
+ const h = asNum(hi.v);
67
+ if (l !== null && h !== null) {
68
+ if (l > h)
69
+ return `lower bound ${l} is above upper bound ${h}`;
70
+ if (l === h && !(lo.inclusive && hi.inclusive)) {
71
+ return `bounds around ${l} exclude every value`;
72
+ }
73
+ }
74
+ }
75
+ // When a finite candidate set exists (an exact eq, or an `in` allow-list),
76
+ // the answer is fully decidable: test each candidate against all operators.
77
+ const candidates = [];
78
+ if (has('eq'))
79
+ candidates.push(ops['eq']);
80
+ if (Array.isArray(ops['in']))
81
+ candidates.push(...ops['in']);
82
+ if (candidates.length > 0 && !candidates.some((value) => satisfies(value, ops))) {
83
+ return 'no value satisfies all of its operators at once';
84
+ }
85
+ return null;
86
+ }
87
+ /** Every key of a composed guard whose operators provably contradict. */
88
+ export function unsatisfiableKeys(guard) {
89
+ if (!guard)
90
+ return [];
91
+ const out = [];
92
+ for (const [key, ops] of Object.entries(guard)) {
93
+ if (ops && typeof ops === 'object' && !Array.isArray(ops)) {
94
+ const reason = unsatisfiableReason(ops);
95
+ if (reason)
96
+ out.push({ key, reason });
97
+ }
98
+ }
99
+ return out;
100
+ }
101
+ //# sourceMappingURL=satisfiable.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"satisfiable.js","sourceRoot":"","sources":["../../../src/testing/model/satisfiable.ts"],"names":[],"mappings":"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"}