@nac3/forge-cli 1.19.4 → 1.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/dist/bin/yf.d.ts.map +1 -1
- package/dist/bin/yf.js +2 -1
- package/dist/bin/yf.js.map +1 -1
- package/dist/chat/instance_lock.d.ts +98 -0
- package/dist/chat/instance_lock.d.ts.map +1 -1
- package/dist/chat/instance_lock.js +135 -11
- package/dist/chat/instance_lock.js.map +1 -1
- package/dist/chat/instances.d.ts +84 -0
- package/dist/chat/instances.d.ts.map +1 -0
- package/dist/chat/instances.js +224 -0
- package/dist/chat/instances.js.map +1 -0
- package/dist/chat/panel.d.ts.map +1 -1
- package/dist/chat/panel.js +284 -6
- package/dist/chat/panel.js.map +1 -1
- package/dist/chat/server.d.ts.map +1 -1
- package/dist/chat/server.js +64 -1
- package/dist/chat/server.js.map +1 -1
- package/dist/chat/tools/evidence.d.ts.map +1 -1
- package/dist/chat/tools/evidence.js +7 -3
- package/dist/chat/tools/evidence.js.map +1 -1
- package/dist/chat/tools.d.ts.map +1 -1
- package/dist/chat/tools.js +26 -3
- package/dist/chat/tools.js.map +1 -1
- package/dist/commands/task.d.ts +13 -0
- package/dist/commands/task.d.ts.map +1 -1
- package/dist/commands/task.js +148 -3
- package/dist/commands/task.js.map +1 -1
- package/dist/commands/upgrade.d.ts +15 -0
- package/dist/commands/upgrade.d.ts.map +1 -1
- package/dist/commands/upgrade.js +63 -0
- package/dist/commands/upgrade.js.map +1 -1
- package/dist/commands/urazuke.d.ts.map +1 -1
- package/dist/commands/urazuke.js +68 -1
- package/dist/commands/urazuke.js.map +1 -1
- package/dist/core/forge_profile.d.ts.map +1 -1
- package/dist/core/forge_profile.js +41 -4
- package/dist/core/forge_profile.js.map +1 -1
- package/dist/core/whats_new.d.ts.map +1 -1
- package/dist/core/whats_new.js +14 -0
- package/dist/core/whats_new.js.map +1 -1
- package/dist/tasks/supervisor.d.ts +14 -0
- package/dist/tasks/supervisor.d.ts.map +1 -1
- package/dist/tasks/supervisor.js +34 -9
- package/dist/tasks/supervisor.js.map +1 -1
- package/dist/tasks/win_spawn.d.ts +60 -0
- package/dist/tasks/win_spawn.d.ts.map +1 -0
- package/dist/tasks/win_spawn.js +189 -0
- package/dist/tasks/win_spawn.js.map +1 -0
- package/dist/urazuke/current.d.ts +5 -0
- package/dist/urazuke/current.d.ts.map +1 -1
- package/dist/urazuke/current.js +14 -3
- package/dist/urazuke/current.js.map +1 -1
- package/dist/urazuke/cycle.d.ts.map +1 -1
- package/dist/urazuke/cycle.js +6 -2
- package/dist/urazuke/cycle.js.map +1 -1
- package/dist/urazuke/evidence.d.ts.map +1 -1
- package/dist/urazuke/evidence.js +9 -1
- package/dist/urazuke/evidence.js.map +1 -1
- package/dist/urazuke/freeze.d.ts +29 -0
- package/dist/urazuke/freeze.d.ts.map +1 -0
- package/dist/urazuke/freeze.js +150 -0
- package/dist/urazuke/freeze.js.map +1 -0
- package/dist/urazuke/gate.d.ts +18 -1
- package/dist/urazuke/gate.d.ts.map +1 -1
- package/dist/urazuke/gate.js +21 -0
- package/dist/urazuke/gate.js.map +1 -1
- package/dist/urazuke/gates_data.d.ts +219 -0
- package/dist/urazuke/gates_data.d.ts.map +1 -0
- package/dist/urazuke/gates_data.js +441 -0
- package/dist/urazuke/gates_data.js.map +1 -0
- package/dist/urazuke/prompts_client.d.ts +6 -0
- package/dist/urazuke/prompts_client.d.ts.map +1 -1
- package/dist/urazuke/prompts_client.js +1 -1
- package/dist/urazuke/prompts_client.js.map +1 -1
- package/dist/urazuke/receipts.d.ts +53 -1
- package/dist/urazuke/receipts.d.ts.map +1 -1
- package/dist/urazuke/receipts.js +65 -3
- package/dist/urazuke/receipts.js.map +1 -1
- package/dist/urazuke/ruleset.d.ts +56 -0
- package/dist/urazuke/ruleset.d.ts.map +1 -0
- package/dist/urazuke/ruleset.js +159 -0
- package/dist/urazuke/ruleset.js.map +1 -0
- package/dist/urazuke/start.d.ts +5 -0
- package/dist/urazuke/start.d.ts.map +1 -1
- package/dist/urazuke/start.js +39 -2
- package/dist/urazuke/start.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +20 -18
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { type UrazukeArtifact, type UrazukeTramo, type UrazukeForm, type UrazukeMandatory } from './catalog.js';
|
|
2
|
+
import { type GatesSource } from './ruleset.js';
|
|
3
|
+
/** Where the method publishes it. Only `fetchPublishedGates` reads this,
|
|
4
|
+
* and only the explicit pull calls that. */
|
|
5
|
+
export declare const DEFAULT_GATES_URL = "https://yujin.app/urazuke/api/gates.json";
|
|
6
|
+
/** The frozen copy, inside the project. Next to the prompt overrides
|
|
7
|
+
* (`.forge/urazuke-prompts.json`) on purpose: the two halves of the same
|
|
8
|
+
* freeze belong together, and a reader looking for one finds the other.
|
|
9
|
+
*
|
|
10
|
+
* This file is meant to be COMMITTED. The prevailing convention for
|
|
11
|
+
* `.forge/` in a project is local state -- `workflow-config.json` is in
|
|
12
|
+
* .gitignore -- and the freeze has to break that convention to mean
|
|
13
|
+
* anything: a copy that does not travel with the repository is a freeze
|
|
14
|
+
* that thaws on the next clone and on every CI run, where no copy is found
|
|
15
|
+
* and the newest definition answers instead. A lock that only closes on one
|
|
16
|
+
* laptop. See docs/PROPUESTA_URAZUKE_VERSION_POR_PROYECTO_2026_10_22.md. */
|
|
17
|
+
export declare const GATES_FROZEN_REL: string;
|
|
18
|
+
export declare function frozenGatesPath(projectRoot: string): string;
|
|
19
|
+
/** The highest published schema this Forge knows how to read. A file that
|
|
20
|
+
* declares more than this is not refused -- the schema is append-only by
|
|
21
|
+
* the method's own rule -- but what it adds is reported as unread rather
|
|
22
|
+
* than silently dropped. */
|
|
23
|
+
export declare const MAX_SCHEMA_VERSION = 2;
|
|
24
|
+
/** One evaluator a step asks for. `required` decides the compatibility
|
|
25
|
+
* branch when this installation does not implement it. */
|
|
26
|
+
export interface PublishedEvaluatorRef {
|
|
27
|
+
name: string;
|
|
28
|
+
since_schema?: number;
|
|
29
|
+
required?: boolean;
|
|
30
|
+
}
|
|
31
|
+
/** An artifact rule as published. Same fields as the compiled
|
|
32
|
+
* `UrazukeArtifact` plus the schema marker the method uses to tell the
|
|
33
|
+
* frozen base apart from what it added afterwards. */
|
|
34
|
+
export interface PublishedArtifact extends UrazukeArtifact {
|
|
35
|
+
since_schema?: number;
|
|
36
|
+
anchor?: number;
|
|
37
|
+
}
|
|
38
|
+
export interface PublishedStep {
|
|
39
|
+
num: number;
|
|
40
|
+
id: string;
|
|
41
|
+
tramo?: string;
|
|
42
|
+
title: string;
|
|
43
|
+
summary?: string;
|
|
44
|
+
form?: string;
|
|
45
|
+
mandatory?: string;
|
|
46
|
+
prompt_key: string;
|
|
47
|
+
prompt_indexes?: number[];
|
|
48
|
+
inputs?: PublishedArtifact[];
|
|
49
|
+
artifacts?: PublishedArtifact[];
|
|
50
|
+
verify?: unknown[];
|
|
51
|
+
evaluators?: PublishedEvaluatorRef[];
|
|
52
|
+
}
|
|
53
|
+
export interface PublishedContentHash {
|
|
54
|
+
algo: string;
|
|
55
|
+
encoding: string;
|
|
56
|
+
canonical_form: string;
|
|
57
|
+
covers: string[];
|
|
58
|
+
excludes: string[];
|
|
59
|
+
value: string;
|
|
60
|
+
note?: string;
|
|
61
|
+
}
|
|
62
|
+
export interface PublishedGatesFile {
|
|
63
|
+
schema_version: number;
|
|
64
|
+
catalog_version: string;
|
|
65
|
+
content_hash: PublishedContentHash;
|
|
66
|
+
evaluators?: {
|
|
67
|
+
name: string;
|
|
68
|
+
since_schema?: number;
|
|
69
|
+
checks?: string;
|
|
70
|
+
}[];
|
|
71
|
+
compatibility?: Record<string, string>;
|
|
72
|
+
steps: PublishedStep[];
|
|
73
|
+
stations: PublishedStep[];
|
|
74
|
+
/** Everything else the method publishes (tramos, heuristics, adapters,
|
|
75
|
+
* project_config, languages, loop_entry, ...). Kept verbatim: the hash
|
|
76
|
+
* covers it, so dropping a key on the way in would break the seal. */
|
|
77
|
+
[key: string]: unknown;
|
|
78
|
+
}
|
|
79
|
+
/** The payload the declared hash covers, in the order the file declares.
|
|
80
|
+
* Built from `content_hash.covers` and not from a list of our own: the
|
|
81
|
+
* method owns which keys bear the requirement, and a consumer that keeps
|
|
82
|
+
* its own list stops agreeing the day the method adds one. */
|
|
83
|
+
export declare function coveredPayload(file: PublishedGatesFile): Record<string, unknown>;
|
|
84
|
+
export interface HashVerdict {
|
|
85
|
+
ok: boolean;
|
|
86
|
+
declared: string;
|
|
87
|
+
recomputed: string;
|
|
88
|
+
/** Top-level keys the file neither covers nor excludes. Not a detail: a
|
|
89
|
+
* key outside both lists is content nobody is protecting, and a frozen
|
|
90
|
+
* copy altered exactly there would still match its hash. The method
|
|
91
|
+
* raises its own error for this; we check it rather than trust it. */
|
|
92
|
+
uncovered: string[];
|
|
93
|
+
}
|
|
94
|
+
/** Recompute the content hash over the canonicalised covered payload and
|
|
95
|
+
* compare. The canonical form is the one the method declares
|
|
96
|
+
* (`json_sorted_keys_codepoint_no_whitespace_utf8`, sha256, hex lower),
|
|
97
|
+
* which is also the one `ruleset.ts` uses -- so one implementation serves
|
|
98
|
+
* both the seal and this check, and the two sides cannot drift apart. */
|
|
99
|
+
export declare function verifyContentHash(file: PublishedGatesFile): HashVerdict;
|
|
100
|
+
export interface ValidationResult {
|
|
101
|
+
ok: boolean;
|
|
102
|
+
file?: PublishedGatesFile;
|
|
103
|
+
problems: string[];
|
|
104
|
+
/** The hash verdict, when the shape was good enough to compute one. */
|
|
105
|
+
hash?: HashVerdict;
|
|
106
|
+
/** The file declares a schema this Forge does not fully know. Not a
|
|
107
|
+
* refusal: the schema is append-only, so an older reader stays correct
|
|
108
|
+
* about what it does read. It IS reported, because what it adds may
|
|
109
|
+
* carry requirements, and silence there is the silent damage. */
|
|
110
|
+
newer_schema?: number;
|
|
111
|
+
}
|
|
112
|
+
/** Validate a payload that claims to be a published gate definition.
|
|
113
|
+
*
|
|
114
|
+
* Refuses on: a broken shape, or a hash that does not recompute. The second
|
|
115
|
+
* one is the whole point of Pieza 2 -- a definition we cannot verify is not
|
|
116
|
+
* a definition we may enforce, because the only thing that makes a frozen
|
|
117
|
+
* copy trustworthy offline is that its content reproduces its own seal. */
|
|
118
|
+
export declare function validatePublishedGates(raw: unknown): ValidationResult;
|
|
119
|
+
export interface ResolvedEvaluator {
|
|
120
|
+
name: string;
|
|
121
|
+
required: boolean;
|
|
122
|
+
since_schema: number;
|
|
123
|
+
}
|
|
124
|
+
/** One step or station, in the one shape `gate.ts` evaluates -- whether it
|
|
125
|
+
* came from the compiled catalogue or from a published file. */
|
|
126
|
+
export interface ResolvedGateStep {
|
|
127
|
+
num: number;
|
|
128
|
+
id: string;
|
|
129
|
+
title: string;
|
|
130
|
+
tramo?: UrazukeTramo | string;
|
|
131
|
+
summary?: string;
|
|
132
|
+
form?: UrazukeForm | string;
|
|
133
|
+
mandatory?: UrazukeMandatory | string;
|
|
134
|
+
prompt_key: string;
|
|
135
|
+
inputs: UrazukeArtifact[];
|
|
136
|
+
artifacts: UrazukeArtifact[];
|
|
137
|
+
verify: string[];
|
|
138
|
+
evaluators: ResolvedEvaluator[];
|
|
139
|
+
}
|
|
140
|
+
export interface ResolvedGates {
|
|
141
|
+
source: GatesSource;
|
|
142
|
+
version: string;
|
|
143
|
+
/** Recomputed here. For `compiled` it is the requirement hash of
|
|
144
|
+
* catalog.ts; for a file, its verified content hash. The two are
|
|
145
|
+
* different domains and must not be compared across sources -- the
|
|
146
|
+
* source is part of the identity, which is why the stamp carries both. */
|
|
147
|
+
hash: string;
|
|
148
|
+
schema_version: number;
|
|
149
|
+
steps: ResolvedGateStep[];
|
|
150
|
+
stations: ResolvedGateStep[];
|
|
151
|
+
/** The evaluator vocabulary the definition declares. Informative: what
|
|
152
|
+
* decides is whether THIS Forge implements each name. */
|
|
153
|
+
declared_evaluators: string[];
|
|
154
|
+
/** Things the reader wants said out loud: a refused frozen copy, a schema
|
|
155
|
+
* newer than this reader, a definition with no artifacts. Never swallowed
|
|
156
|
+
* -- a resolution that quietly fell back is the bug this module exists
|
|
157
|
+
* to prevent. */
|
|
158
|
+
notices: string[];
|
|
159
|
+
}
|
|
160
|
+
/** The published definition, in the resolved shape. */
|
|
161
|
+
export declare function publishedToResolved(file: PublishedGatesFile, source: GatesSource, hash: string): ResolvedGates;
|
|
162
|
+
/** Forge's own compiled catalogue, in the same resolved shape.
|
|
163
|
+
*
|
|
164
|
+
* This is the BASE: what Forge enforced before the method took ownership,
|
|
165
|
+
* and what a project freezes when it is started by a Forge that has not
|
|
166
|
+
* pulled a published definition. Every step declares exactly one evaluator
|
|
167
|
+
* and every one of them is required -- the compiled catalogue has no notion
|
|
168
|
+
* of an optional evaluator, which is correct, because it also has no
|
|
169
|
+
* evaluator it cannot measure. */
|
|
170
|
+
export declare function compiledGatesResolved(): ResolvedGates;
|
|
171
|
+
export interface FrozenReadResult {
|
|
172
|
+
/** The definition, when the file was there AND verified. */
|
|
173
|
+
gates?: ResolvedGates;
|
|
174
|
+
/** No file: the normal case for a project that has not been frozen. */
|
|
175
|
+
absent: boolean;
|
|
176
|
+
/** The file was there and was REFUSED. The reason is for the owner, not
|
|
177
|
+
* for a log nobody reads: a refused freeze means the project is being
|
|
178
|
+
* judged by the compiled rule and not by the one it thought it froze. */
|
|
179
|
+
refused?: string[];
|
|
180
|
+
}
|
|
181
|
+
export declare function readFrozenGates(projectRoot: string): Promise<FrozenReadResult>;
|
|
182
|
+
/** Write the frozen copy. Atomic (tmp + rename) for the same reason the
|
|
183
|
+
* prompt overrides are: a half-written definition read by the next turn is
|
|
184
|
+
* a project with no rule at all. */
|
|
185
|
+
export declare function writeFrozenGates(projectRoot: string, file: PublishedGatesFile): Promise<void>;
|
|
186
|
+
/** Serialise the compiled catalogue INTO the published shape, sealed with a
|
|
187
|
+
* hash computed the published way, so a frozen copy of the base is read
|
|
188
|
+
* back by exactly the same path as a frozen copy of the portal's.
|
|
189
|
+
*
|
|
190
|
+
* This is what "freeze every existing project with what it reads today"
|
|
191
|
+
* writes: the base, unchanged, with nothing added. The first day the
|
|
192
|
+
* difference is exactly zero and it can be demonstrated -- that is the
|
|
193
|
+
* criterion the whole proposal rests on. */
|
|
194
|
+
export declare function compiledAsPublishedFile(): PublishedGatesFile;
|
|
195
|
+
/** WHICH gate definition is in force for this project.
|
|
196
|
+
*
|
|
197
|
+
* Precedence: the project's frozen copy, then the compiled catalogue. The
|
|
198
|
+
* portal is deliberately absent -- see rule 1 at the top of this file. A
|
|
199
|
+
* refused frozen copy does NOT promote the portal and does not fail the
|
|
200
|
+
* read: it falls back to the compiled catalogue, which is the safe state
|
|
201
|
+
* (nothing can tighten without an install), and it says so. */
|
|
202
|
+
export declare function resolveGates(projectRoot?: string): Promise<ResolvedGates>;
|
|
203
|
+
export interface FetchGatesOptions {
|
|
204
|
+
fetchImpl?: typeof fetch;
|
|
205
|
+
url?: string;
|
|
206
|
+
timeout_ms?: number;
|
|
207
|
+
}
|
|
208
|
+
/** Bring the published definition over the network.
|
|
209
|
+
*
|
|
210
|
+
* NOT part of `resolveGates`, on purpose and by decision: the owner asked
|
|
211
|
+
* that a project start and finish on the same version of the method, so
|
|
212
|
+
* the only legitimate caller is the explicit double lock (Pieza 4), which
|
|
213
|
+
* has to show how many green steps would turn red BEFORE it writes
|
|
214
|
+
* anything. A validated file or nothing -- an unverifiable definition is
|
|
215
|
+
* never handed back. */
|
|
216
|
+
export declare function fetchPublishedGates(opts?: FetchGatesOptions): Promise<ValidationResult>;
|
|
217
|
+
/** One line for a report or the CLI. */
|
|
218
|
+
export declare function renderGatesSource(g: ResolvedGates): string;
|
|
219
|
+
//# sourceMappingURL=gates_data.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gates_data.d.ts","sourceRoot":"","sources":["../../src/urazuke/gates_data.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE GATE DEFINITION AS DATA -- reading the gates the method publishes
|
|
3
|
+
* (PND-165, Pieza 2).
|
|
4
|
+
*
|
|
5
|
+
* Until 2026-10-23 the mechanical definition of the gates lived compiled
|
|
6
|
+
* inside Forge (`catalog.ts`): 24 steps, 8 stations, one evaluator each. On
|
|
7
|
+
* that day the method took ownership of it and started publishing it as
|
|
8
|
+
* data (D-050), the same way it already published the prompts. So a
|
|
9
|
+
* requirement of the method no longer waits for a Forge release -- which is
|
|
10
|
+
* the point -- and a project can now be re-read under a rule that did not
|
|
11
|
+
* exist when its evidence was produced, which is the danger. The owner's
|
|
12
|
+
* decision bounds that: a project starts and finishes on the same version of
|
|
13
|
+
* the method unless somebody presses the double lock.
|
|
14
|
+
*
|
|
15
|
+
* This module is the READ side of that decision. Three rules it enforces,
|
|
16
|
+
* each one because its absence turns the freeze into a label:
|
|
17
|
+
*
|
|
18
|
+
* 1. PRECEDENCE: the copy frozen in the project wins over everything.
|
|
19
|
+
* Second, and only second, Forge's own compiled catalogue. The portal
|
|
20
|
+
* is NOT on this path -- `fetchPublishedGates` exists for the explicit
|
|
21
|
+
* pull (Pieza 4) and nothing here calls it. A read that can reach the
|
|
22
|
+
* network is a read that can change a project's rule between two
|
|
23
|
+
* turns, and no decision asked for that.
|
|
24
|
+
*
|
|
25
|
+
* 2. THE HASH IS RECOMPUTED, NEVER READ. A frozen copy edited by hand or
|
|
26
|
+
* truncated by a merge has to be detectable without the network. A
|
|
27
|
+
* stamp that only repeats what the file claims about itself is a
|
|
28
|
+
* rotulo, not a seal (adenda 2). A frozen copy whose recomputed hash
|
|
29
|
+
* disagrees with its own declared value is REFUSED, loudly, and the
|
|
30
|
+
* compiled catalogue answers instead -- a tampered rule must never be
|
|
31
|
+
* the rule in force.
|
|
32
|
+
*
|
|
33
|
+
* 3. AN UNKNOWN EVALUATOR IS NOT A GREEN AND NOT AN APPROXIMATION. The
|
|
34
|
+
* published catalogue already declares twelve evaluators and this
|
|
35
|
+
* Forge implements eight. The method's own compatibility clause says
|
|
36
|
+
* what to do: a REQUIRED evaluator this installation cannot measure is
|
|
37
|
+
* reported as "declared, not measured by this installation", naming it,
|
|
38
|
+
* and the step does not advance. Approximating with the closest
|
|
39
|
+
* evaluator, or ignoring the block, is forbidden. Zero silent damage.
|
|
40
|
+
*
|
|
41
|
+
* What this module does NOT do: evaluate. It resolves WHICH definition is in
|
|
42
|
+
* force and hands it over in one shape, whatever its source. `gate.ts`
|
|
43
|
+
* measures.
|
|
44
|
+
*
|
|
45
|
+
* ASCII-only.
|
|
46
|
+
*/
|
|
47
|
+
import fs from 'node:fs/promises';
|
|
48
|
+
import path from 'node:path';
|
|
49
|
+
import { URAZUKE_STEPS, URAZUKE_LEGACY_STATIONS, } from './catalog.js';
|
|
50
|
+
import { canonicalJson, sha256Hex, compiledGatesHash, FORGE_COMPILED_GATES_VERSION, } from './ruleset.js';
|
|
51
|
+
/** Where the method publishes it. Only `fetchPublishedGates` reads this,
|
|
52
|
+
* and only the explicit pull calls that. */
|
|
53
|
+
export const DEFAULT_GATES_URL = 'https://yujin.app/urazuke/api/gates.json';
|
|
54
|
+
/** The frozen copy, inside the project. Next to the prompt overrides
|
|
55
|
+
* (`.forge/urazuke-prompts.json`) on purpose: the two halves of the same
|
|
56
|
+
* freeze belong together, and a reader looking for one finds the other.
|
|
57
|
+
*
|
|
58
|
+
* This file is meant to be COMMITTED. The prevailing convention for
|
|
59
|
+
* `.forge/` in a project is local state -- `workflow-config.json` is in
|
|
60
|
+
* .gitignore -- and the freeze has to break that convention to mean
|
|
61
|
+
* anything: a copy that does not travel with the repository is a freeze
|
|
62
|
+
* that thaws on the next clone and on every CI run, where no copy is found
|
|
63
|
+
* and the newest definition answers instead. A lock that only closes on one
|
|
64
|
+
* laptop. See docs/PROPUESTA_URAZUKE_VERSION_POR_PROYECTO_2026_10_22.md. */
|
|
65
|
+
export const GATES_FROZEN_REL = path.join('.forge', 'urazuke-gates.json');
|
|
66
|
+
export function frozenGatesPath(projectRoot) {
|
|
67
|
+
return path.join(projectRoot, GATES_FROZEN_REL);
|
|
68
|
+
}
|
|
69
|
+
/** The highest published schema this Forge knows how to read. A file that
|
|
70
|
+
* declares more than this is not refused -- the schema is append-only by
|
|
71
|
+
* the method's own rule -- but what it adds is reported as unread rather
|
|
72
|
+
* than silently dropped. */
|
|
73
|
+
export const MAX_SCHEMA_VERSION = 2;
|
|
74
|
+
/* ---------------- the hash ---------------- */
|
|
75
|
+
/** The payload the declared hash covers, in the order the file declares.
|
|
76
|
+
* Built from `content_hash.covers` and not from a list of our own: the
|
|
77
|
+
* method owns which keys bear the requirement, and a consumer that keeps
|
|
78
|
+
* its own list stops agreeing the day the method adds one. */
|
|
79
|
+
export function coveredPayload(file) {
|
|
80
|
+
const out = {};
|
|
81
|
+
for (const k of file.content_hash.covers)
|
|
82
|
+
out[k] = file[k];
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
/** Recompute the content hash over the canonicalised covered payload and
|
|
86
|
+
* compare. The canonical form is the one the method declares
|
|
87
|
+
* (`json_sorted_keys_codepoint_no_whitespace_utf8`, sha256, hex lower),
|
|
88
|
+
* which is also the one `ruleset.ts` uses -- so one implementation serves
|
|
89
|
+
* both the seal and this check, and the two sides cannot drift apart. */
|
|
90
|
+
export function verifyContentHash(file) {
|
|
91
|
+
const recomputed = sha256Hex(canonicalJson(coveredPayload(file)));
|
|
92
|
+
const accounted = new Set([...file.content_hash.covers, ...file.content_hash.excludes]);
|
|
93
|
+
const uncovered = Object.keys(file).filter((k) => !accounted.has(k)).sort();
|
|
94
|
+
return {
|
|
95
|
+
ok: recomputed === file.content_hash.value && uncovered.length === 0,
|
|
96
|
+
declared: file.content_hash.value,
|
|
97
|
+
recomputed,
|
|
98
|
+
uncovered,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/* ---------------- validation ---------------- */
|
|
102
|
+
function isArtifactArray(x) {
|
|
103
|
+
return Array.isArray(x) && x.every((a) => a && typeof a === 'object' && typeof a.path === 'string');
|
|
104
|
+
}
|
|
105
|
+
function validStepArray(x, label, problems) {
|
|
106
|
+
if (!Array.isArray(x) || !x.length) {
|
|
107
|
+
problems.push(label + ': missing or empty');
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
let ok = true;
|
|
111
|
+
for (const s of x) {
|
|
112
|
+
if (!s || typeof s !== 'object') {
|
|
113
|
+
problems.push(label + ': a non-object entry');
|
|
114
|
+
ok = false;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (typeof s.id !== 'string' || !s.id) {
|
|
118
|
+
problems.push(label + ': an entry with no id');
|
|
119
|
+
ok = false;
|
|
120
|
+
}
|
|
121
|
+
if (typeof s.num !== 'number') {
|
|
122
|
+
problems.push(label + ' ' + s.id + ': num is not a number');
|
|
123
|
+
ok = false;
|
|
124
|
+
}
|
|
125
|
+
if (typeof s.title !== 'string') {
|
|
126
|
+
problems.push(label + ' ' + s.id + ': title is not a string');
|
|
127
|
+
ok = false;
|
|
128
|
+
}
|
|
129
|
+
if (s.inputs !== undefined && !isArtifactArray(s.inputs)) {
|
|
130
|
+
problems.push(label + ' ' + s.id + ': inputs is not a list of artifacts');
|
|
131
|
+
ok = false;
|
|
132
|
+
}
|
|
133
|
+
if (s.artifacts !== undefined && !isArtifactArray(s.artifacts)) {
|
|
134
|
+
problems.push(label + ' ' + s.id + ': artifacts is not a list of artifacts');
|
|
135
|
+
ok = false;
|
|
136
|
+
}
|
|
137
|
+
if (s.evaluators !== undefined) {
|
|
138
|
+
if (!Array.isArray(s.evaluators) || !s.evaluators.every((e) => e && typeof e.name === 'string')) {
|
|
139
|
+
problems.push(label + ' ' + s.id + ': evaluators is not a list of {name}');
|
|
140
|
+
ok = false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return ok;
|
|
145
|
+
}
|
|
146
|
+
/** Validate a payload that claims to be a published gate definition.
|
|
147
|
+
*
|
|
148
|
+
* Refuses on: a broken shape, or a hash that does not recompute. The second
|
|
149
|
+
* one is the whole point of Pieza 2 -- a definition we cannot verify is not
|
|
150
|
+
* a definition we may enforce, because the only thing that makes a frozen
|
|
151
|
+
* copy trustworthy offline is that its content reproduces its own seal. */
|
|
152
|
+
export function validatePublishedGates(raw) {
|
|
153
|
+
const problems = [];
|
|
154
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
155
|
+
return { ok: false, problems: ['not a JSON object'] };
|
|
156
|
+
}
|
|
157
|
+
const f = raw;
|
|
158
|
+
if (typeof f.schema_version !== 'number')
|
|
159
|
+
problems.push('schema_version missing or not a number');
|
|
160
|
+
if (typeof f.catalog_version !== 'string' || !f.catalog_version)
|
|
161
|
+
problems.push('catalog_version missing');
|
|
162
|
+
const ch = f.content_hash;
|
|
163
|
+
if (!ch || typeof ch !== 'object' || typeof ch.value !== 'string'
|
|
164
|
+
|| !Array.isArray(ch.covers) || !Array.isArray(ch.excludes)) {
|
|
165
|
+
problems.push('content_hash missing or incomplete (needs value + covers + excludes)');
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
if (ch.algo && ch.algo !== 'sha256')
|
|
169
|
+
problems.push('content_hash.algo is ' + ch.algo + ', and this reader only does sha256');
|
|
170
|
+
if (ch.canonical_form && ch.canonical_form !== 'json_sorted_keys_codepoint_no_whitespace_utf8') {
|
|
171
|
+
problems.push('content_hash.canonical_form is ' + ch.canonical_form + ', not the form this reader canonicalises to');
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
validStepArray(f.steps, 'steps', problems);
|
|
175
|
+
validStepArray(f.stations, 'stations', problems);
|
|
176
|
+
if (problems.length)
|
|
177
|
+
return { ok: false, problems };
|
|
178
|
+
const hash = verifyContentHash(f);
|
|
179
|
+
if (!hash.ok) {
|
|
180
|
+
if (hash.recomputed !== hash.declared) {
|
|
181
|
+
problems.push('content hash does not recompute: declared ' + hash.declared.slice(0, 16)
|
|
182
|
+
+ ', recomputed ' + hash.recomputed.slice(0, 16)
|
|
183
|
+
+ ' -- the content and its seal disagree, so this definition cannot be trusted');
|
|
184
|
+
}
|
|
185
|
+
if (hash.uncovered.length) {
|
|
186
|
+
problems.push('top-level key(s) neither covered nor excluded by content_hash: ' + hash.uncovered.join(', ')
|
|
187
|
+
+ ' -- content nobody protects, so an edit there would still match the seal');
|
|
188
|
+
}
|
|
189
|
+
return { ok: false, problems, hash };
|
|
190
|
+
}
|
|
191
|
+
const res = { ok: true, file: f, problems, hash };
|
|
192
|
+
if (f.schema_version > MAX_SCHEMA_VERSION)
|
|
193
|
+
res.newer_schema = f.schema_version;
|
|
194
|
+
return res;
|
|
195
|
+
}
|
|
196
|
+
function artifactsOf(xs) {
|
|
197
|
+
return (xs ?? []).map((a) => ({
|
|
198
|
+
path: a.path,
|
|
199
|
+
...(a.must_contain ? { must_contain: [...a.must_contain] } : {}),
|
|
200
|
+
...(a.should_contain ? { should_contain: [...a.should_contain] } : {}),
|
|
201
|
+
...(a.min_bytes != null ? { min_bytes: a.min_bytes } : {}),
|
|
202
|
+
...(a.fresh_days != null ? { fresh_days: a.fresh_days } : {}),
|
|
203
|
+
...(a.label ? { label: a.label } : {}),
|
|
204
|
+
}));
|
|
205
|
+
}
|
|
206
|
+
/** `verify` entries are strings in the base and objects with `{text,
|
|
207
|
+
* since_schema, anchor}` in what the method added. Both are owner prose;
|
|
208
|
+
* flatten to text so a consumer does not have to know the difference. */
|
|
209
|
+
function verifyOf(xs) {
|
|
210
|
+
const out = [];
|
|
211
|
+
for (const v of xs ?? []) {
|
|
212
|
+
if (typeof v === 'string')
|
|
213
|
+
out.push(v);
|
|
214
|
+
else if (v && typeof v === 'object' && typeof v.text === 'string') {
|
|
215
|
+
out.push(v.text);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return out;
|
|
219
|
+
}
|
|
220
|
+
function evaluatorsOf(xs) {
|
|
221
|
+
return (xs ?? []).map((e) => ({
|
|
222
|
+
name: e.name,
|
|
223
|
+
required: e.required !== false,
|
|
224
|
+
since_schema: e.since_schema ?? 1,
|
|
225
|
+
}));
|
|
226
|
+
}
|
|
227
|
+
function publishedStepToResolved(s) {
|
|
228
|
+
return {
|
|
229
|
+
num: s.num,
|
|
230
|
+
id: s.id,
|
|
231
|
+
title: s.title,
|
|
232
|
+
...(s.tramo ? { tramo: s.tramo } : {}),
|
|
233
|
+
...(s.summary ? { summary: s.summary } : {}),
|
|
234
|
+
...(s.form ? { form: s.form } : {}),
|
|
235
|
+
...(s.mandatory ? { mandatory: s.mandatory } : {}),
|
|
236
|
+
prompt_key: s.prompt_key,
|
|
237
|
+
inputs: artifactsOf(s.inputs),
|
|
238
|
+
artifacts: artifactsOf(s.artifacts),
|
|
239
|
+
verify: verifyOf(s.verify),
|
|
240
|
+
evaluators: evaluatorsOf(s.evaluators),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
/** The published definition, in the resolved shape. */
|
|
244
|
+
export function publishedToResolved(file, source, hash) {
|
|
245
|
+
const notices = [];
|
|
246
|
+
if (file.schema_version > MAX_SCHEMA_VERSION) {
|
|
247
|
+
notices.push('the definition declares schema ' + file.schema_version + ' and this Forge reads up to '
|
|
248
|
+
+ MAX_SCHEMA_VERSION + ': what schema ' + file.schema_version + ' adds is not read here. '
|
|
249
|
+
+ 'Nothing is approximated -- an evaluator this installation does not implement is reported as declared, not measured.');
|
|
250
|
+
}
|
|
251
|
+
const declared = (file.evaluators ?? []).map((e) => e.name).filter((n) => typeof n === 'string').sort();
|
|
252
|
+
return {
|
|
253
|
+
source,
|
|
254
|
+
version: file.catalog_version,
|
|
255
|
+
hash,
|
|
256
|
+
schema_version: file.schema_version,
|
|
257
|
+
steps: file.steps.map(publishedStepToResolved),
|
|
258
|
+
stations: (file.stations ?? []).map(publishedStepToResolved),
|
|
259
|
+
declared_evaluators: declared,
|
|
260
|
+
notices,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
/** Forge's own compiled catalogue, in the same resolved shape.
|
|
264
|
+
*
|
|
265
|
+
* This is the BASE: what Forge enforced before the method took ownership,
|
|
266
|
+
* and what a project freezes when it is started by a Forge that has not
|
|
267
|
+
* pulled a published definition. Every step declares exactly one evaluator
|
|
268
|
+
* and every one of them is required -- the compiled catalogue has no notion
|
|
269
|
+
* of an optional evaluator, which is correct, because it also has no
|
|
270
|
+
* evaluator it cannot measure. */
|
|
271
|
+
export function compiledGatesResolved() {
|
|
272
|
+
const one = (name) => [{ name, required: true, since_schema: 1 }];
|
|
273
|
+
return {
|
|
274
|
+
source: 'compiled',
|
|
275
|
+
version: FORGE_COMPILED_GATES_VERSION,
|
|
276
|
+
hash: compiledGatesHash(),
|
|
277
|
+
schema_version: 1,
|
|
278
|
+
steps: URAZUKE_STEPS.map((s) => ({
|
|
279
|
+
num: s.num, id: s.id, title: s.title, tramo: s.tramo, summary: s.summary,
|
|
280
|
+
form: s.form, mandatory: s.mandatory, prompt_key: s.prompt_key,
|
|
281
|
+
inputs: s.inputs.map((a) => ({ ...a })),
|
|
282
|
+
artifacts: s.artifacts.map((a) => ({ ...a })),
|
|
283
|
+
verify: [...s.verify],
|
|
284
|
+
evaluators: one(s.evaluator),
|
|
285
|
+
})),
|
|
286
|
+
stations: URAZUKE_LEGACY_STATIONS.map((s) => ({
|
|
287
|
+
num: s.num, id: s.id, title: s.title, summary: s.summary, prompt_key: s.prompt_key,
|
|
288
|
+
inputs: [],
|
|
289
|
+
artifacts: s.artifacts.map((a) => ({ ...a })),
|
|
290
|
+
verify: [...s.verify],
|
|
291
|
+
evaluators: one(s.evaluator),
|
|
292
|
+
})),
|
|
293
|
+
declared_evaluators: [...new Set([
|
|
294
|
+
...URAZUKE_STEPS.map((s) => s.evaluator),
|
|
295
|
+
...URAZUKE_LEGACY_STATIONS.map((s) => s.evaluator),
|
|
296
|
+
])].sort(),
|
|
297
|
+
notices: [],
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
export async function readFrozenGates(projectRoot) {
|
|
301
|
+
let text;
|
|
302
|
+
try {
|
|
303
|
+
text = await fs.readFile(frozenGatesPath(projectRoot), 'utf8');
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
return { absent: true };
|
|
307
|
+
}
|
|
308
|
+
let raw;
|
|
309
|
+
try {
|
|
310
|
+
raw = JSON.parse(text);
|
|
311
|
+
}
|
|
312
|
+
catch (e) {
|
|
313
|
+
return { absent: false, refused: ['the frozen copy is not valid JSON: ' + e.message] };
|
|
314
|
+
}
|
|
315
|
+
const v = validatePublishedGates(raw);
|
|
316
|
+
if (!v.ok || !v.file || !v.hash)
|
|
317
|
+
return { absent: false, refused: v.problems };
|
|
318
|
+
return { absent: false, gates: publishedToResolved(v.file, 'project', v.hash.recomputed) };
|
|
319
|
+
}
|
|
320
|
+
/** Write the frozen copy. Atomic (tmp + rename) for the same reason the
|
|
321
|
+
* prompt overrides are: a half-written definition read by the next turn is
|
|
322
|
+
* a project with no rule at all. */
|
|
323
|
+
export async function writeFrozenGates(projectRoot, file) {
|
|
324
|
+
const f = frozenGatesPath(projectRoot);
|
|
325
|
+
await fs.mkdir(path.dirname(f), { recursive: true });
|
|
326
|
+
const tmp = f + '.tmp';
|
|
327
|
+
await fs.writeFile(tmp, JSON.stringify(file, null, 2) + '\n', 'utf8');
|
|
328
|
+
await fs.rename(tmp, f);
|
|
329
|
+
}
|
|
330
|
+
/** Serialise the compiled catalogue INTO the published shape, sealed with a
|
|
331
|
+
* hash computed the published way, so a frozen copy of the base is read
|
|
332
|
+
* back by exactly the same path as a frozen copy of the portal's.
|
|
333
|
+
*
|
|
334
|
+
* This is what "freeze every existing project with what it reads today"
|
|
335
|
+
* writes: the base, unchanged, with nothing added. The first day the
|
|
336
|
+
* difference is exactly zero and it can be demonstrated -- that is the
|
|
337
|
+
* criterion the whole proposal rests on. */
|
|
338
|
+
export function compiledAsPublishedFile() {
|
|
339
|
+
const r = compiledGatesResolved();
|
|
340
|
+
const body = {
|
|
341
|
+
schema_version: 1,
|
|
342
|
+
catalog_version: FORGE_COMPILED_GATES_VERSION,
|
|
343
|
+
evaluators: r.declared_evaluators.map((name) => ({ name, since_schema: 1 })),
|
|
344
|
+
steps: r.steps.map((s) => ({
|
|
345
|
+
num: s.num, id: s.id, tramo: s.tramo, title: s.title, summary: s.summary,
|
|
346
|
+
form: s.form, mandatory: s.mandatory, prompt_key: s.prompt_key,
|
|
347
|
+
inputs: s.inputs, artifacts: s.artifacts, verify: s.verify,
|
|
348
|
+
evaluators: s.evaluators.map((e) => ({ name: e.name, since_schema: e.since_schema, required: e.required })),
|
|
349
|
+
})),
|
|
350
|
+
stations: r.stations.map((s) => ({
|
|
351
|
+
num: s.num, id: s.id, title: s.title, summary: s.summary, prompt_key: s.prompt_key,
|
|
352
|
+
artifacts: s.artifacts, verify: s.verify,
|
|
353
|
+
evaluators: s.evaluators.map((e) => ({ name: e.name, since_schema: e.since_schema, required: e.required })),
|
|
354
|
+
})),
|
|
355
|
+
};
|
|
356
|
+
const covers = ['schema_version', 'catalog_version', 'evaluators', 'steps', 'stations'];
|
|
357
|
+
const excludes = ['content_hash', 'what', 'frozen_at', 'frozen_from'];
|
|
358
|
+
const payload = {};
|
|
359
|
+
for (const k of covers)
|
|
360
|
+
payload[k] = body[k];
|
|
361
|
+
const file = {
|
|
362
|
+
what: 'The gate definition frozen for this project (PND-165). Forge reads THIS'
|
|
363
|
+
+ ' before its own compiled catalogue and before the portal. It is meant to be'
|
|
364
|
+
+ ' committed: a copy that does not travel with the repository thaws on the next'
|
|
365
|
+
+ ' clone. Replaced only by the explicit double lock, never on its own.',
|
|
366
|
+
...body,
|
|
367
|
+
content_hash: {
|
|
368
|
+
algo: 'sha256',
|
|
369
|
+
encoding: 'hex_lower',
|
|
370
|
+
canonical_form: 'json_sorted_keys_codepoint_no_whitespace_utf8',
|
|
371
|
+
covers,
|
|
372
|
+
excludes,
|
|
373
|
+
value: sha256Hex(canonicalJson(payload)),
|
|
374
|
+
note: 'Recomputed on every read, never trusted as read: that is what makes a'
|
|
375
|
+
+ ' hand-edited or merge-truncated copy detectable without the network.',
|
|
376
|
+
},
|
|
377
|
+
};
|
|
378
|
+
return file;
|
|
379
|
+
}
|
|
380
|
+
/* ---------------- resolution ---------------- */
|
|
381
|
+
/** WHICH gate definition is in force for this project.
|
|
382
|
+
*
|
|
383
|
+
* Precedence: the project's frozen copy, then the compiled catalogue. The
|
|
384
|
+
* portal is deliberately absent -- see rule 1 at the top of this file. A
|
|
385
|
+
* refused frozen copy does NOT promote the portal and does not fail the
|
|
386
|
+
* read: it falls back to the compiled catalogue, which is the safe state
|
|
387
|
+
* (nothing can tighten without an install), and it says so. */
|
|
388
|
+
export async function resolveGates(projectRoot) {
|
|
389
|
+
if (projectRoot) {
|
|
390
|
+
const frozen = await readFrozenGates(projectRoot);
|
|
391
|
+
if (frozen.gates)
|
|
392
|
+
return frozen.gates;
|
|
393
|
+
if (frozen.refused?.length) {
|
|
394
|
+
const base = compiledGatesResolved();
|
|
395
|
+
return {
|
|
396
|
+
...base,
|
|
397
|
+
notices: [
|
|
398
|
+
'the frozen gate definition of this project was REFUSED and the compiled catalogue is'
|
|
399
|
+
+ ' judging instead: ' + frozen.refused.join('; ')
|
|
400
|
+
+ '. Until it is restored, this project is not being measured by the version it froze.',
|
|
401
|
+
...base.notices,
|
|
402
|
+
],
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return compiledGatesResolved();
|
|
407
|
+
}
|
|
408
|
+
/** Bring the published definition over the network.
|
|
409
|
+
*
|
|
410
|
+
* NOT part of `resolveGates`, on purpose and by decision: the owner asked
|
|
411
|
+
* that a project start and finish on the same version of the method, so
|
|
412
|
+
* the only legitimate caller is the explicit double lock (Pieza 4), which
|
|
413
|
+
* has to show how many green steps would turn red BEFORE it writes
|
|
414
|
+
* anything. A validated file or nothing -- an unverifiable definition is
|
|
415
|
+
* never handed back. */
|
|
416
|
+
export async function fetchPublishedGates(opts = {}) {
|
|
417
|
+
const url = opts.url ?? process.env['YF_URAZUKE_GATES_URL'] ?? DEFAULT_GATES_URL;
|
|
418
|
+
const impl = opts.fetchImpl ?? fetch;
|
|
419
|
+
const ctrl = new AbortController();
|
|
420
|
+
const timer = setTimeout(() => ctrl.abort(), opts.timeout_ms ?? 10000);
|
|
421
|
+
try {
|
|
422
|
+
const r = await impl(url, { signal: ctrl.signal });
|
|
423
|
+
if (!r.ok)
|
|
424
|
+
return { ok: false, problems: ['GET ' + url + ' answered ' + r.status] };
|
|
425
|
+
return validatePublishedGates(await r.json());
|
|
426
|
+
}
|
|
427
|
+
catch (e) {
|
|
428
|
+
return { ok: false, problems: ['GET ' + url + ' failed: ' + e.message] };
|
|
429
|
+
}
|
|
430
|
+
finally {
|
|
431
|
+
clearTimeout(timer);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
/* ---------------- what this installation can measure ---------------- */
|
|
435
|
+
/** One line for a report or the CLI. */
|
|
436
|
+
export function renderGatesSource(g) {
|
|
437
|
+
return g.version + ' [' + g.source + '] schema ' + g.schema_version
|
|
438
|
+
+ ' hash ' + g.hash.slice(0, 12)
|
|
439
|
+
+ ' -- ' + g.steps.length + ' steps, ' + g.stations.length + ' stations';
|
|
440
|
+
}
|
|
441
|
+
//# sourceMappingURL=gates_data.js.map
|