wicked-crew 0.7.20 → 0.7.22

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.
@@ -4,6 +4,7 @@ import { CoreAdapter } from '../core/adapter.js';
4
4
  import { type GateCache } from './gate-cache.js';
5
5
  import type { ElicitationCache } from './elicitation-cache.js';
6
6
  import { QeGateCache } from '../qe/gate-events.js';
7
+ import type { ConformanceRule, SteeringType } from '../core/types.js';
7
8
  import { SeatHealthTracker } from './seat-health.js';
8
9
  import { type ProjectRoutesDeps } from '../projects/routes.js';
9
10
  import { InteractiveBridgePool } from '../interactive/bridge-pool.js';
@@ -18,6 +19,30 @@ import { DeliveryDerivationCache } from './delivery-cache.js';
18
19
  import { type DeliverExec, type WorktreeReprovisioner } from './post-hoc-deliver.js';
19
20
  import { type AuthMode } from './auth.js';
20
21
  export { API_PREFIX } from './api-prefix.js';
22
+ /**
23
+ * Map an APPROVED policy proposal (estate `proposal.approve` → `handed_off`) into a steering
24
+ * ConformanceRule (DES-MEM-FACETED-001 §5.2). A policy proposal's `kind_type` is
25
+ * `policy:<steering_type>` and its `payload` is `{ rule, severity }`; estate writes NOTHING for it
26
+ * (the AW-11 "no rules.write on estate" invariant) and hands the payload back for crew — the ONE
27
+ * governed rules-write path — to land. Returns the rule to upsert plus the resolved steering type,
28
+ * or a loud `error` string when the proposal cannot be shaped into a valid rule (a malformed
29
+ * `kind_type` / missing `rule` / out-of-enum `severity`) — the caller reports it as a failed
30
+ * landing, never a silent drop (the crew#388 anti-silent-loss doctrine).
31
+ *
32
+ * The rule id is DETERMINISTIC (`proposal:<id>`) so a re-driven landing UPSERTS the same rule
33
+ * (idempotent) instead of minting a duplicate; it sits OUTSIDE the reserved `PAT-/POL-` namespace,
34
+ * which UI/chat/proposal-authored rules are free to do (INV-C1). The rule carries NO `effect`
35
+ * (recall-only — exactly what `{rule, severity}` supports: an enforcement rule would need a
36
+ * non-blank `applies_to`, which the payload does not carry, and INV-S3 fails such a rule closed).
37
+ * `steering_type` is validated against the vocabulary here so the engine's INV-S1 never rejects it
38
+ * as an unknown page.
39
+ */
40
+ export declare function policyProposalToRule(proposalId: string, kindType: string, payload: unknown, facets: Record<string, string> | undefined): {
41
+ rule: ConformanceRule;
42
+ steeringType: SteeringType;
43
+ } | {
44
+ error: string;
45
+ };
21
46
  /**
22
47
  * The launch body. Every field but `problem` is optional, which is what made stripping dangerous:
23
48
  * omitting one is a legitimate request that gets an engine default, so a MISSPELLED one was
@@ -1 +1 @@
1
- {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/api/routes.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/api/routes.ts"],"names":[],"mappings":"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"}
@@ -100,6 +100,80 @@ function isStringRecord(v) {
100
100
  !Array.isArray(v) &&
101
101
  Object.values(v).every((x) => typeof x === 'string'));
102
102
  }
103
+ /**
104
+ * Map an APPROVED policy proposal (estate `proposal.approve` → `handed_off`) into a steering
105
+ * ConformanceRule (DES-MEM-FACETED-001 §5.2). A policy proposal's `kind_type` is
106
+ * `policy:<steering_type>` and its `payload` is `{ rule, severity }`; estate writes NOTHING for it
107
+ * (the AW-11 "no rules.write on estate" invariant) and hands the payload back for crew — the ONE
108
+ * governed rules-write path — to land. Returns the rule to upsert plus the resolved steering type,
109
+ * or a loud `error` string when the proposal cannot be shaped into a valid rule (a malformed
110
+ * `kind_type` / missing `rule` / out-of-enum `severity`) — the caller reports it as a failed
111
+ * landing, never a silent drop (the crew#388 anti-silent-loss doctrine).
112
+ *
113
+ * The rule id is DETERMINISTIC (`proposal:<id>`) so a re-driven landing UPSERTS the same rule
114
+ * (idempotent) instead of minting a duplicate; it sits OUTSIDE the reserved `PAT-/POL-` namespace,
115
+ * which UI/chat/proposal-authored rules are free to do (INV-C1). The rule carries NO `effect`
116
+ * (recall-only — exactly what `{rule, severity}` supports: an enforcement rule would need a
117
+ * non-blank `applies_to`, which the payload does not carry, and INV-S3 fails such a rule closed).
118
+ * `steering_type` is validated against the vocabulary here so the engine's INV-S1 never rejects it
119
+ * as an unknown page.
120
+ */
121
+ export function policyProposalToRule(proposalId, kindType, payload, facets) {
122
+ const steeringType = kindType.startsWith('policy:') ? kindType.slice('policy:'.length) : '';
123
+ if (!STEERING_TYPES.has(steeringType)) {
124
+ return {
125
+ error: `the approved proposal's kind_type ${JSON.stringify(kindType)} does not name a steering ` +
126
+ `type — expected \`policy:<${STEERING_TYPE_VALUES.join('|')}>\``,
127
+ };
128
+ }
129
+ const body = typeof payload === 'object' && payload !== null && !Array.isArray(payload)
130
+ ? payload
131
+ : {};
132
+ const statement = typeof body['rule'] === 'string' ? body['rule'].trim() : '';
133
+ if (statement === '') {
134
+ return {
135
+ error: 'the approved policy proposal payload has no `rule` string to make a rule statement from',
136
+ };
137
+ }
138
+ const rawSeverity = body['severity'];
139
+ // Normalize common LLM drift before the enum check: a model naturally writes the English word
140
+ // "warning", but the engine's enum is the short "warn" (info/error/critical are already the
141
+ // natural words). Tolerate case/whitespace too. A capture worker that says "warning" must still
142
+ // land its policy — otherwise every derived policy at the middle band silently fails to land.
143
+ const normSeverity = typeof rawSeverity === 'string'
144
+ ? (() => {
145
+ const s = rawSeverity.trim().toLowerCase();
146
+ return s === 'warning' ? 'warn' : s;
147
+ })()
148
+ : rawSeverity;
149
+ const severity = normSeverity === 'info' || normSeverity === 'warn' || normSeverity === 'error' || normSeverity === 'critical'
150
+ ? normSeverity
151
+ : undefined;
152
+ if (rawSeverity !== undefined && severity === undefined) {
153
+ return {
154
+ error: `the approved policy proposal payload has an invalid severity ${JSON.stringify(rawSeverity)} — ` +
155
+ 'expected info|warn|error|critical (or the natural "warning" for warn)',
156
+ };
157
+ }
158
+ const language = facets?.['language'];
159
+ const rule = {
160
+ id: `proposal:${proposalId}`,
161
+ rule_type: 'policy',
162
+ statement,
163
+ // A policy proposal SHOULD carry severity; a missing one defaults to `warn` (the middle band)
164
+ // rather than failing the landing, but a present-but-garbage one fails loud above.
165
+ severity: severity ?? 'warn',
166
+ // REQUIRED engine-side (f32, no serde default, INV-C2 `[0,1]`); the payload carries none, so a
167
+ // fixed authority — the same 0.8 the steering-author landing defaults to.
168
+ confidence: 0.8,
169
+ // Only the `language` facet maps onto the engine's Targets facet object; `repo`/`project`
170
+ // facets have no ConformanceRule slot and are dropped (see the route's open question).
171
+ targets: typeof language === 'string' && language !== '' ? { language } : {},
172
+ provenance: { source: 'proposal', source_kinds: [] },
173
+ steering_type: steeringType,
174
+ };
175
+ return { rule, steeringType: steeringType };
176
+ }
103
177
  /** A JSON object whose every value is a number (a coverage breakdown map). */
104
178
  function isNumberRecord(v) {
105
179
  return (typeof v === 'object' &&
@@ -2744,23 +2818,95 @@ runtime = {}) {
2744
2818
  }
2745
2819
  });
2746
2820
  // POST /proposals/:id/approve → proposal.approve →
2747
- // { outcome:'promoted', active_id } — a MEMORY proposal, now an active memory (complete);
2748
- // { outcome:'handed_off', payload } — a POLICY proposal, returned VERBATIM.
2821
+ // { outcome:'promoted', active_id } — a MEMORY proposal, now an active memory (complete);
2822
+ // { outcome:'handed_off', payload, landing } — a POLICY proposal, LANDED here as a steering rule.
2823
+ //
2824
+ // The policy→steering landing (DES-MEM-FACETED-001 §5.2): estate performs NO rules-write (the
2825
+ // AW-11 invariant) — it marks the proposal approved and hands its `{ rule, severity }` payload
2826
+ // back, and CREW is the one governed rules-write path. On a `handed_off` outcome we recover the
2827
+ // proposal's `kind_type` (which carries the `policy:<steering_type>` the approve response does
2828
+ // NOT echo), shape a `ConformanceRule` from it, and upsert it via the single-writer actor. The
2829
+ // posture mirrors the steering-author landing (crew#388): the estate decision (approve) already
2830
+ // stands, so a landing failure is reported LOUD in-band as `landing.outcome:"failed"` — never a
2831
+ // 500, never the silent no-op that crew#388 exists to end — and `payload` is still returned for
2832
+ // backward compatibility. The MEMORY path is UNTOUCHED: a `promoted` outcome returns immediately
2833
+ // with no extra estate call and no landing.
2749
2834
  app.post(`${V}/proposals/:id/approve`, { config: { manifest: { responseType: 'ApproveProposalResponse', statusCodes: [200, 400, 502] } } }, async (req, reply) => {
2750
- const { id } = req.params;
2751
- if (id.trim() === '') {
2835
+ // Normalize ONCE and use the trimmed value throughout — an id like `%20pol1%20`
2836
+ // decodes to a padded, non-empty string that would otherwise ride upstream as-is
2837
+ // and into `proposal:<id>` derivations.
2838
+ const id = req.params.id.trim();
2839
+ if (id === '') {
2752
2840
  return reply.code(400).send({ error: '`id` is required' });
2753
2841
  }
2842
+ let approved;
2754
2843
  try {
2755
- // TODO: route handed_off policy payload to steering (crew steering-write, DES-MEM-FACETED-001
2756
- // §5.2). Policy→steering routing is OUT OF SCOPE here: a `handed_off` outcome is passed
2757
- // through as-is for a later steering-write to consume; memory proposals return `promoted` and
2758
- // need nothing more.
2759
- return (await estateTool('proposal.approve', { id }));
2844
+ approved = (await estateTool('proposal.approve', { id }));
2760
2845
  }
2761
2846
  catch (err) {
2762
2847
  return estateUpstreamError(reply, err);
2763
2848
  }
2849
+ // MEMORY proposal (or any future non-policy outcome): estate already promoted it — pass through
2850
+ // verbatim, exactly as before. No extra estate round-trip on the memory path.
2851
+ if (approved.outcome !== 'handed_off') {
2852
+ return approved;
2853
+ }
2854
+ // POLICY proposal — land the handed-off payload as a steering rule.
2855
+ const landFailed = (error) => {
2856
+ audit.record('governance.steering.landing_failed', actorOf(req), {
2857
+ detail: { proposalId: id, error },
2858
+ });
2859
+ return { outcome: 'handed_off', payload: approved.payload, landing: { outcome: 'failed', error } };
2860
+ };
2861
+ // A pre-steering engine would SILENTLY DROP `steering_type` and the other steering fields
2862
+ // (ConformanceRule has no deny_unknown_fields), persisting a rule that enforces differently than
2863
+ // authored — the same guard POST /governance/rules applies. Fail loud (before any further estate
2864
+ // round-trip), persist nothing.
2865
+ if (!adapter.steeringSupported()) {
2866
+ return landFailed(`${new SteeringUnsupportedError('Landing an approved policy proposal').message} — the ` +
2867
+ `installed engine would silently drop the rule's steering fields; upgrade wicked-core-ts (>= 0.7.5)`);
2868
+ }
2869
+ // Recover the proposal's `kind_type` + `facets`: `proposal.approve` does not echo them, and the
2870
+ // steering type lives ONLY in `kind_type` (`policy:<type>`). The proposal is `approved` now, so
2871
+ // read it back from the approved queue.
2872
+ let proposal;
2873
+ try {
2874
+ const listed = (await estateTool('proposal.list', { state: 'approved' }));
2875
+ proposal = listed.proposals.find((p) => p.id === id);
2876
+ }
2877
+ catch (err) {
2878
+ return landFailed(`the policy proposal was approved but its record could not be re-read to land a steering ` +
2879
+ `rule (${message(err)}); land it by hand via POST ${V}/governance/rules`);
2880
+ }
2881
+ if (proposal === undefined || typeof proposal.kind_type !== 'string') {
2882
+ return landFailed(`the policy proposal was approved but could not be found in the approved queue to recover ` +
2883
+ `its steering type; land it by hand via POST ${V}/governance/rules`);
2884
+ }
2885
+ const built = policyProposalToRule(id, proposal.kind_type, approved.payload, isStringRecord(proposal.facets) ? proposal.facets : undefined);
2886
+ if ('error' in built) {
2887
+ return landFailed(built.error);
2888
+ }
2889
+ try {
2890
+ await adapter.upsertConformanceRule(built.rule);
2891
+ }
2892
+ catch (err) {
2893
+ return landFailed(`the store refused the steering rule derived from the approved policy proposal: ${message(err)}`);
2894
+ }
2895
+ audit.record('governance.rule.upserted', actorOf(req), {
2896
+ detail: {
2897
+ id: built.rule.id,
2898
+ source: 'proposal',
2899
+ via: 'proposal-approve',
2900
+ steeringType: built.steeringType,
2901
+ proposalId: id,
2902
+ },
2903
+ });
2904
+ const landing = {
2905
+ outcome: 'landed',
2906
+ ruleId: built.rule.id,
2907
+ steering_type: built.steeringType,
2908
+ };
2909
+ return { outcome: 'handed_off', payload: approved.payload, landing };
2764
2910
  });
2765
2911
  // POST /proposals/:id/reject → proposal.reject → { ok: true }.
2766
2912
  app.post(`${V}/proposals/:id/reject`, { config: { manifest: { responseType: 'RejectProposalResponse', statusCodes: [200, 400, 502] } } }, async (req, reply) => {