@holmes-lab/holmes-kit 0.19.6 → 0.20.1
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 +143 -0
- package/README.md +9 -2
- 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/doctor-check.d.ts +15 -0
- package/dist/holmes/cli/doctor-check.js +2 -0
- package/dist/holmes/cli/doctor.d.ts +2 -8
- package/dist/holmes/cli/doctor.js +12 -3
- 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/npx-cache-check.d.ts +16 -0
- package/dist/holmes/cli/npx-cache-check.js +130 -0
- package/dist/holmes/cli/playbook-skills.d.ts +2 -22
- package/dist/holmes/cli/playbook-skills.js +10 -54
- package/dist/holmes/cli/probe-process.d.ts +17 -2
- package/dist/holmes/cli/probe-process.js +44 -11
- 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 +91 -13
- 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 +72 -0
- package/dist/holmes/mcp/handlers/slice-orchestration.js +373 -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 +661 -424
- package/dist/holmes/mcp/handlers.js +270 -3251
- package/dist/holmes/mcp/server-specs-dir.d.ts +2 -0
- package/dist/holmes/mcp/server-specs-dir.js +71 -0
- package/dist/holmes/mcp/server.js +12 -2
- 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 +121 -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 +12 -1
- package/dist/holmes/project/root.js +22 -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 +136 -5
- 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/rtm/test-scope.js +4 -1
- 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 +216 -0
- package/dist/holmes/spec/entity-integration.js +760 -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 +1053 -0
- package/dist/holmes/spec/entity-transaction.d.ts +105 -0
- package/dist/holmes/spec/entity-transaction.js +741 -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 +5 -2
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { SpecStore } from '../../spec/spec-store';
|
|
2
|
+
import { Spec } from '../../spec/spec-parser';
|
|
3
|
+
export interface SpecAuthoringContext {
|
|
4
|
+
store: SpecStore;
|
|
5
|
+
projectRootOf(root: string): string;
|
|
6
|
+
resolver(specs: Spec[]): (id: string) => Spec | null;
|
|
7
|
+
}
|
|
8
|
+
export declare function createSpecAuthoringHandlers(context: SpecAuthoringContext): {
|
|
9
|
+
spec_create(a: any): Promise<{
|
|
10
|
+
ok: boolean;
|
|
11
|
+
reason: string;
|
|
12
|
+
created?: undefined;
|
|
13
|
+
} | {
|
|
14
|
+
created: any;
|
|
15
|
+
ok?: undefined;
|
|
16
|
+
reason?: undefined;
|
|
17
|
+
}>;
|
|
18
|
+
/**
|
|
19
|
+
* @implements A-SPEC-184
|
|
20
|
+
* Raise ONE named document from an older spec format to the current one.
|
|
21
|
+
*
|
|
22
|
+
* The compatibility policy this implements: older documents are read and left alone by default,
|
|
23
|
+
* and rise only when a human points at the one they intend to use. There is deliberately no bulk
|
|
24
|
+
* path — 155 documents here and 90 in the measured adoption target, and which of them are still
|
|
25
|
+
* live specifications is a judgement only a person holds.
|
|
26
|
+
*
|
|
27
|
+
* It declares a KIND. It does not confer approval: 37 of this repository's legacy documents read
|
|
28
|
+
* `status: Approved`, and carrying that across would mint approvals that never passed the sealing
|
|
29
|
+
* act. The old value is preserved as evidence and the document restarts at `draft`.
|
|
30
|
+
*
|
|
31
|
+
* Takes no `root`: the store is bound at server construction, exactly as `spec_create` is.
|
|
32
|
+
*/
|
|
33
|
+
spec_upgrade(a: {
|
|
34
|
+
id: string;
|
|
35
|
+
}): Promise<{
|
|
36
|
+
ok: boolean;
|
|
37
|
+
reason: string;
|
|
38
|
+
upgraded?: undefined;
|
|
39
|
+
type?: undefined;
|
|
40
|
+
legacyStatus?: undefined;
|
|
41
|
+
remaining?: undefined;
|
|
42
|
+
} | {
|
|
43
|
+
ok: boolean;
|
|
44
|
+
upgraded: boolean;
|
|
45
|
+
reason: string;
|
|
46
|
+
type?: undefined;
|
|
47
|
+
legacyStatus?: undefined;
|
|
48
|
+
remaining?: undefined;
|
|
49
|
+
} | {
|
|
50
|
+
ok: boolean;
|
|
51
|
+
upgraded: boolean;
|
|
52
|
+
type: import("../../spec/spec-types").SpecType;
|
|
53
|
+
legacyStatus: string;
|
|
54
|
+
remaining: import("../../spec/validator").Finding[];
|
|
55
|
+
reason?: undefined;
|
|
56
|
+
}>;
|
|
57
|
+
};
|
|
@@ -0,0 +1,326 @@
|
|
|
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.createSpecAuthoringHandlers = createSpecAuthoringHandlers;
|
|
37
|
+
// @implements A-SPEC-618, A-SPEC-100.2, A-SPEC-128, A-SPEC-151, A-SPEC-169, A-SPEC-189, A-SPEC-420, A-SPEC-174, A-SPEC-252, A-SPEC-188, A-SPEC-146, A-SPEC-184
|
|
38
|
+
const fs = __importStar(require("node:fs"));
|
|
39
|
+
const path = __importStar(require("node:path"));
|
|
40
|
+
const spec_store_1 = require("../../spec/spec-store");
|
|
41
|
+
const validator_1 = require("../../spec/validator");
|
|
42
|
+
const spec_types_1 = require("../../spec/spec-types");
|
|
43
|
+
const legacy_format_1 = require("../../spec/legacy-format");
|
|
44
|
+
const write_target_1 = require("../../guardrail/write-target");
|
|
45
|
+
const spec_id_guard_1 = require("../spec-id-guard");
|
|
46
|
+
function createSpecAuthoringHandlers(context) {
|
|
47
|
+
const { store, projectRootOf, resolver } = context;
|
|
48
|
+
return {
|
|
49
|
+
async spec_create(a) {
|
|
50
|
+
// @implements A-SPEC-169
|
|
51
|
+
// `root` is a CONTROL argument. Without excluding it, the catch-all below stores it as
|
|
52
|
+
// frontmatter and a machine-local absolute path lands in a git-shared document. A blacklist,
|
|
53
|
+
// not a whitelist: domain fields (`priority`, `slice`, …) must keep flowing through without
|
|
54
|
+
// anyone maintaining a list of them.
|
|
55
|
+
const { type, id, title, depends_on = [], root: reqRoot, ...extra } = a;
|
|
56
|
+
// The server binds its spec store once, so a `root` naming another project cannot be honoured.
|
|
57
|
+
// Silently writing to this one is a data-integrity problem; refusing says so. Compared by
|
|
58
|
+
// resolved identity (A-SPEC-163) — `<root>//.` is the same directory, not a different project.
|
|
59
|
+
// @implements A-SPEC-189 §9 (round 11) — the question is "same PROJECT?", and this asked "does
|
|
60
|
+
// `<root>/.ax/specs` equal the bound store?" — the default layout hardcoded. On the first-class
|
|
61
|
+
// `--specs-dir docs/specs` deployment no root could ever satisfy it: measured, the project's own
|
|
62
|
+
// root was refused as another project's, so `spec_create` was unusable there. `spec_approve`
|
|
63
|
+
// learned this in round 3 and compares at project level; this is the same comparison.
|
|
64
|
+
// @implements A-SPEC-189 §16 (round 13) — the siblings all ask "did the derivation actually
|
|
65
|
+
// FIND a marker?" before speaking for a project (spec_approve, phase_status,
|
|
66
|
+
// foreignRootReason). Without it, a store whose derivation lands on itself (an absolute
|
|
67
|
+
// `--specs-dir` outside any `.ax` tree) calls itself the bound project and refuses the
|
|
68
|
+
// project's real root — the inverse defect rounds 3/9/11 fixed elsewhere.
|
|
69
|
+
// 파생은 이 분기 안에서만, 그리고 실패는 '표지를 못 찾았다'로 읽는다 — 스펙 트리를 이제
|
|
70
|
+
// 만들려는 참이라 스토어 경로가 아직 없을 수 있고, 그것이 예외가 되어선 안 된다.
|
|
71
|
+
const boundSpecsRoot = store instanceof spec_store_1.LocalMarkdownRepository ? store.specsRoot : null;
|
|
72
|
+
const derivedForCreate = (() => {
|
|
73
|
+
if (boundSpecsRoot === null)
|
|
74
|
+
return null;
|
|
75
|
+
try {
|
|
76
|
+
const d = projectRootOf(boundSpecsRoot);
|
|
77
|
+
return fs.existsSync(path.join(d, '.ax')) ? d : null;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
})();
|
|
83
|
+
if (typeof reqRoot === 'string' && reqRoot !== '' && derivedForCreate !== null) {
|
|
84
|
+
const asked = (0, write_target_1.resolveTarget)(projectRootOf(reqRoot), '.');
|
|
85
|
+
const bound = (0, write_target_1.resolveTarget)(derivedForCreate, '.');
|
|
86
|
+
if (asked !== bound) {
|
|
87
|
+
return {
|
|
88
|
+
ok: false,
|
|
89
|
+
// A-SPEC-169: 두 경로를 모두 이름한다 — 어느 저장소가 묶여 있는지 호출자가 보아야 한다.
|
|
90
|
+
reason: `이 서버는 ${bound} 프로젝트(스펙 저장소 ${boundSpecsRoot})에 바인딩되어 있습니다`
|
|
91
|
+
+ ` — 요청한 root ${reqRoot}는 ${asked}를 가리킵니다.`
|
|
92
|
+
+ ' 다른 프로젝트를 조용히 수정하지 않기 위해 거부합니다.',
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// @implements A-SPEC-420
|
|
97
|
+
// The branch above steps aside when the server's spec directory does not exist yet, so that a
|
|
98
|
+
// project can create its first spec. Right intent, but the gap let something else through:
|
|
99
|
+
// measured against the INSTALLED server from a cwd outside any project, `spec_create` with
|
|
100
|
+
// `root` naming an established other project wrote REQ-990 into the SERVER's directory and
|
|
101
|
+
// answered `{"created":"REQ-990"}`. The caller's project gained nothing and was told it had.
|
|
102
|
+
//
|
|
103
|
+
// Layout-independent on purpose: the test is not "<root>/.ax/specs equals my store" — that
|
|
104
|
+
// hardcodes the default layout and, as the note above records, made `--specs-dir docs/specs`
|
|
105
|
+
// deployments refuse their own root. It is "you named a project that already exists, and my
|
|
106
|
+
// store is not inside it", which is true whatever the specs directory is called.
|
|
107
|
+
if (typeof reqRoot === 'string' && reqRoot !== '' && derivedForCreate === null && boundSpecsRoot !== null) {
|
|
108
|
+
const real = (p2) => { try {
|
|
109
|
+
return fs.realpathSync(p2);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return path.resolve(p2);
|
|
113
|
+
} };
|
|
114
|
+
let askedRoot = null;
|
|
115
|
+
try {
|
|
116
|
+
askedRoot = real(projectRootOf(reqRoot));
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
askedRoot = null;
|
|
120
|
+
}
|
|
121
|
+
if (askedRoot !== null && fs.existsSync(path.join(askedRoot, '.ax'))
|
|
122
|
+
&& !real(boundSpecsRoot).startsWith(askedRoot + path.sep)) {
|
|
123
|
+
return {
|
|
124
|
+
ok: false,
|
|
125
|
+
reason: `이 서버의 스펙 저장소 ${boundSpecsRoot} 는 아직 만들어지지 않았고 ${askedRoot} 안에도`
|
|
126
|
+
+ ' 있지 않습니다 — 요청한 root 는 이미 스펙 저장소를 가진 다른 프로젝트입니다.'
|
|
127
|
+
+ ' 여기서 만들면 파일은 호출자가 지정한 곳이 아니라 이 서버 쪽에 생깁니다.'
|
|
128
|
+
+ ' 서버를 그 프로젝트에서 시작하거나 HOLMES_SPECS 로 그 프로젝트의 스펙 경로를 지정하십시오.',
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// @implements A-SPEC-174
|
|
133
|
+
// Refuse what can never become valid; accept what is merely not filled in yet.
|
|
134
|
+
//
|
|
135
|
+
// Measured 2026-08-12 against the installed server: `REQ-7`, `hello`, `A-SPEC-7.1` and four
|
|
136
|
+
// more were all created and are all refused at approval on `bad-id`. The id IS the filename,
|
|
137
|
+
// so the author's only repair is to delete the spec and start over — after writing the body.
|
|
138
|
+
// The stubs below are the opposite case: they are placeholders BY DESIGN, and blocking
|
|
139
|
+
// approval until someone answers them is what they are for (A-SPEC-146).
|
|
140
|
+
//
|
|
141
|
+
// Every judgement here quotes SPEC_TYPES, which is what approval reads. A second statement of
|
|
142
|
+
// the rule drifts, and the drift is either this defect again or its worse inverse — refused
|
|
143
|
+
// at creation, accepted at approval.
|
|
144
|
+
const def = spec_types_1.SPEC_TYPES[type];
|
|
145
|
+
if (!def) {
|
|
146
|
+
return { ok: false, reason: `알 수 없는 스펙 타입 "${String(type)}" — 유효한 타입: ${spec_types_1.SPEC_ORDER.join(', ')}` };
|
|
147
|
+
}
|
|
148
|
+
if (!def.idRegex.test(String(id))) {
|
|
149
|
+
return {
|
|
150
|
+
ok: false,
|
|
151
|
+
reason: `id "${String(id)}"는 ${def.type}의 형식 ${def.idRegex}에 맞지 않습니다 — 예: ${def.example}.`
|
|
152
|
+
+ ' 승인 시점에 거부될 값이므로 지금 거부합니다(id는 파일명이라 나중에 고칠 수 없습니다).',
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
for (const pid of depends_on) {
|
|
156
|
+
// The parent is judged WITHOUT resolving it: not existing yet is a legitimate state an
|
|
157
|
+
// author reaches by creating the child first. A wrong type is wrong forever.
|
|
158
|
+
const ptype = (0, spec_types_1.specTypeOfId)(String(pid));
|
|
159
|
+
if (ptype === null) {
|
|
160
|
+
return { ok: false, reason: `depends_on "${String(pid)}"는 어떤 스펙 타입의 id 형식에도 맞지 않습니다 — 그 id를 가진 스펙은 존재할 수 없습니다.` };
|
|
161
|
+
}
|
|
162
|
+
if (!def.parents.includes(ptype)) {
|
|
163
|
+
return { ok: false, reason: `depends_on "${String(pid)}"는 ${ptype}입니다 — ${def.type}의 부모는 ${def.parents.join('|') || '없음(빈 depends_on)'}이어야 합니다.` };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// @implements A-SPEC-252
|
|
167
|
+
// A new id may not LEAP past the sequence. spec_slice_init assigns nextId = max(base)+1, which
|
|
168
|
+
// is only predictable while numbering stays sequential; a hand-picked wild id (REQ-1403) once
|
|
169
|
+
// dragged max to 1403 and every slice after jumped to 1404. Refused here, at the one door that
|
|
170
|
+
// lets a manual id in — so max+1 can never be poisoned again. After the shape check on purpose:
|
|
171
|
+
// an id whose base cannot be read is the shape check's to name, not this guard's.
|
|
172
|
+
const corpus = await store.list();
|
|
173
|
+
const seq = (0, spec_id_guard_1.sequentialIdVerdict)(String(id), corpus.map((s) => s.id));
|
|
174
|
+
if (!seq.ok)
|
|
175
|
+
return { ok: false, reason: seq.reason };
|
|
176
|
+
// @implements A-SPEC-188
|
|
177
|
+
// Creation creates. Probed before this check existed: spec_create over an approved+sealed
|
|
178
|
+
// REQ returned {"created"} while the disk went draft / seal gone / prose gone — the sanctioned
|
|
179
|
+
// door destroying what risk_check guards every shell path against. Checked AFTER the format
|
|
180
|
+
// refusals on purpose: a malformed id must keep its own message (order is pinned by test).
|
|
181
|
+
// Legacy (typeless) documents count as existing too — overwriting one makes the store's
|
|
182
|
+
// orphan removal delete the differently-named original file along with its history.
|
|
183
|
+
const existing = corpus.find((s) => s.id === id);
|
|
184
|
+
if (existing) {
|
|
185
|
+
const sealed = existing.status === 'approved' || Boolean(existing.frontmatter?.approved_digest);
|
|
186
|
+
const kind = existing.type ? `${existing.type} (status: ${existing.status})` : `옛 형식 문서 (status: ${existing.status})`;
|
|
187
|
+
return {
|
|
188
|
+
ok: false,
|
|
189
|
+
reason: `${id}은(는) 이미 존재합니다 — ${kind}. `
|
|
190
|
+
+ (sealed
|
|
191
|
+
? '봉인(approved)된 문서이므로, 내용을 바꾸려면 파일을 편집한 뒤 spec_approve로 재봉인하십시오. '
|
|
192
|
+
: '그 문서를 고치려는 것이면 파일을 직접 편집하십시오. ')
|
|
193
|
+
+ '새 문서를 만들려는 것이면 다른 id를 쓰십시오. 생성은 아무것도 덮어쓰지 않습니다.',
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
const sections = Object.fromEntries(def.requiredSections.map((s) => [s, spec_types_1.FIELD_PLACEHOLDER]));
|
|
197
|
+
// Stub the required FRONTMATTER too, not just the sections. Without this every created spec
|
|
198
|
+
// fails the project's own `spec_validate` on `missing-field` the moment it is written, and
|
|
199
|
+
// the author has to rediscover the per-type field list by reading spec-types.ts. Caller-
|
|
200
|
+
// supplied values win; `coverage` needs a shaped default because T-SPEC reads its keys.
|
|
201
|
+
// `source` is a citation LIST, so a bare 'TODO' string would fail the shape check the moment
|
|
202
|
+
// the spec is written. The stub is validly shaped but obviously unfilled: it validates while
|
|
203
|
+
// the REQ is drafted and blocks approval until a real origin replaces the placeholder.
|
|
204
|
+
const stub = (f) => {
|
|
205
|
+
if (f === 'coverage')
|
|
206
|
+
return { normal: false, corner: false, negative: false, boundary: false };
|
|
207
|
+
if (f === 'source')
|
|
208
|
+
return [{ kind: 'other', ref: spec_types_1.CITATION_PLACEHOLDER, note: 'cite the real origin before approval' }];
|
|
209
|
+
return spec_types_1.FIELD_PLACEHOLDER;
|
|
210
|
+
};
|
|
211
|
+
const required = Object.fromEntries(def.requiredFields.filter((f) => !(f in extra)).map((f) => [f, stub(f)]));
|
|
212
|
+
// @implements A-SPEC-146
|
|
213
|
+
// Scaffolded but not required by `validateSpec` — see SpecTypeDef.stubOnlyFields for why the
|
|
214
|
+
// gap exists. The stub value is `TODO`, which is NOT an accepted grade, so the field is
|
|
215
|
+
// visible to the author while the approval still refuses until it is actually answered.
|
|
216
|
+
// Pre-filling `none` would be easier and would make the formulaic answer the default, which is
|
|
217
|
+
// exactly what ADR-013 recorded as its revisit trigger.
|
|
218
|
+
const stubOnly = Object.fromEntries((def.stubOnlyFields ?? []).filter((f) => !(f in extra)).map((f) => [f, stub(f)]));
|
|
219
|
+
const spec = { id, type, title, status: 'draft', dependsOn: depends_on, frontmatter: { ...required, ...stubOnly, ...extra }, sections };
|
|
220
|
+
// @implements A-SPEC-188 — existence is a PATH question, not only a list() question.
|
|
221
|
+
// Round-2 review probed the gap: list() drops unparseable files, so a prose note or
|
|
222
|
+
// broken-YAML document sitting at the exact target path was invisible to the id check above
|
|
223
|
+
// and got silently overwritten — with the refusal text elsewhere promising the opposite.
|
|
224
|
+
// A file the store cannot read as this id is a file a human must look at first.
|
|
225
|
+
if (store instanceof spec_store_1.LocalMarkdownRepository) {
|
|
226
|
+
const target = store.targetPathFor(spec);
|
|
227
|
+
if (fs.existsSync(target)) {
|
|
228
|
+
return {
|
|
229
|
+
ok: false,
|
|
230
|
+
reason: `${id}의 목적지(${target})에 파일이 이미 있는데 스토어가 이 id의 스펙으로 읽지 못합니다`
|
|
231
|
+
+ ' — 손으로 쓰던 초안이거나 깨진 문서일 수 있습니다. 사람이 확인해 옮기거나 고치기 전까지 덮어쓰지 않습니다'
|
|
232
|
+
+ ' (doctor가 읽을 수 없는 스펙 파일을 보고합니다).',
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
try {
|
|
237
|
+
await store.write(spec);
|
|
238
|
+
}
|
|
239
|
+
catch (e) {
|
|
240
|
+
// The pre-write path check above makes this a narrow race backstop; the store guard is the
|
|
241
|
+
// authority and this act relays it in the same wording the other writers use.
|
|
242
|
+
if (e instanceof spec_store_1.TargetPathOccupiedError) {
|
|
243
|
+
return {
|
|
244
|
+
ok: false,
|
|
245
|
+
reason: `${id}의 목적지(${e.occupiedPath})에 스토어가 읽지 못하는 파일이 이미 있습니다 — 덮어쓰지 않습니다.`
|
|
246
|
+
+ ' 사람이 확인해 옮기거나 고친 뒤 다시 시도하십시오.',
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
throw e;
|
|
250
|
+
}
|
|
251
|
+
return { created: id };
|
|
252
|
+
},
|
|
253
|
+
/**
|
|
254
|
+
* @implements A-SPEC-184
|
|
255
|
+
* Raise ONE named document from an older spec format to the current one.
|
|
256
|
+
*
|
|
257
|
+
* The compatibility policy this implements: older documents are read and left alone by default,
|
|
258
|
+
* and rise only when a human points at the one they intend to use. There is deliberately no bulk
|
|
259
|
+
* path — 155 documents here and 90 in the measured adoption target, and which of them are still
|
|
260
|
+
* live specifications is a judgement only a person holds.
|
|
261
|
+
*
|
|
262
|
+
* It declares a KIND. It does not confer approval: 37 of this repository's legacy documents read
|
|
263
|
+
* `status: Approved`, and carrying that across would mint approvals that never passed the sealing
|
|
264
|
+
* act. The old value is preserved as evidence and the document restarts at `draft`.
|
|
265
|
+
*
|
|
266
|
+
* Takes no `root`: the store is bound at server construction, exactly as `spec_create` is.
|
|
267
|
+
*/
|
|
268
|
+
async spec_upgrade(a) {
|
|
269
|
+
// @implements A-SPEC-188 — same read-then-write shape as spec_approve, so the same window
|
|
270
|
+
// AND the same duplicate hazard: read() resolves the last-walked copy, and the write's orphan
|
|
271
|
+
// removal would delete the other. Refuse rather than pick a side.
|
|
272
|
+
if ((await store.list()).filter((s) => s.id === a.id).length > 1) {
|
|
273
|
+
return {
|
|
274
|
+
ok: false,
|
|
275
|
+
reason: `${a.id}이(가) 스토어에 두 번 이상 존재합니다 — 어느 사본이 진본인지 도구가 고를 수 없어 보강 전에 거부합니다.`
|
|
276
|
+
+ ' doctor로 중복 파일을 확인해 하나로 정리한 뒤 다시 시도하십시오.',
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
const cur = await store.read(a.id);
|
|
280
|
+
if (!cur)
|
|
281
|
+
return { ok: false, reason: `spec ${a.id} not found` };
|
|
282
|
+
const spec = cur.spec;
|
|
283
|
+
const plan = (0, legacy_format_1.upgradePlan)(spec);
|
|
284
|
+
if (!plan) {
|
|
285
|
+
const why = (0, legacy_format_1.legacyMessage)(spec);
|
|
286
|
+
// No plan for two opposite reasons. Already current is success and writes nothing — a second
|
|
287
|
+
// run must not churn the file. An unsupported kind is a refusal, and its wording must not
|
|
288
|
+
// send the holder after a type that does not exist.
|
|
289
|
+
return why ? { ok: false, reason: why } : { ok: true, upgraded: false, reason: `${a.id}은(는) 이미 현행 양식입니다` };
|
|
290
|
+
}
|
|
291
|
+
const upgraded = {
|
|
292
|
+
...spec,
|
|
293
|
+
type: plan.type,
|
|
294
|
+
status: 'draft',
|
|
295
|
+
frontmatter: (0, legacy_format_1.upgradedFrontmatter)(spec, plan),
|
|
296
|
+
};
|
|
297
|
+
try {
|
|
298
|
+
await store.write(upgraded, { expectedVersion: cur.version });
|
|
299
|
+
}
|
|
300
|
+
catch (e) {
|
|
301
|
+
if (e instanceof spec_store_1.SpecVersionConflictError) {
|
|
302
|
+
return {
|
|
303
|
+
ok: false,
|
|
304
|
+
reason: `보강 진행 중 ${a.id}이(가) 바뀌었습니다 — 바뀐 내용을 확인하고 다시 시도하십시오.`
|
|
305
|
+
+ ' 이번 보강은 아무것도 쓰지 않았습니다.',
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
// @implements A-SPEC-188 — the upgrade RELOCATES by design (legacy filename → canonical),
|
|
309
|
+
// so an unreadable file at the canonical path is the store's occupied-target refusal.
|
|
310
|
+
if (e instanceof spec_store_1.TargetPathOccupiedError) {
|
|
311
|
+
return {
|
|
312
|
+
ok: false,
|
|
313
|
+
reason: `${a.id}의 목적지(${e.occupiedPath})에 스토어가 읽지 못하는 파일이 이미 있습니다 — 덮어쓰지 않습니다.`
|
|
314
|
+
+ ' 사람이 확인해 옮기거나 고친 뒤 다시 시도하십시오. 이번 보강은 아무것도 쓰지 않았습니다.',
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
throw e;
|
|
318
|
+
}
|
|
319
|
+
// Report what still stands between this document and approval. Silence would read as "done",
|
|
320
|
+
// and a legacy document typically lacks most required sections.
|
|
321
|
+
const after = await store.list();
|
|
322
|
+
const remaining = (0, validator_1.validateSpec)(upgraded, resolver(after)).findings.filter((f) => f.level === 'error');
|
|
323
|
+
return { ok: true, upgraded: true, type: plan.type, legacyStatus: plan.legacyStatus, remaining };
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { SpecStore } from '../../spec/spec-store';
|
|
2
|
+
import { Approval } from '../../guardrail/risk-gate';
|
|
3
|
+
export interface SpecLifecycleContext {
|
|
4
|
+
store: SpecStore;
|
|
5
|
+
resolveLedgerRoot(root?: string): {
|
|
6
|
+
ok: true;
|
|
7
|
+
root: string;
|
|
8
|
+
} | {
|
|
9
|
+
ok: false;
|
|
10
|
+
reason: string;
|
|
11
|
+
};
|
|
12
|
+
resolveHandlerApproval(root: string | undefined, approval: Approval | undefined, action: {
|
|
13
|
+
kind: string;
|
|
14
|
+
target: string;
|
|
15
|
+
}, now: string): {
|
|
16
|
+
approval: Approval;
|
|
17
|
+
source: 'env' | 'grant';
|
|
18
|
+
root?: string;
|
|
19
|
+
} | undefined;
|
|
20
|
+
refusalQueueHint(root: string | undefined, request: {
|
|
21
|
+
kind: string;
|
|
22
|
+
target: string;
|
|
23
|
+
why: string;
|
|
24
|
+
}): string;
|
|
25
|
+
}
|
|
26
|
+
export declare function createSpecLifecycleHandlers(context: SpecLifecycleContext): {
|
|
27
|
+
/**
|
|
28
|
+
* Move a document to `outdated` — the only path there.
|
|
29
|
+
*
|
|
30
|
+
* @implements A-SPEC-222
|
|
31
|
+
* `outdated` was in SPEC_STATUSES with no code writing it and no document carrying it: a
|
|
32
|
+
* declared state nothing could reach. That mattered the moment 23 deprecated REQs needed
|
|
33
|
+
* cleaning up, because the only transition tool sends everything to `draft`, and calling a
|
|
34
|
+
* retired document "draft" is a worse lie than the non-canonical word it replaced.
|
|
35
|
+
*
|
|
36
|
+
* Retirement withdraws authority rather than granting it, so it does NOT need the approval key
|
|
37
|
+
* by default. Two cases invert that, and the threshold follows the RISK rather than the name of
|
|
38
|
+
* the act:
|
|
39
|
+
*
|
|
40
|
+
* - A sealed document. The code gate demands an approved T-SPEC naming the target A-SPEC;
|
|
41
|
+
* retiring that T-SPEC removes the demand. Unguarded, retirement is an approval bypass.
|
|
42
|
+
* - A document an APPROVED spec depends on. That chain is holding something up right now.
|
|
43
|
+
*/
|
|
44
|
+
spec_retire(a: {
|
|
45
|
+
root?: string;
|
|
46
|
+
id: string;
|
|
47
|
+
reason?: string;
|
|
48
|
+
}): Promise<{
|
|
49
|
+
ok: boolean;
|
|
50
|
+
reason: string;
|
|
51
|
+
retired?: undefined;
|
|
52
|
+
id?: undefined;
|
|
53
|
+
dependents?: undefined;
|
|
54
|
+
} | {
|
|
55
|
+
ok: boolean;
|
|
56
|
+
retired: boolean;
|
|
57
|
+
id: string;
|
|
58
|
+
dependents: never[];
|
|
59
|
+
reason: string;
|
|
60
|
+
} | {
|
|
61
|
+
ok: boolean;
|
|
62
|
+
retired: boolean;
|
|
63
|
+
id: string;
|
|
64
|
+
dependents: string[];
|
|
65
|
+
reason?: undefined;
|
|
66
|
+
}>;
|
|
67
|
+
spec_renumber(a: {
|
|
68
|
+
root?: string;
|
|
69
|
+
oldBase: string;
|
|
70
|
+
newBase: string;
|
|
71
|
+
dryRun?: boolean;
|
|
72
|
+
}): Promise<{
|
|
73
|
+
ok: boolean;
|
|
74
|
+
dryRun: boolean;
|
|
75
|
+
plan: import("../../spec/renumber").RenumberPlan;
|
|
76
|
+
reason?: undefined;
|
|
77
|
+
movedSpecs?: undefined;
|
|
78
|
+
} | {
|
|
79
|
+
ok: boolean;
|
|
80
|
+
dryRun: boolean;
|
|
81
|
+
movedSpecs: number;
|
|
82
|
+
plan: import("../../spec/renumber").RenumberPlan;
|
|
83
|
+
reason?: undefined;
|
|
84
|
+
} | {
|
|
85
|
+
ok: boolean;
|
|
86
|
+
reason: string;
|
|
87
|
+
}>;
|
|
88
|
+
/**
|
|
89
|
+
* @implements A-SPEC-538.1
|
|
90
|
+
* The inverse of spec_approve: returns a SEALED (approved) spec to an editable `draft`, clearing
|
|
91
|
+
* `approved_digest` and `parent_digests` in ONE act, and records `spec-unsealed`. Hand-editing
|
|
92
|
+
* only `status` leaves seal residue that later trips validation; this atomizes the reverse.
|
|
93
|
+
*
|
|
94
|
+
* Un-sealing WITHDRAWS a seal, so — like retiring a sealed document — it demands a covering
|
|
95
|
+
* out-of-band HOLMES_APPROVAL (fail-closed): un-sealing an approved T-SPEC removes the code
|
|
96
|
+
* gate's demand, so an unguarded un-seal would be an approval bypass. And it refuses when an
|
|
97
|
+
* APPROVED spec depends on the target, because that dependent's `parent_digests` — the snapshot
|
|
98
|
+
* drift detection compares against — would silently go stale.
|
|
99
|
+
*/
|
|
100
|
+
spec_unseal(a: {
|
|
101
|
+
root?: string;
|
|
102
|
+
id: string;
|
|
103
|
+
}): Promise<{
|
|
104
|
+
ok: boolean;
|
|
105
|
+
reason: string;
|
|
106
|
+
unsealed?: undefined;
|
|
107
|
+
id?: undefined;
|
|
108
|
+
dependents?: undefined;
|
|
109
|
+
} | {
|
|
110
|
+
ok: boolean;
|
|
111
|
+
unsealed: boolean;
|
|
112
|
+
id: string;
|
|
113
|
+
dependents: never[];
|
|
114
|
+
reason: string;
|
|
115
|
+
} | {
|
|
116
|
+
ok: boolean;
|
|
117
|
+
unsealed: boolean;
|
|
118
|
+
id: string;
|
|
119
|
+
dependents: string[];
|
|
120
|
+
reason?: undefined;
|
|
121
|
+
}>;
|
|
122
|
+
};
|