@holmes-lab/holmes-kit 0.19.5 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +135 -0
- package/README.md +17 -1
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/approve-status.d.ts +24 -0
- package/dist/holmes/cli/approve-status.js +166 -0
- package/dist/holmes/cli/approve-watch.d.ts +6 -0
- package/dist/holmes/cli/approve-watch.js +6 -0
- package/dist/holmes/cli/approve.d.ts +27 -0
- package/dist/holmes/cli/approve.js +68 -7
- package/dist/holmes/cli/cli-execution.d.ts +2 -0
- package/dist/holmes/cli/cli-execution.js +24 -0
- package/dist/holmes/cli/gitignore-merge.d.ts +0 -5
- package/dist/holmes/cli/gitignore-merge.js +6 -2
- package/dist/holmes/cli/index.js +176 -139
- package/dist/holmes/cli/playbook-skills.d.ts +2 -22
- package/dist/holmes/cli/playbook-skills.js +10 -54
- package/dist/holmes/governance/approval-grants.d.ts +22 -0
- package/dist/holmes/governance/approval-grants.js +55 -1
- package/dist/holmes/governance/approval-queue.d.ts +43 -4
- package/dist/holmes/governance/approval-queue.js +86 -12
- package/dist/holmes/governance/display-id-aliases.d.ts +29 -0
- package/dist/holmes/governance/display-id-aliases.js +86 -0
- package/dist/holmes/governance/ledger-store.d.ts +42 -14
- package/dist/holmes/governance/ledger-store.js +49 -3
- package/dist/holmes/governance/ledger-timeline.d.ts +3 -1
- package/dist/holmes/governance/ledger-timeline.js +11 -1
- package/dist/holmes/governance/provenance-chain.d.ts +2 -0
- package/dist/holmes/governance/provenance-chain.js +13 -2
- package/dist/holmes/governance/provenance-ledger.d.ts +28 -0
- package/dist/holmes/governance/provenance-ledger.js +69 -16
- package/dist/holmes/governance/session-context.d.ts +3 -0
- package/dist/holmes/governance/session-context.js +26 -8
- package/dist/holmes/guardrail/risk-classifier.js +2 -2
- package/dist/holmes/guardrail/write-target.js +2 -1
- package/dist/holmes/hooks/pre-tool-use.js +12 -1
- package/dist/holmes/hooks/stop.js +14 -3
- package/dist/holmes/mcp/handlers/adoption.d.ts +118 -0
- package/dist/holmes/mcp/handlers/adoption.js +186 -0
- package/dist/holmes/mcp/handlers/entity-integration.d.ts +100 -0
- package/dist/holmes/mcp/handlers/entity-integration.js +324 -0
- package/dist/holmes/mcp/handlers/entity-renumber.d.ts +137 -0
- package/dist/holmes/mcp/handlers/entity-renumber.js +228 -0
- package/dist/holmes/mcp/handlers/entity-store.d.ts +78 -0
- package/dist/holmes/mcp/handlers/entity-store.js +212 -0
- package/dist/holmes/mcp/handlers/graph-operations.d.ts +90 -0
- package/dist/holmes/mcp/handlers/graph-operations.js +260 -0
- package/dist/holmes/mcp/handlers/maintenance-analysis.d.ts +34 -0
- package/dist/holmes/mcp/handlers/maintenance-analysis.js +288 -0
- package/dist/holmes/mcp/handlers/maintenance-evidence.d.ts +40 -0
- package/dist/holmes/mcp/handlers/maintenance-evidence.js +104 -0
- package/dist/holmes/mcp/handlers/operator-inspection.d.ts +167 -0
- package/dist/holmes/mcp/handlers/operator-inspection.js +284 -0
- package/dist/holmes/mcp/handlers/phase-queries.d.ts +34 -0
- package/dist/holmes/mcp/handlers/phase-queries.js +86 -0
- package/dist/holmes/mcp/handlers/review-evidence.d.ts +47 -0
- package/dist/holmes/mcp/handlers/review-evidence.js +370 -0
- package/dist/holmes/mcp/handlers/review-queries.d.ts +68 -0
- package/dist/holmes/mcp/handlers/review-queries.js +79 -0
- package/dist/holmes/mcp/handlers/risk-assessment.d.ts +25 -0
- package/dist/holmes/mcp/handlers/risk-assessment.js +181 -0
- package/dist/holmes/mcp/handlers/slice-orchestration.d.ts +55 -0
- package/dist/holmes/mcp/handlers/slice-orchestration.js +324 -0
- package/dist/holmes/mcp/handlers/spec-approval.d.ts +105 -0
- package/dist/holmes/mcp/handlers/spec-approval.js +504 -0
- package/dist/holmes/mcp/handlers/spec-authoring.d.ts +57 -0
- package/dist/holmes/mcp/handlers/spec-authoring.js +326 -0
- package/dist/holmes/mcp/handlers/spec-lifecycle.d.ts +122 -0
- package/dist/holmes/mcp/handlers/spec-lifecycle.js +339 -0
- package/dist/holmes/mcp/handlers/spec-queries.d.ts +30 -0
- package/dist/holmes/mcp/handlers/spec-queries.js +74 -0
- package/dist/holmes/mcp/handlers/taint-screening.d.ts +48 -0
- package/dist/holmes/mcp/handlers/taint-screening.js +183 -0
- package/dist/holmes/mcp/handlers/test-execution.d.ts +63 -0
- package/dist/holmes/mcp/handlers/test-execution.js +210 -0
- package/dist/holmes/mcp/handlers/workspace-identity.d.ts +30 -0
- package/dist/holmes/mcp/handlers/workspace-identity.js +68 -0
- package/dist/holmes/mcp/handlers/workspace-queries.d.ts +77 -0
- package/dist/holmes/mcp/handlers/workspace-queries.js +193 -0
- package/dist/holmes/mcp/handlers.d.ts +653 -424
- package/dist/holmes/mcp/handlers.js +267 -3250
- package/dist/holmes/mcp/server.js +2 -1
- package/dist/holmes/mcp/tool-schemas.js +43 -2
- package/dist/holmes/project/execution-context.d.ts +17 -0
- package/dist/holmes/project/execution-context.js +120 -0
- package/dist/holmes/project/installer-markers.d.ts +34 -0
- package/dist/holmes/project/installer-markers.js +65 -0
- package/dist/holmes/project/root.d.ts +3 -1
- package/dist/holmes/project/root.js +4 -3
- package/dist/holmes/project/workspace-identity.d.ts +29 -0
- package/dist/holmes/project/workspace-identity.js +181 -0
- package/dist/holmes/review/test-outcomes.d.ts +6 -1
- package/dist/holmes/review/test-outcomes.js +23 -5
- package/dist/holmes/review/test-runner.d.ts +18 -0
- package/dist/holmes/review/test-runner.js +132 -4
- package/dist/holmes/rtm/graph-store.d.ts +50 -1
- package/dist/holmes/rtm/graph-store.js +244 -3
- package/dist/holmes/rtm/incremental.d.ts +1 -0
- package/dist/holmes/rtm/incremental.js +12 -3
- package/dist/holmes/rtm/localize.js +7 -0
- package/dist/holmes/rtm/rtm-graph.d.ts +9 -0
- package/dist/holmes/rtm/rtm-graph.js +13 -0
- package/dist/holmes/semantic/vector-cache.d.ts +16 -1
- package/dist/holmes/semantic/vector-cache.js +155 -19
- package/dist/holmes/spec/approval-status.d.ts +10 -0
- package/dist/holmes/spec/approval-status.js +7 -3
- package/dist/holmes/spec/entity-git-snapshot.d.ts +50 -0
- package/dist/holmes/spec/entity-git-snapshot.js +276 -0
- package/dist/holmes/spec/entity-integration-discharge.d.ts +73 -0
- package/dist/holmes/spec/entity-integration-discharge.js +233 -0
- package/dist/holmes/spec/entity-integration-transaction.d.ts +179 -0
- package/dist/holmes/spec/entity-integration-transaction.js +1221 -0
- package/dist/holmes/spec/entity-integration.d.ts +207 -0
- package/dist/holmes/spec/entity-integration.js +747 -0
- package/dist/holmes/spec/entity-renumber-transaction.d.ts +82 -0
- package/dist/holmes/spec/entity-renumber-transaction.js +711 -0
- package/dist/holmes/spec/entity-renumber.d.ts +32 -0
- package/dist/holmes/spec/entity-renumber.js +156 -0
- package/dist/holmes/spec/entity-store.d.ts +135 -0
- package/dist/holmes/spec/entity-store.js +1051 -0
- package/dist/holmes/spec/entity-transaction.d.ts +89 -0
- package/dist/holmes/spec/entity-transaction.js +701 -0
- package/dist/holmes/spec/renumber.d.ts +58 -0
- package/dist/holmes/spec/renumber.js +200 -1
- package/dist/holmes/spec/spec-store.d.ts +3 -2
- package/dist/holmes/spec/spec-store.js +23 -2
- package/dist/holmes/spec/transition-policy.d.ts +36 -0
- package/dist/holmes/spec/transition-policy.js +135 -0
- package/package.json +1 -1
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.createSpecLifecycleHandlers = createSpecLifecycleHandlers;
|
|
37
|
+
// @implements A-SPEC-628, A-SPEC-627.1, A-SPEC-621, A-SPEC-100.2, A-SPEC-128, A-SPEC-151, A-SPEC-222, A-SPEC-538.1, A-SPEC-255, A-SPEC-133, A-SPEC-188, A-SPEC-189, A-SPEC-244, A-SPEC-245
|
|
38
|
+
// @implements A-SPEC-638
|
|
39
|
+
const path = __importStar(require("node:path"));
|
|
40
|
+
const spec_store_1 = require("../../spec/spec-store");
|
|
41
|
+
const transition_policy_1 = require("../../spec/transition-policy");
|
|
42
|
+
const entity_store_1 = require("../../spec/entity-store");
|
|
43
|
+
const entity_transaction_1 = require("../../spec/entity-transaction");
|
|
44
|
+
const workspace_identity_1 = require("../../project/workspace-identity");
|
|
45
|
+
const node_crypto_1 = require("node:crypto");
|
|
46
|
+
const ledger_store_1 = require("../../governance/ledger-store");
|
|
47
|
+
const provenance_chain_1 = require("../../governance/provenance-chain");
|
|
48
|
+
const approval_grants_1 = require("../../governance/approval-grants");
|
|
49
|
+
const renumber_1 = require("../../spec/renumber");
|
|
50
|
+
function createSpecLifecycleHandlers(context) {
|
|
51
|
+
const { store, resolveLedgerRoot } = context;
|
|
52
|
+
return {
|
|
53
|
+
/**
|
|
54
|
+
* Move a document to `outdated` — the only path there.
|
|
55
|
+
*
|
|
56
|
+
* @implements A-SPEC-222
|
|
57
|
+
* `outdated` was in SPEC_STATUSES with no code writing it and no document carrying it: a
|
|
58
|
+
* declared state nothing could reach. That mattered the moment 23 deprecated REQs needed
|
|
59
|
+
* cleaning up, because the only transition tool sends everything to `draft`, and calling a
|
|
60
|
+
* retired document "draft" is a worse lie than the non-canonical word it replaced.
|
|
61
|
+
*
|
|
62
|
+
* Retirement withdraws authority rather than granting it, so it does NOT need the approval key
|
|
63
|
+
* by default. Two cases invert that, and the threshold follows the RISK rather than the name of
|
|
64
|
+
* the act:
|
|
65
|
+
*
|
|
66
|
+
* - A sealed document. The code gate demands an approved T-SPEC naming the target A-SPEC;
|
|
67
|
+
* retiring that T-SPEC removes the demand. Unguarded, retirement is an approval bypass.
|
|
68
|
+
* - A document an APPROVED spec depends on. That chain is holding something up right now.
|
|
69
|
+
*/
|
|
70
|
+
async spec_retire(a) {
|
|
71
|
+
const all = await store.list();
|
|
72
|
+
if (all.filter((s) => s.id === a.id).length > 1) {
|
|
73
|
+
return {
|
|
74
|
+
ok: false,
|
|
75
|
+
reason: `${a.id}이(가) 스토어에 두 번 이상 존재합니다 — 어느 사본이 진본인지 도구가 고를 수 없어 폐기 전에 거부합니다.`,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
const cur = await store.read(a.id);
|
|
79
|
+
if (!cur)
|
|
80
|
+
return { ok: false, reason: `spec ${a.id} not found` };
|
|
81
|
+
const spec = cur.spec;
|
|
82
|
+
// @implements A-SPEC-222 — a document with no `type:` cannot be filed: the store derives its
|
|
83
|
+
// folder from the kind, and there is none. Refuse rather than infer, because a wrongly
|
|
84
|
+
// inferred kind MOVES the document into another folder. Measured 2026-08-21: without this the
|
|
85
|
+
// store threw `Cannot read properties of undefined (reading 'folder')` on the first of the 23
|
|
86
|
+
// legacy documents this tool was built for — every unit fixture carried `type:`, which is how
|
|
87
|
+
// it got through. Declaring the kind is `spec_upgrade`'s job, so the refusal names it.
|
|
88
|
+
if (!spec.type) {
|
|
89
|
+
return {
|
|
90
|
+
ok: false,
|
|
91
|
+
reason: `${a.id}에는 \`type:\` 선언이 없습니다(구형식 문서) — 어느 폴더에 속하는지 알 수 없어 폐기할 수 없습니다.`
|
|
92
|
+
+ ' 종류를 추론하지 않는 이유는 잘못 추론한 종류가 문서를 다른 폴더로 옮기기 때문입니다.'
|
|
93
|
+
+ ` 먼저 spec_upgrade({ id: "${a.id}" })로 형식을 올린 뒤 다시 폐기하십시오 — 그 도구가 이전 status를 legacy_status로 보존합니다.`,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
// Idempotent, and it writes NOTHING on the second call: overwriting the first retirement's
|
|
97
|
+
// reason would make the record graffiti rather than history.
|
|
98
|
+
if (spec.status === 'outdated') {
|
|
99
|
+
return { ok: true, retired: false, id: a.id, dependents: [], reason: `${a.id}은(는) 이미 outdated 입니다` };
|
|
100
|
+
}
|
|
101
|
+
const approvalRaw = process.env.HOLMES_APPROVAL;
|
|
102
|
+
let approval;
|
|
103
|
+
try {
|
|
104
|
+
approval = approvalRaw ? JSON.parse(approvalRaw) : undefined;
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
approval = undefined;
|
|
108
|
+
}
|
|
109
|
+
const retireResolved = context.resolveHandlerApproval(a.root, approval, { kind: 'spec-approve', target: a.id }, new Date().toISOString());
|
|
110
|
+
const covered = retireResolved !== undefined;
|
|
111
|
+
const sealed = typeof spec.frontmatter.approved_digest === 'string';
|
|
112
|
+
if (sealed && !covered) {
|
|
113
|
+
return {
|
|
114
|
+
ok: false,
|
|
115
|
+
reason: `${a.id}은(는) 봉인된 문서입니다 — 폐기는 이 행위를 덮는 유효한 대역외 HOLMES_APPROVAL 이 필요합니다.`
|
|
116
|
+
+ ' 코드 게이트를 막고 있는 approved T-SPEC 을 폐기하면 그 게이트가 열리므로, 폐기가 승인 우회 경로가 되지 않도록 fail-closed 로 막습니다.'
|
|
117
|
+
+ ' (범위를 쓰면 kind "spec-approve")'
|
|
118
|
+
+ context.refusalQueueHint(a.root, { kind: 'spec-approve', target: a.id, why: '봉인된 스펙의 폐기' }),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
// @implements A-SPEC-245 — a grant that authorized breaking a seal is spent by it.
|
|
122
|
+
if (sealed && retireResolved?.source === 'grant' && retireResolved.root && retireResolved.approval.nonce) {
|
|
123
|
+
(0, approval_grants_1.consumeGrantFile)(retireResolved.root, retireResolved.approval.nonce);
|
|
124
|
+
}
|
|
125
|
+
const prepared = (0, transition_policy_1.prepareLifecycleSpec)(spec, all, 'retire', a.reason);
|
|
126
|
+
if (!prepared.ok)
|
|
127
|
+
return prepared;
|
|
128
|
+
const { dependents, candidate } = prepared;
|
|
129
|
+
// Destination BEFORE the write, the order A-SPEC-188 fixed for spec_approve: a resolution
|
|
130
|
+
// failure must leave nothing behind.
|
|
131
|
+
const dest = resolveLedgerRoot(a.root);
|
|
132
|
+
if (!dest.ok)
|
|
133
|
+
return { ok: false, reason: dest.reason };
|
|
134
|
+
const ledgerRoot = dest.root;
|
|
135
|
+
// Blank is omitted, never written: `retired_reason: ""` asserts a ground that does not exist,
|
|
136
|
+
// the same rule `legacy_status` follows. A document already carrying `superseded_by` has
|
|
137
|
+
// answered "why" already and is not asked twice.
|
|
138
|
+
const reason = typeof a.reason === 'string' ? a.reason.trim() : '';
|
|
139
|
+
try {
|
|
140
|
+
await store.write(candidate, { expectedVersion: cur.version });
|
|
141
|
+
}
|
|
142
|
+
catch (e) {
|
|
143
|
+
if (e instanceof spec_store_1.SpecVersionConflictError) {
|
|
144
|
+
return { ok: false, reason: `폐기 진행 중 ${a.id}이(가) 바뀌었습니다 — 확인 후 다시 시도하십시오. 이번 폐기는 아무것도 쓰지 않았습니다.` };
|
|
145
|
+
}
|
|
146
|
+
throw e;
|
|
147
|
+
}
|
|
148
|
+
new ledger_store_1.FileLedgerStore(path.join(ledgerRoot, '.ax', 'ledger')).append({
|
|
149
|
+
ts: new Date().toISOString(),
|
|
150
|
+
actor: approval?.actor ?? 'unattributed',
|
|
151
|
+
kind: 'spec-retired',
|
|
152
|
+
summary: `retired ${a.id}${reason ? ` — ${reason}` : ''}`,
|
|
153
|
+
inputs: [a.id],
|
|
154
|
+
rationale: reason || (approval?.rationale ?? 'retired'),
|
|
155
|
+
...(approval ? { authorization: (0, provenance_chain_1.authorizationRef)(approval.actor, approval.token) } : {}),
|
|
156
|
+
});
|
|
157
|
+
return { ok: true, retired: true, id: a.id, dependents: dependents.map((s) => s.id) };
|
|
158
|
+
},
|
|
159
|
+
// @implements A-SPEC-255 — the WIRING only. The judgment is `planRenumber`, which is pure and
|
|
160
|
+
// tested directly; this handler adds no rules of its own. Re-sealing is deliberately absent:
|
|
161
|
+
// `spec_approve` is the only sealer, so the plan reports the two ORDERS and the caller runs them.
|
|
162
|
+
// @implements A-SPEC-255 — WIRING only. The judgment lives in `planRenumber`, which is pure and
|
|
163
|
+
// tested directly; nothing here adds a rule. Re-sealing is deliberately absent: `spec_approve`
|
|
164
|
+
// is the only sealer (a second sealer becomes a second truth), so the plan reports the two
|
|
165
|
+
// ORDERS and the caller runs them. `dryRun` defaults to true — a renumber is read before it runs.
|
|
166
|
+
async spec_renumber(a) {
|
|
167
|
+
const specsRoot = store.specsRoot;
|
|
168
|
+
if (typeof specsRoot !== 'string')
|
|
169
|
+
return { ok: false, reason: '파일 스토어에 묶인 서버에서만 리넘버할 수 있습니다.' };
|
|
170
|
+
const run = (journaled) => {
|
|
171
|
+
try {
|
|
172
|
+
if ((0, entity_store_1.inspectEntityStore)(specsRoot).state === 'active')
|
|
173
|
+
return { ok: false, reason: 'An activated store requires entity_renumber to preserve entity UUIDs and references; raw legacy renumber is unavailable.' };
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
// Unmarked legacy file stores retain the existing API. Corrupt or pending entity state never falls back.
|
|
177
|
+
if (!(error instanceof entity_transaction_1.EntityStoreError) || error.code !== 'missing-workspace') {
|
|
178
|
+
if (error instanceof entity_transaction_1.EntityStoreError)
|
|
179
|
+
return { ok: false, reason: error.message };
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const projectRoot = path.resolve(specsRoot, '..', '..');
|
|
184
|
+
const plan = (0, renumber_1.planRenumber)({
|
|
185
|
+
specs: (0, renumber_1.readSpecsForRenumber)(specsRoot),
|
|
186
|
+
sources: (0, renumber_1.readSourcesForRenumber)(projectRoot),
|
|
187
|
+
oldBase: String(a.oldBase), newBase: String(a.newBase),
|
|
188
|
+
});
|
|
189
|
+
if (plan.refusal)
|
|
190
|
+
return { ok: false, reason: plan.refusal };
|
|
191
|
+
if (a.dryRun !== false)
|
|
192
|
+
return { ok: true, dryRun: true, plan };
|
|
193
|
+
// @implements A-SPEC-638 — in a registered workspace the raw publication is preceded by a
|
|
194
|
+
// rollback journal holding the original bytes of every file it will touch, so an interrupted
|
|
195
|
+
// run is inspectable and reversible through entity_store recovery.
|
|
196
|
+
let retire;
|
|
197
|
+
const identity = journaled ? (0, workspace_identity_1.workspaceIdentity)(journaled.root) : undefined;
|
|
198
|
+
if (journaled && identity && identity.state === 'registered') {
|
|
199
|
+
const operationId = (0, node_crypto_1.randomUUID)();
|
|
200
|
+
const rel = (abs) => path.relative(journaled.root, abs).split(path.sep).join('/');
|
|
201
|
+
const touched = [...new Set([
|
|
202
|
+
...plan.moves.flatMap((m) => [rel(path.join(specsRoot, m.from)), rel(path.join(specsRoot, m.to))]),
|
|
203
|
+
...plan.dependsOn.map((d) => rel(path.join(specsRoot, d.file))),
|
|
204
|
+
...plan.slices.map((s) => rel(path.join(specsRoot, s.file))),
|
|
205
|
+
...plan.anchors.map((anchor) => rel(path.join(projectRoot, anchor.file))),
|
|
206
|
+
])].sort();
|
|
207
|
+
const changes = touched.map((locator) => ({ locator, before: (0, entity_transaction_1.readEntityBytes)(path.join(journaled.root, locator))?.toString('base64') ?? null }));
|
|
208
|
+
const text = JSON.stringify({ schema: entity_store_1.LEGACY_RENUMBER_JOURNAL_SCHEMA, operationId, workspaceId: identity.workspaceId, storeLocator: journaled.locator, oldBase: String(a.oldBase), newBase: String(a.newBase), changes }) + '\n';
|
|
209
|
+
(0, entity_transaction_1.publishEntityRecord)(journaled.root, '.ax/state/entity-transactions/' + operationId + '/journal.json', text);
|
|
210
|
+
retire = () => (0, entity_transaction_1.retireEntityOperation)(journaled.root, operationId, journaled.locator, (0, entity_transaction_1.entityContentVersion)(text));
|
|
211
|
+
}
|
|
212
|
+
const movedSpecs = (0, renumber_1.applyRenumber)(specsRoot, { ...plan, anchors: [] });
|
|
213
|
+
(0, renumber_1.applyRenumber)(projectRoot, { ...plan, moves: [], dependsOn: [], slices: [] });
|
|
214
|
+
new ledger_store_1.FileLedgerStore(path.join(projectRoot, '.ax', 'ledger')).append({
|
|
215
|
+
ts: new Date().toISOString(),
|
|
216
|
+
actor: 'spec_renumber',
|
|
217
|
+
kind: 'spec-renumbered',
|
|
218
|
+
summary: `renumbered base ${a.oldBase} -> ${a.newBase}: ${movedSpecs} spec(s), ${plan.anchors.length} anchored file(s), ${plan.proseCandidates.length} prose candidate(s) left for a human`,
|
|
219
|
+
inputs: plan.moves.map((m) => `${m.oldId}->${m.newId}`),
|
|
220
|
+
});
|
|
221
|
+
retire?.();
|
|
222
|
+
return { ok: true, dryRun: false, movedSpecs, plan };
|
|
223
|
+
};
|
|
224
|
+
if (a.dryRun !== false)
|
|
225
|
+
return run();
|
|
226
|
+
let bound;
|
|
227
|
+
try {
|
|
228
|
+
bound = (0, entity_store_1.entityStoreBinding)(specsRoot);
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
if (error instanceof entity_transaction_1.EntityStoreError && error.code === 'missing-workspace')
|
|
232
|
+
return run();
|
|
233
|
+
if (error instanceof entity_transaction_1.EntityStoreError)
|
|
234
|
+
return { ok: false, reason: error.message };
|
|
235
|
+
throw error;
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
// Adoption uses the same store ownership. Recheck activation within this hold.
|
|
239
|
+
return (0, entity_transaction_1.withEntityStoreLock)(bound.root, bound.locator, () => run({ root: bound.root, locator: bound.locator }));
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
if (error instanceof entity_transaction_1.EntityStoreError)
|
|
243
|
+
return { ok: false, reason: error.message };
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
/**
|
|
248
|
+
* @implements A-SPEC-538.1
|
|
249
|
+
* The inverse of spec_approve: returns a SEALED (approved) spec to an editable `draft`, clearing
|
|
250
|
+
* `approved_digest` and `parent_digests` in ONE act, and records `spec-unsealed`. Hand-editing
|
|
251
|
+
* only `status` leaves seal residue that later trips validation; this atomizes the reverse.
|
|
252
|
+
*
|
|
253
|
+
* Un-sealing WITHDRAWS a seal, so — like retiring a sealed document — it demands a covering
|
|
254
|
+
* out-of-band HOLMES_APPROVAL (fail-closed): un-sealing an approved T-SPEC removes the code
|
|
255
|
+
* gate's demand, so an unguarded un-seal would be an approval bypass. And it refuses when an
|
|
256
|
+
* APPROVED spec depends on the target, because that dependent's `parent_digests` — the snapshot
|
|
257
|
+
* drift detection compares against — would silently go stale.
|
|
258
|
+
*/
|
|
259
|
+
async spec_unseal(a) {
|
|
260
|
+
const all = await store.list();
|
|
261
|
+
if (all.filter((s) => s.id === a.id).length > 1) {
|
|
262
|
+
return {
|
|
263
|
+
ok: false,
|
|
264
|
+
reason: `${a.id}이(가) 스토어에 두 번 이상 존재합니다 — 어느 사본이 진본인지 도구가 고를 수 없어 봉인 해제 전에 거부합니다.`,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
const cur = await store.read(a.id);
|
|
268
|
+
if (!cur) {
|
|
269
|
+
const dir = store.specsRoot;
|
|
270
|
+
const unreadable = typeof dir === 'string' ? (0, spec_store_1.unreadableSpecFiles)(dir) : [];
|
|
271
|
+
return { ok: false, reason: (0, spec_store_1.notFoundReason)(a.id, unreadable) };
|
|
272
|
+
}
|
|
273
|
+
const spec = cur.spec;
|
|
274
|
+
if (!spec.type) {
|
|
275
|
+
return {
|
|
276
|
+
ok: false,
|
|
277
|
+
reason: `${a.id}에는 \`type:\` 선언이 없습니다(구형식 문서) — 어느 폴더에 속하는지 알 수 없어 봉인 해제할 수 없습니다.`
|
|
278
|
+
+ ` 먼저 spec_upgrade({ id: "${a.id}" })로 형식을 올린 뒤 다시 시도하십시오.`,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
// Sealed ⇔ carries approved_digest. Un-sealing an already-draft spec writes NOTHING (a second
|
|
282
|
+
// un-seal is history, not graffiti) — mirrors spec_retire's idempotent no-op.
|
|
283
|
+
const sealed = typeof spec.frontmatter.approved_digest === 'string';
|
|
284
|
+
if (!sealed) {
|
|
285
|
+
return { ok: true, unsealed: false, id: a.id, dependents: [], reason: `${a.id}은(는) 이미 미봉인(draft) 상태입니다` };
|
|
286
|
+
}
|
|
287
|
+
const approvalRaw = process.env.HOLMES_APPROVAL;
|
|
288
|
+
let approval;
|
|
289
|
+
try {
|
|
290
|
+
approval = approvalRaw ? JSON.parse(approvalRaw) : undefined;
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
approval = undefined;
|
|
294
|
+
}
|
|
295
|
+
const resolved = context.resolveHandlerApproval(a.root, approval, { kind: 'spec-approve', target: a.id }, new Date().toISOString());
|
|
296
|
+
if (resolved === undefined) {
|
|
297
|
+
return {
|
|
298
|
+
ok: false,
|
|
299
|
+
reason: `${a.id}은(는) 봉인된 문서입니다 — 봉인 해제는 이 행위를 덮는 유효한 대역외 HOLMES_APPROVAL 이 필요합니다.`
|
|
300
|
+
+ ' 봉인을 해제하면 코드 게이트가 요구하던 approved 스펙의 봉인이 사라지므로, 해제가 승인 우회 경로가 되지 않도록 fail-closed 로 막습니다.'
|
|
301
|
+
+ ' (범위를 쓰면 kind "spec-approve")'
|
|
302
|
+
+ context.refusalQueueHint(a.root, { kind: 'spec-approve', target: a.id, why: '봉인된 스펙의 해제' }),
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
// @implements A-SPEC-245 — a grant that authorized breaking a seal is spent by it.
|
|
306
|
+
if (resolved.source === 'grant' && resolved.root && resolved.approval.nonce) {
|
|
307
|
+
(0, approval_grants_1.consumeGrantFile)(resolved.root, resolved.approval.nonce);
|
|
308
|
+
}
|
|
309
|
+
const prepared = (0, transition_policy_1.prepareLifecycleSpec)(spec, all, 'unseal');
|
|
310
|
+
if (!prepared.ok)
|
|
311
|
+
return prepared;
|
|
312
|
+
const { dependents, candidate } = prepared;
|
|
313
|
+
// Destination BEFORE the write (A-SPEC-188 order): a resolution failure leaves nothing behind.
|
|
314
|
+
const dest = resolveLedgerRoot(a.root);
|
|
315
|
+
if (!dest.ok)
|
|
316
|
+
return { ok: false, reason: dest.reason };
|
|
317
|
+
const ledgerRoot = dest.root;
|
|
318
|
+
try {
|
|
319
|
+
await store.write(candidate, { expectedVersion: cur.version });
|
|
320
|
+
}
|
|
321
|
+
catch (e) {
|
|
322
|
+
if (e instanceof spec_store_1.SpecVersionConflictError) {
|
|
323
|
+
return { ok: false, reason: `봉인 해제 진행 중 ${a.id}이(가) 바뀌었습니다 — 확인 후 다시 시도하십시오. 이번 해제는 아무것도 쓰지 않았습니다.` };
|
|
324
|
+
}
|
|
325
|
+
throw e;
|
|
326
|
+
}
|
|
327
|
+
new ledger_store_1.FileLedgerStore(path.join(ledgerRoot, '.ax', 'ledger')).append({
|
|
328
|
+
ts: new Date().toISOString(),
|
|
329
|
+
actor: approval?.actor ?? 'unattributed',
|
|
330
|
+
kind: 'spec-unsealed',
|
|
331
|
+
summary: `unsealed ${a.id}`,
|
|
332
|
+
inputs: [a.id],
|
|
333
|
+
rationale: approval?.rationale ?? 'unsealed',
|
|
334
|
+
...(approval ? { authorization: (0, provenance_chain_1.authorizationRef)(approval.actor, approval.token) } : {}),
|
|
335
|
+
});
|
|
336
|
+
return { ok: true, unsealed: true, id: a.id, dependents: dependents.map((s) => s.id) };
|
|
337
|
+
},
|
|
338
|
+
};
|
|
339
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Spec } from '../../spec/spec-parser';
|
|
2
|
+
export interface SpecQueryContext {
|
|
3
|
+
listSpecs(): Promise<Spec[]>;
|
|
4
|
+
assertStoreReachable(tool: string, root: unknown): void;
|
|
5
|
+
resolver(specs: Spec[]): (id: string) => Spec | null;
|
|
6
|
+
}
|
|
7
|
+
export declare function createSpecQueryHandlers(context: SpecQueryContext): {
|
|
8
|
+
spec_validate(a: any): Promise<{
|
|
9
|
+
ok: boolean;
|
|
10
|
+
findings: {
|
|
11
|
+
level: string;
|
|
12
|
+
code: string;
|
|
13
|
+
message: any;
|
|
14
|
+
}[];
|
|
15
|
+
}>;
|
|
16
|
+
spec_list(a: any): Promise<{
|
|
17
|
+
specs: {
|
|
18
|
+
legacy?: boolean | undefined;
|
|
19
|
+
parent?: string | undefined;
|
|
20
|
+
id: string;
|
|
21
|
+
type: import("../../spec/spec-types").SpecType;
|
|
22
|
+
status: "draft" | "review" | "approved" | "outdated";
|
|
23
|
+
}[];
|
|
24
|
+
}>;
|
|
25
|
+
spec_next(): Promise<{
|
|
26
|
+
next: import("../../spec/spec-types").SpecType;
|
|
27
|
+
} | {
|
|
28
|
+
next: null;
|
|
29
|
+
}>;
|
|
30
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createSpecQueryHandlers = createSpecQueryHandlers;
|
|
4
|
+
const validator_1 = require("../../spec/validator");
|
|
5
|
+
const legacy_format_1 = require("../../spec/legacy-format");
|
|
6
|
+
const spec_types_1 = require("../../spec/spec-types");
|
|
7
|
+
function createSpecQueryHandlers(context) {
|
|
8
|
+
return {
|
|
9
|
+
async spec_validate(a) {
|
|
10
|
+
const specs = await context.listSpecs();
|
|
11
|
+
const spec = specs.find((s) => s.id === a.id);
|
|
12
|
+
if (!spec)
|
|
13
|
+
return { ok: false, findings: [{ level: 'error', code: 'not-found', message: a.id }] };
|
|
14
|
+
return (0, validator_1.validateSpec)(spec, context.resolver(specs));
|
|
15
|
+
},
|
|
16
|
+
async spec_list(a) {
|
|
17
|
+
context.assertStoreReachable('spec_list', a.root); // @implements A-SPEC-419
|
|
18
|
+
const specs = await context.listSpecs();
|
|
19
|
+
// @implements A-SPEC-186
|
|
20
|
+
// The parent travels with the id because a bare one cannot be read. Measured: a reader took
|
|
21
|
+
// `T-SPEC-1841` for a count of 1,841 documents. The old-habit ids this repository once held
|
|
22
|
+
// have since all been renamed or upgraded (measured 2026-08-15: zero remain), but ADOPTED
|
|
23
|
+
// repositories still write them, so the parent keeps traveling alongside.
|
|
24
|
+
//
|
|
25
|
+
// Read from `depends_on`, NEVER parsed out of the id — parsing would answer `A-SPEC-129` for
|
|
26
|
+
// a `T-SPEC-1291` that actually hangs off something else, which is the tool committing the
|
|
27
|
+
// very misreading this exists to prevent. Omitted rather than emptied when there is none:
|
|
28
|
+
// an empty value cannot be told apart from "we looked and failed".
|
|
29
|
+
const parentOf = (s) => {
|
|
30
|
+
// @implements A-SPEC-192 §6R (round 10) — the row became format-aware in the `legacy` field
|
|
31
|
+
// only; `parent` still keyed off the DECLARED type, so an old-format document that never
|
|
32
|
+
// declared one showed no parent even when its depends_on names one. The inferred kind
|
|
33
|
+
// answers the same question the legacy mark already asks.
|
|
34
|
+
const state = (0, legacy_format_1.formatStateOf)({ id: s.id, type: s.type });
|
|
35
|
+
const kind = s.type ?? (state.kind === 'legacy' ? state.inferred : undefined);
|
|
36
|
+
const kinds = (0, spec_types_1.specTypeDef)(kind)?.parents ?? [];
|
|
37
|
+
return s.dependsOn?.find((d) => kinds.some((k) => d.startsWith(`${k}-`)));
|
|
38
|
+
};
|
|
39
|
+
// @implements A-SPEC-192 — a non-canonical status must not dress like a governed row:
|
|
40
|
+
// measured 155 of 483 rows ('Approved', 'Proposed', 'deprecated'…) rendered identically to canonical
|
|
41
|
+
// ones, and an adopter read capital-A 'Approved' as an approval. Absence of the key marks the
|
|
42
|
+
// canonical row (the same omission convention as `parent` above). The set derives from the
|
|
43
|
+
// SPEC_STATUSES runtime list — a hand-copy here was a second truth the compiler cannot police.
|
|
44
|
+
const CANONICAL_STATUSES = new Set(spec_types_1.SPEC_STATUSES);
|
|
45
|
+
return {
|
|
46
|
+
specs: specs
|
|
47
|
+
.filter((s) => !a?.type || s.type === a.type)
|
|
48
|
+
.map((s) => {
|
|
49
|
+
const parent = parentOf(s);
|
|
50
|
+
// @implements A-SPEC-192 §5R (round 9) — the mark asks 'is this row a GOVERNED spec?',
|
|
51
|
+
// and status alone could not answer it: an old-format document that never declared a
|
|
52
|
+
// `type` came through with a lone `approved` and dressed exactly like a canonical row —
|
|
53
|
+
// the misreading this REQ exists to prevent. Format and status are both grounds.
|
|
54
|
+
const legacyFormat = (0, legacy_format_1.formatStateOf)({ id: s.id, type: s.type }).kind !== 'current';
|
|
55
|
+
return {
|
|
56
|
+
id: s.id, type: s.type, status: s.status,
|
|
57
|
+
...(parent ? { parent } : {}),
|
|
58
|
+
...(CANONICAL_STATUSES.has(s.status) && !legacyFormat ? {} : { legacy: true }),
|
|
59
|
+
};
|
|
60
|
+
}),
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
async spec_next() {
|
|
64
|
+
const specs = await context.listSpecs();
|
|
65
|
+
// 다음에 작성 가능한 첫 미승인 단계
|
|
66
|
+
for (const t of spec_types_1.SPEC_ORDER) {
|
|
67
|
+
const has = specs.some((s) => s.type === t && s.status === 'approved');
|
|
68
|
+
if (!has)
|
|
69
|
+
return { next: t };
|
|
70
|
+
}
|
|
71
|
+
return { next: null };
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Spec } from '../../spec/spec-parser';
|
|
2
|
+
import { ScannedFile } from '../../cpg/cpg-scanner';
|
|
3
|
+
export interface TaintScreeningContext {
|
|
4
|
+
listSpecs(): Promise<Spec[]>;
|
|
5
|
+
foreignRootReason(root?: string): string | null;
|
|
6
|
+
projectRootOf(root: string): string;
|
|
7
|
+
cachedScan(root: string, repoRoot?: string): ScannedFile[];
|
|
8
|
+
}
|
|
9
|
+
export declare function createTaintScreeningHandlers(context: TaintScreeningContext): {
|
|
10
|
+
/**
|
|
11
|
+
* @implements A-SPEC-138
|
|
12
|
+
* Call-graph taint REACHABILITY screen: source-named functions that reach sink-named functions
|
|
13
|
+
* through call edges. A SCREENING signal that routes a security review, NOT a data-flow proof —
|
|
14
|
+
* the result always carries the honesty envelope (kind + limits) and phrases pairs as "reaches",
|
|
15
|
+
* never "vulnerable".
|
|
16
|
+
*/
|
|
17
|
+
taint_scan(a: {
|
|
18
|
+
root: string;
|
|
19
|
+
dataFlow?: boolean;
|
|
20
|
+
}): Promise<{
|
|
21
|
+
kind: string;
|
|
22
|
+
limits: ("name-based matching" | "no def-use" | "no sanitizers" | "call-edge-only")[];
|
|
23
|
+
maxPaths: number | undefined;
|
|
24
|
+
truncated: number;
|
|
25
|
+
pairs: import("../../rtm/taint").TaintPair[];
|
|
26
|
+
} | {
|
|
27
|
+
ok: boolean;
|
|
28
|
+
reason: string;
|
|
29
|
+
} | {
|
|
30
|
+
dataFlow: {
|
|
31
|
+
findings: Record<string, unknown>[];
|
|
32
|
+
/** Findings the reaching-definition evidence positively refuted (dead or fully sanitized). */
|
|
33
|
+
refutedByReachingDefs: number;
|
|
34
|
+
limits: ("no field sensitivity (obj.a and obj.b are one value)" | "no aliasing" | "no path sensitivity" | "no container/element tracking" | "no reflection or dynamic dispatch" | "sink matching is by callee NAME with no receiver type — RE.exec(s) is indistinguishable from child_process.exec(s)" | "no anonymous-function parameters (arrow/function-expression params are not bound)" | "not statement-order sensitive — a variable tainted anywhere in a function is tainted throughout it" | "reports reachability of tainted data to a sink, never exploitability")[];
|
|
35
|
+
converged: boolean;
|
|
36
|
+
truncated: number;
|
|
37
|
+
/** Which languages this run could actually judge, so a zero is readable. */
|
|
38
|
+
languagesAnalysed: (string | null)[];
|
|
39
|
+
};
|
|
40
|
+
kind: string;
|
|
41
|
+
limits: ("name-based matching" | "no def-use" | "no sanitizers" | "call-edge-only")[];
|
|
42
|
+
maxPaths: number | undefined;
|
|
43
|
+
truncated: number;
|
|
44
|
+
pairs: import("../../rtm/taint").TaintPair[];
|
|
45
|
+
ok?: undefined;
|
|
46
|
+
reason?: undefined;
|
|
47
|
+
}>;
|
|
48
|
+
};
|