halfcycle 0.3.7
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/.claude-plugin/plugin.json +16 -0
- package/LICENSE +21 -0
- package/README.md +187 -0
- package/bin/bin.bundle.mjs +16771 -0
- package/commands/agent-teams-orchestration.md +11 -0
- package/commands/consistency-check.md +11 -0
- package/commands/context-update.md +11 -0
- package/commands/halfcycle-design.md +11 -0
- package/commands/halfcycle-setup.md +11 -0
- package/commands/stack-assembly-worker.md +11 -0
- package/commands/task-execution.md +11 -0
- package/commands/task-graph.md +11 -0
- package/commands/write-spec.md +11 -0
- package/dist/account-credential.d.ts +94 -0
- package/dist/account-credential.d.ts.map +1 -0
- package/dist/bin.d.ts +34 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +3156 -0
- package/dist/bin.js.map +6 -0
- package/dist/build-record/assemble.d.ts +37 -0
- package/dist/build-record/assemble.d.ts.map +1 -0
- package/dist/build-record/index.d.ts +39 -0
- package/dist/build-record/index.d.ts.map +1 -0
- package/dist/build-record/sources.d.ts +80 -0
- package/dist/build-record/sources.d.ts.map +1 -0
- package/dist/build-record/template.d.ts +18 -0
- package/dist/build-record/template.d.ts.map +1 -0
- package/dist/build-record/types.d.ts +163 -0
- package/dist/build-record/types.d.ts.map +1 -0
- package/dist/build-record/write.d.ts +26 -0
- package/dist/build-record/write.d.ts.map +1 -0
- package/dist/cli-contract.d.ts +120 -0
- package/dist/cli-contract.d.ts.map +1 -0
- package/dist/close-phase.d.ts +114 -0
- package/dist/close-phase.d.ts.map +1 -0
- package/dist/create-engagement.d.ts +248 -0
- package/dist/create-engagement.d.ts.map +1 -0
- package/dist/device-signin.d.ts +255 -0
- package/dist/device-signin.d.ts.map +1 -0
- package/dist/engagement-credential.d.ts +267 -0
- package/dist/engagement-credential.d.ts.map +1 -0
- package/dist/identity.d.ts +76 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2083 -0
- package/dist/index.js.map +6 -0
- package/dist/install.d.ts +355 -0
- package/dist/install.d.ts.map +1 -0
- package/dist/loopback-signin.d.ts +173 -0
- package/dist/loopback-signin.d.ts.map +1 -0
- package/dist/mcp-endpoint.d.ts +35 -0
- package/dist/mcp-endpoint.d.ts.map +1 -0
- package/dist/merge-settings.d.ts +57 -0
- package/dist/merge-settings.d.ts.map +1 -0
- package/dist/mint-board-code.d.ts +41 -0
- package/dist/mint-board-code.d.ts.map +1 -0
- package/dist/open-phase.d.ts +107 -0
- package/dist/open-phase.d.ts.map +1 -0
- package/dist/probe-mcp.d.ts +47 -0
- package/dist/probe-mcp.d.ts.map +1 -0
- package/dist/resolve-credential.d.ts +126 -0
- package/dist/resolve-credential.d.ts.map +1 -0
- package/dist/scan.d.ts +131 -0
- package/dist/scan.d.ts.map +1 -0
- package/dist/setup/discovery.d.ts +73 -0
- package/dist/setup/discovery.d.ts.map +1 -0
- package/dist/setup/engagement.d.ts +99 -0
- package/dist/setup/engagement.d.ts.map +1 -0
- package/dist/setup/index.d.ts +17 -0
- package/dist/setup/index.d.ts.map +1 -0
- package/dist/setup/manifest.d.ts +155 -0
- package/dist/setup/manifest.d.ts.map +1 -0
- package/dist/setup/rows.d.ts +57 -0
- package/dist/setup/rows.d.ts.map +1 -0
- package/package.json +55 -0
- package/scaffolding/test/fixtures/captured/.gitkeep +0 -0
- package/scaffolding/test/fixtures/captured/manifest.json +4 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ONE HOME for the composition `MCP origin → MCP endpoint URL` (T-24, #446).
|
|
3
|
+
*
|
|
4
|
+
* The control plane reports an ORIGIN (`mcpUrl` on the create-engagement 201,
|
|
5
|
+
* persisted as `HALFCYCLE_MCP_URL`); the method-delivery service serves MCP at
|
|
6
|
+
* `/mcp` on it. Two callers need the composed endpoint and they must not disagree:
|
|
7
|
+
*
|
|
8
|
+
* - `install.ts` — writes it into `.mcp.json` as a LITERAL url (T-24).
|
|
9
|
+
* - `probe-mcp.ts` — calls it at install time to say whether that origin answers.
|
|
10
|
+
*
|
|
11
|
+
* Until T-24 the registration carried `${HALFCYCLE_MCP_URL}/mcp` — a template the
|
|
12
|
+
* client expanded — so `probe-mcp.ts` held the only executable copy of this rule
|
|
13
|
+
* and `install.ts` held a textual one. Now that the installer writes the composed
|
|
14
|
+
* address itself, both copies would be real, and a probe that appended `/mcp`
|
|
15
|
+
* while the registration appended something else would report an origin reachable
|
|
16
|
+
* and still install a registration that reaches nothing. That is the two-homes
|
|
17
|
+
* failure `create-engagement.ts` already names in prose ("the one place that
|
|
18
|
+
* composes the MCP URL"); this module is that one place.
|
|
19
|
+
*
|
|
20
|
+
* INV-001: no imports at all — not even Node builtins.
|
|
21
|
+
*/
|
|
22
|
+
/** The path the method-delivery service serves MCP on. Stated once. */
|
|
23
|
+
export declare const MCP_ENDPOINT_PATH = "/mcp";
|
|
24
|
+
/**
|
|
25
|
+
* Compose the MCP endpoint URL for an origin.
|
|
26
|
+
*
|
|
27
|
+
* Trailing slashes on the origin are stripped, so an origin reported as
|
|
28
|
+
* `https://host/` cannot compose `https://host//mcp`. `createEngagement` already
|
|
29
|
+
* normalises what it returns, but `install()` is also called programmatically, so
|
|
30
|
+
* the normalisation lives with the composition rather than only at the boundary.
|
|
31
|
+
*
|
|
32
|
+
* @param origin The MCP origin (no path), e.g. `https://halfcycle-delivery.fly.dev`.
|
|
33
|
+
*/
|
|
34
|
+
export declare function mcpEndpointUrl(origin: string): string;
|
|
35
|
+
//# sourceMappingURL=mcp-endpoint.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-endpoint.d.ts","sourceRoot":"","sources":["../src/mcp-endpoint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,uEAAuE;AACvE,eAAO,MAAM,iBAAiB,SAAS,CAAC;AAExC;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAErD"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings merge — the one function whose bug destroys client work.
|
|
3
|
+
*
|
|
4
|
+
* `.claude/settings.json` is MERGED, never replaced (operator decision (aq)).
|
|
5
|
+
* Every key the developer has is kept at its existing value, `$schema` excepted.
|
|
6
|
+
* Halfcycle's three hook entries are appended beside the developer's — possible
|
|
7
|
+
* without conflict because Claude Code runs every hook registered for an event.
|
|
8
|
+
*
|
|
9
|
+
* The merge is UPSERT-BY-IDENTITY, not append: a Halfcycle-authored entry already
|
|
10
|
+
* present is replaced in place, so a second install does not fire the guard twice
|
|
11
|
+
* per edit. Identity is decided by the hook command string, which is the stable,
|
|
12
|
+
* generated marker of a Halfcycle-authored entry (each of our three commands is a
|
|
13
|
+
* distinct `bash ./.claude/hooks/...` invocation).
|
|
14
|
+
*
|
|
15
|
+
* See docs/features/client-install-surface.md, criterion 4.
|
|
16
|
+
* INV-001: this file imports only Node builtins (none needed).
|
|
17
|
+
*/
|
|
18
|
+
/** A single hook command as Claude Code's settings schema records it. */
|
|
19
|
+
export interface HookCommand {
|
|
20
|
+
type?: string;
|
|
21
|
+
command: string;
|
|
22
|
+
timeout?: number;
|
|
23
|
+
}
|
|
24
|
+
/** A matcher-scoped group of hook commands for one event. */
|
|
25
|
+
export interface HookEntry {
|
|
26
|
+
matcher?: string;
|
|
27
|
+
hooks: HookCommand[];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The permission rules Claude Code evaluates, deny → ask → allow, first match
|
|
31
|
+
* wins. Only `deny` is ever generated by Halfcycle (F-23 decision (b)); the other
|
|
32
|
+
* two appear here because a developer may own them and they must survive.
|
|
33
|
+
*/
|
|
34
|
+
export interface ClaudePermissions {
|
|
35
|
+
deny?: string[];
|
|
36
|
+
allow?: string[];
|
|
37
|
+
ask?: string[];
|
|
38
|
+
[key: string]: unknown;
|
|
39
|
+
}
|
|
40
|
+
/** The subset of `.claude/settings.json` this merge reasons about. */
|
|
41
|
+
export interface ClaudeSettings {
|
|
42
|
+
$schema?: string;
|
|
43
|
+
hooks?: Record<string, HookEntry[]>;
|
|
44
|
+
permissions?: ClaudePermissions;
|
|
45
|
+
[key: string]: unknown;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Merge Halfcycle's generated hook entries into an existing settings object.
|
|
49
|
+
*
|
|
50
|
+
* @param existing the developer's parsed `.claude/settings.json` (may be `{}`).
|
|
51
|
+
* @param generated Halfcycle's fresh settings object (from `generateSettingsJson`).
|
|
52
|
+
* @returns a new settings object: every developer key preserved, `$schema` taken
|
|
53
|
+
* from `generated`, and for each event Halfcycle's entries upserted
|
|
54
|
+
* beside the developer's — exactly one Halfcycle entry per event.
|
|
55
|
+
*/
|
|
56
|
+
export declare function mergeSettings(existing: ClaudeSettings, generated: ClaudeSettings): ClaudeSettings;
|
|
57
|
+
//# sourceMappingURL=merge-settings.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"merge-settings.d.ts","sourceRoot":"","sources":["../src/merge-settings.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,yEAAyE;AACzE,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,6DAA6D;AAC7D,MAAM,WAAW,SAAS;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,WAAW,EAAE,CAAC;CACtB;AAED;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC;IACf,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,sEAAsE;AACtE,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC;IACpC,WAAW,CAAC,EAAE,iBAAiB,CAAC;IAChC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAYD;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,cAAc,EAAE,SAAS,EAAE,cAAc,GAAG,cAAc,CAwCjG"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mint the board's first-visit code — the installer's second HTTP call (T-19,
|
|
3
|
+
* #363; `client-mission-control` criterion 4, step 2).
|
|
4
|
+
*
|
|
5
|
+
* `POST {baseUrl}/board/enter-codes` with the engagement's own credential as a
|
|
6
|
+
* bearer and an empty body: the engagement is resolved from the credential and
|
|
7
|
+
* never read from the body. The control plane answers
|
|
8
|
+
* `{ enterCode, engagementId, boardUrl, expiresAt }`. The installer prints the
|
|
9
|
+
* address and the code; the developer pastes the code once, in a browser.
|
|
10
|
+
*
|
|
11
|
+
* The installer is the ONE minting site this phase builds. A standing re-mint
|
|
12
|
+
* command belongs to the installer spec's second-run work; until it exists, an
|
|
13
|
+
* expired or spent code is recovered by re-running the installer — which the
|
|
14
|
+
* printed lines say rather than leave to be discovered.
|
|
15
|
+
*
|
|
16
|
+
* INV-001: this file imports only the global `fetch` (Node 20). It never imports
|
|
17
|
+
* the control service — the mint is an external HTTP contract, exactly as
|
|
18
|
+
* `create-engagement.ts` treats `POST /engagements`.
|
|
19
|
+
*/
|
|
20
|
+
/** What `POST /board/enter-codes` returns on 201. */
|
|
21
|
+
export interface MintedBoardCode {
|
|
22
|
+
/** The single-use plaintext, printed once and never stored by the installer. */
|
|
23
|
+
enterCode: string;
|
|
24
|
+
engagementId: string;
|
|
25
|
+
/** The first-visit address — a plain URL carrying no credential. */
|
|
26
|
+
boardUrl: string;
|
|
27
|
+
/** UTC ISO-8601 absolute expiry. */
|
|
28
|
+
expiresAt: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Mint a first-visit board code for the engagement the credential resolves to.
|
|
32
|
+
*
|
|
33
|
+
* @param baseUrl The platform origin (`HALFCYCLE_SERVICE_URL`).
|
|
34
|
+
* @param sessionToken The engagement's own credential, as a bearer.
|
|
35
|
+
* @throws on transport failure, a non-2xx status, or a body missing
|
|
36
|
+
* any of the four fields. The caller decides what a failure
|
|
37
|
+
* means; for the installer it means printing a recoverable
|
|
38
|
+
* note, never failing an install that already succeeded.
|
|
39
|
+
*/
|
|
40
|
+
export declare function mintBoardEnterCode(baseUrl: string, sessionToken: string): Promise<MintedBoardCode>;
|
|
41
|
+
//# sourceMappingURL=mint-board-code.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mint-board-code.d.ts","sourceRoot":"","sources":["../src/mint-board-code.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,qDAAqD;AACrD,MAAM,WAAW,eAAe;IAC9B,gFAAgF;IAChF,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,oEAAoE;IACpE,QAAQ,EAAE,MAAM,CAAC;IACjB,oCAAoC;IACpC,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;GASG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,eAAe,CAAC,CAwC1B"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Open-phase wiring — the CLI's call to `POST {baseUrl}/engagements/:id/phase`
|
|
3
|
+
* (FX-11, operator decision D-2).
|
|
4
|
+
*
|
|
5
|
+
* `openPhase` has had a real entry gate since FW-7 — it refuses an unmet entry
|
|
6
|
+
* condition, demands an explicit override, and records who crossed what — and no
|
|
7
|
+
* surface at all, so the only way anyone has ever opened a phase is a scratch script
|
|
8
|
+
* that ran inside the control plane. This module is the CLIENT half of the answer:
|
|
9
|
+
* the developer whose repo the phase belongs to can open it from their own repo,
|
|
10
|
+
* through the gate, with their own credential.
|
|
11
|
+
*
|
|
12
|
+
* WIRING, NOT MACHINERY. Every rule lives on the control plane: what makes a decision
|
|
13
|
+
* substantive, what the store can evaluate for itself, what an override crosses. This
|
|
14
|
+
* module builds the request, sends it, and reports what came back — including the
|
|
15
|
+
* refusal, in the plane's own words. It re-checks nothing, because a second copy of
|
|
16
|
+
* the entry rule in a shippable package is a copy that can disagree with the one that
|
|
17
|
+
* decides.
|
|
18
|
+
*
|
|
19
|
+
* INV-001: this file imports the global `fetch` (Node 20), Node builtins, and
|
|
20
|
+
* `@halfcycle/events` — one of the exactly two workspace packages a shippable may
|
|
21
|
+
* import. `phaseEntryDecisionSchema` is the ONE home for the decision's shape
|
|
22
|
+
* (INV-003), so the CLI validates what it builds against the same schema the route
|
|
23
|
+
* parses with rather than a second hand-written copy. It never imports the control
|
|
24
|
+
* service; the route is an external HTTP contract, exactly as `createEngagement` and
|
|
25
|
+
* `mintBoardEnterCode` treat theirs.
|
|
26
|
+
*/
|
|
27
|
+
import { type PhaseEntryDecision } from '@halfcycle/events';
|
|
28
|
+
/** What `POST /engagements/:id/phase` returns on 200 (the fields the CLI reports). */
|
|
29
|
+
export interface OpenedPhase {
|
|
30
|
+
/** What is open now. `null` is project scope. */
|
|
31
|
+
phase: string | null;
|
|
32
|
+
/** What was open before. `null` is project scope. */
|
|
33
|
+
previousPhase: string | null;
|
|
34
|
+
decision: {
|
|
35
|
+
decisionId: string;
|
|
36
|
+
decision: string;
|
|
37
|
+
actor: string;
|
|
38
|
+
justification: string;
|
|
39
|
+
/** The finding this decision crossed, or `null` when it crossed none. */
|
|
40
|
+
blockingFinding: string | null;
|
|
41
|
+
decidedAt: string;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* A refusal from the control plane, carried as an error the CLI can print whole.
|
|
46
|
+
*
|
|
47
|
+
* The `finding` is the point. A `409` here means the phase the engagement is in was
|
|
48
|
+
* never accepted, and the plane says WHICH phase and WHY — the developer needs that
|
|
49
|
+
* sentence, not a status code. It rides its own field so the CLI never parses prose.
|
|
50
|
+
*/
|
|
51
|
+
export declare class OpenPhaseRefused extends Error {
|
|
52
|
+
readonly status: number;
|
|
53
|
+
readonly finding: string | null;
|
|
54
|
+
constructor(status: number, message: string, finding: string | null);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The three credential keys this call needs. They are the installer's own keys —
|
|
58
|
+
* `install.ts` writes them and `.mcp.json`/the guard hook read them — and this list
|
|
59
|
+
* is deliberately a SUBSET of that write set, asserted as one in the tests rather
|
|
60
|
+
* than left to two lists agreeing by eye.
|
|
61
|
+
*/
|
|
62
|
+
export declare const OPEN_PHASE_ENV_KEYS: readonly ["HALFCYCLE_SERVICE_URL", "HALFCYCLE_TOKEN", "HALFCYCLE_ENGAGEMENT_ID"];
|
|
63
|
+
export interface RepoCredential {
|
|
64
|
+
serviceUrl: string;
|
|
65
|
+
token: string;
|
|
66
|
+
engagementId: string;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Read the credential for the engagement this repository is installed against.
|
|
70
|
+
*
|
|
71
|
+
* **WHERE IT LOOKS CHANGED AT T-09, AND THE REPO ROOT IS STILL THE ARGUMENT.** The
|
|
72
|
+
* credential used to sit in the tree at `.env.local`. It now lives at machine level
|
|
73
|
+
* (`engagement-credential.ts`), keyed on the engagement — so the repo root is what
|
|
74
|
+
* names the engagement (via the committed `.halfcycle/bundle.json` pin) rather than
|
|
75
|
+
* what holds the secret. The parameter kept its meaning: *the repository whose
|
|
76
|
+
* engagement this is*.
|
|
77
|
+
*
|
|
78
|
+
* READ, NEVER SOURCED — a credential store is not code, and this parses `KEY=value`
|
|
79
|
+
* lines exactly as the generated `mcp-headers.sh` does. The process environment WINS
|
|
80
|
+
* when it carries a key, so an operator can point one command at a different plane
|
|
81
|
+
* without editing a file, and a CI job with no per-user store works by exporting
|
|
82
|
+
* them.
|
|
83
|
+
*
|
|
84
|
+
* Why any file is read at all: nothing loads a credential into a shell. A verb that
|
|
85
|
+
* only read `process.env` would work for whoever had already exported three
|
|
86
|
+
* variables and fail, confusingly, for the developer whose install wrote them thirty
|
|
87
|
+
* seconds earlier.
|
|
88
|
+
*
|
|
89
|
+
* @throws with the missing key NAMED, and the file it was looked for in. "Not
|
|
90
|
+
* configured" is the one message a developer cannot act on.
|
|
91
|
+
*/
|
|
92
|
+
export declare function readRepoCredential(repoRoot: string, env?: NodeJS.ProcessEnv, home?: string): RepoCredential;
|
|
93
|
+
/**
|
|
94
|
+
* Open a phase on the engagement (or, with `phase: null`, return it to project
|
|
95
|
+
* scope).
|
|
96
|
+
*
|
|
97
|
+
* The decision is a REQUIRED parameter, mirroring `openPhase`'s own signature and the
|
|
98
|
+
* wire schema's required `entry`: there is no argument-less form here either, at any
|
|
99
|
+
* layer, by construction rather than by review.
|
|
100
|
+
*
|
|
101
|
+
* @throws OpenPhaseRefused on a 4xx the plane explained (an unmet entry condition, a
|
|
102
|
+
* decision that says nothing, a bad credential), so the CLI prints the
|
|
103
|
+
* plane's own sentence and exits non-zero.
|
|
104
|
+
* @throws Error on transport failure or a body that is not the expected shape.
|
|
105
|
+
*/
|
|
106
|
+
export declare function openPhase(credential: RepoCredential, phase: string | null, entry: PhaseEntryDecision): Promise<OpenedPhase>;
|
|
107
|
+
//# sourceMappingURL=open-phase.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"open-phase.d.ts","sourceRoot":"","sources":["../src/open-phase.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAIH,OAAO,EAA4B,KAAK,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAOtF,sFAAsF;AACtF,MAAM,WAAW,WAAW;IAC1B,iDAAiD;IACjD,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,qDAAqD;IACrD,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,QAAQ,EAAE;QACR,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,MAAM,CAAC;QACjB,KAAK,EAAE,MAAM,CAAC;QACd,aAAa,EAAE,MAAM,CAAC;QACtB,yEAAyE;QACzE,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;QAC/B,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;CACH;AAED;;;;;;GAMG;AACH,qBAAa,gBAAiB,SAAQ,KAAK;aAEvB,MAAM,EAAE,MAAM;aAEd,OAAO,EAAE,MAAM,GAAG,IAAI;gBAFtB,MAAM,EAAE,MAAM,EAC9B,OAAO,EAAE,MAAM,EACC,OAAO,EAAE,MAAM,GAAG,IAAI;CAKzC;AAED;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,kFAItB,CAAC;AAEX,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,IAAI,CAAC,EAAE,MAAM,GACZ,cAAc,CA0BhB;AAuBD;;;;;;;;;;;;GAYG;AACH,wBAAsB,SAAS,CAC7B,UAAU,EAAE,cAAc,EAC1B,KAAK,EAAE,MAAM,GAAG,IAAI,EACpB,KAAK,EAAE,kBAAkB,GACxB,OAAO,CAAC,WAAW,CAAC,CA0DtB"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reach the MCP origin the platform reported, so the installer can say WHICH of
|
|
3
|
+
* the two origins is wrong (T-22, #438 — `client-install-surface` criterion 5).
|
|
4
|
+
*
|
|
5
|
+
* **The failure this exists to make legible.** The install writes two addresses:
|
|
6
|
+
* `HALFCYCLE_SERVICE_URL` (the control plane, which the developer configures) and
|
|
7
|
+
* `HALFCYCLE_MCP_URL` (the method-delivery service, which the platform reports).
|
|
8
|
+
* A bad control origin already fails loudly, at the create call. A bad MCP origin
|
|
9
|
+
* failed SILENTLY: the install completed, the tree was correct, `.mcp.json` named
|
|
10
|
+
* an address, and the developer found out when `/halfcycle-setup` did nothing in
|
|
11
|
+
* their editor — a 404 with no clue which half of a two-service platform was
|
|
12
|
+
* wrong. This turns that into one sentence at install time.
|
|
13
|
+
*
|
|
14
|
+
* **It never fails the install.** The tree is already correct and the credential
|
|
15
|
+
* is already written; refusing an install over a service that may simply be
|
|
16
|
+
* restarting would be worse than saying so. Same degrade posture as the board
|
|
17
|
+
* mint in `bin.ts`.
|
|
18
|
+
*
|
|
19
|
+
* **`initialize` is the probe** because it is the one MCP call that needs no
|
|
20
|
+
* credential and identifies the server: a wrong origin 404s, a right one answers
|
|
21
|
+
* with its `serverInfo`. The alternative — a plain `GET /health` — would prove an
|
|
22
|
+
* origin is alive without proving it serves MCP, which is exactly the distinction
|
|
23
|
+
* that was missing.
|
|
24
|
+
*
|
|
25
|
+
* INV-001: Node builtins and the global `fetch` only. No hosted import.
|
|
26
|
+
*/
|
|
27
|
+
export type McpProbe =
|
|
28
|
+
/** `POST {origin}/mcp` answered as an MCP server. `serverName` is what it called itself. */
|
|
29
|
+
{
|
|
30
|
+
reached: true;
|
|
31
|
+
serverName: string;
|
|
32
|
+
}
|
|
33
|
+
/** It did not. `problem` is a plain-words sentence naming what happened. */
|
|
34
|
+
| {
|
|
35
|
+
reached: false;
|
|
36
|
+
problem: string;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Ask the MCP origin to identify itself.
|
|
40
|
+
*
|
|
41
|
+
* @param mcpUrl The ORIGIN (no path) the create response reported. The endpoint
|
|
42
|
+
* is composed by `mcpEndpointUrl` — the SAME function that composes
|
|
43
|
+
* the literal url written into `.mcp.json` (T-24), so this probe
|
|
44
|
+
* cannot report an address the registration does not use.
|
|
45
|
+
*/
|
|
46
|
+
export declare function probeMcpOrigin(mcpUrl: string): Promise<McpProbe>;
|
|
47
|
+
//# sourceMappingURL=probe-mcp.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"probe-mcp.d.ts","sourceRoot":"","sources":["../src/probe-mcp.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAQH,MAAM,MAAM,QAAQ;AAClB,4FAA4F;AAC1F;IAAE,OAAO,EAAE,IAAI,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE;AACvC,4EAA4E;GAC1E;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAExC;;;;;;;GAOG;AACH,wBAAsB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAuCtE"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE CREDENTIAL RESOLVER — environment variable → per-user store → browser
|
|
3
|
+
* handoff (T-08, feature `connect-signup-and-credential`).
|
|
4
|
+
*
|
|
5
|
+
* One home for the question *"what credential does this run present to control, and
|
|
6
|
+
* where did it come from?"*. `bin.ts` asks it once, before anything is written.
|
|
7
|
+
*
|
|
8
|
+
* ---------------------------------------------------------------------------
|
|
9
|
+
* THE ORDER IS AN ACCEPTANCE CRITERION, NOT A CONVENIENCE. *"CI works with no
|
|
10
|
+
* browser and no per-user store: `HALFCYCLE_TOKEN` in the environment is accepted
|
|
11
|
+
* and TAKES PRECEDENCE over the stored credential."* A workflow that exports a
|
|
12
|
+
* credential has said which identity this run is, and a stored credential left
|
|
13
|
+
* behind on a shared or long-lived runner must not silently win over it — the
|
|
14
|
+
* symptom of that inversion is a job that quietly does its work as the wrong
|
|
15
|
+
* account, which nothing about the run's output would reveal.
|
|
16
|
+
*
|
|
17
|
+
* The environment arm also DOES NOT TOUCH THE STORE AT ALL: it neither reads it nor
|
|
18
|
+
* writes it. A CI job must not leave a credential behind on a runner's home
|
|
19
|
+
* directory, and a developer who exports the variable for one command must not find
|
|
20
|
+
* their saved sign-in replaced by it.
|
|
21
|
+
*
|
|
22
|
+
* ---------------------------------------------------------------------------
|
|
23
|
+
* WHY THE SOURCE IS RETURNED AND NOT JUST THE SECRET. When control refuses a
|
|
24
|
+
* credential, the remedy depends entirely on where it came from: a stored one is
|
|
25
|
+
* replaced by signing in again (which this CLI can just do), an environment one
|
|
26
|
+
* cannot be — the variable still takes precedence, so "run it again" would loop
|
|
27
|
+
* forever presenting the same dead value. That is the *remedy naming something the
|
|
28
|
+
* reader cannot act on* defect (`docs/topology-boundary.md` rule 2), and returning
|
|
29
|
+
* the source is what lets the refusal path avoid it.
|
|
30
|
+
*
|
|
31
|
+
* ---------------------------------------------------------------------------
|
|
32
|
+
* WHAT "REFRESH" MEANS HERE, because the client half of it is smaller than it
|
|
33
|
+
* sounds and the difference matters. The 30-day window is refreshed SERVER-SIDE, on
|
|
34
|
+
* every successful use: `resolveAccountCredential` extends `expires_at` in the same
|
|
35
|
+
* statement that validates the credential, and it is the one home of that number. No
|
|
36
|
+
* response tells this client the extended value, so there is nothing to write back
|
|
37
|
+
* and nothing to re-time locally — using the credential IS the refresh. The client
|
|
38
|
+
* half is therefore exactly two things: keep presenting the stored credential, and
|
|
39
|
+
* when control finally refuses one, treat that as *sign in again* rather than as
|
|
40
|
+
* something to retry. Anything more — a locally-tracked expiry, a client-side TTL, a
|
|
41
|
+
* pre-emptive rotate call — would be a second home for a lifetime the operator set
|
|
42
|
+
* in one place, and there is no client-reachable rotate route to call anyway
|
|
43
|
+
* (`rotateToken`'s only non-test caller is an operator script).
|
|
44
|
+
*
|
|
45
|
+
* INV-001: Node builtins, `@halfcycle/events` (transitively, via `device-signin`),
|
|
46
|
+
* and bundle-internal modules.
|
|
47
|
+
*/
|
|
48
|
+
import { type SignInDeps } from './device-signin.js';
|
|
49
|
+
/** Where the credential this run presents came from. */
|
|
50
|
+
export type CredentialSource =
|
|
51
|
+
/** `HALFCYCLE_TOKEN` in the process environment — the CI path, and it wins. */
|
|
52
|
+
'environment'
|
|
53
|
+
/** `~/.halfcycle/account.json` — this machine has signed in before. */
|
|
54
|
+
| 'stored'
|
|
55
|
+
/** The browser handoff just ran, and the result has been stored. */
|
|
56
|
+
| 'browser-handoff';
|
|
57
|
+
export interface ResolvedCredential {
|
|
58
|
+
credential: string;
|
|
59
|
+
source: CredentialSource;
|
|
60
|
+
/**
|
|
61
|
+
* The account the credential speaks for, when it is known. ABSENT for the
|
|
62
|
+
* environment arm — a bare variable carries no identity and this CLI does not go
|
|
63
|
+
* and ask, because the only way to find out is to spend a call, and control will
|
|
64
|
+
* tell us by accepting or refusing it a moment later anyway.
|
|
65
|
+
*/
|
|
66
|
+
accountId?: string;
|
|
67
|
+
}
|
|
68
|
+
export interface ResolveOptions extends SignInDeps {
|
|
69
|
+
/** The process environment to read `HALFCYCLE_TOKEN` and `CI` from. */
|
|
70
|
+
env?: NodeJS.ProcessEnv;
|
|
71
|
+
/** The home directory the per-user store lives under. Defaults to `os.homedir()`. */
|
|
72
|
+
home?: string;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Resolve a credential for `serviceUrl`, signing in through the browser if this
|
|
76
|
+
* machine has none.
|
|
77
|
+
*
|
|
78
|
+
* A successful handoff is STORED before this returns, so the next run skips it —
|
|
79
|
+
* that is what makes *"an already-signed-in machine skips all of it"* true. It is
|
|
80
|
+
* stored BEFORE the credential is used for anything, deliberately: a credential
|
|
81
|
+
* obtained and then lost because the create call failed would send the developer
|
|
82
|
+
* back through the browser for a reason that had nothing to do with signing in.
|
|
83
|
+
*/
|
|
84
|
+
export declare function obtainCredential(serviceUrl: string, opts?: ResolveOptions): Promise<ResolvedCredential>;
|
|
85
|
+
/**
|
|
86
|
+
* Control refused the credential we presented. Replace it, if replacing it is a
|
|
87
|
+
* thing this CLI is allowed to do.
|
|
88
|
+
*
|
|
89
|
+
* Returns the new credential, or `null` when there is nothing to replace — in which
|
|
90
|
+
* case the caller must print the refusal and exit non-zero rather than retry.
|
|
91
|
+
*
|
|
92
|
+
* THE THREE ARMS, and why only one of them re-signs-in:
|
|
93
|
+
*
|
|
94
|
+
* `environment` → null. The variable takes precedence over anything this
|
|
95
|
+
* function could store, so signing in again would produce a
|
|
96
|
+
* credential the very next resolve would ignore. The operator
|
|
97
|
+
* who set the variable is the only one who can fix it.
|
|
98
|
+
* `stored` → forget it and sign in again. There is no grace window on
|
|
99
|
+
* this credential class and nothing rotates it, so a refusal
|
|
100
|
+
* is terminal for that value: keeping it would mean every
|
|
101
|
+
* later run presenting something that can only fail. This is
|
|
102
|
+
* the 30-day window closing on a machine that sat idle — the
|
|
103
|
+
* *"sends the developer back through the browser once"* half
|
|
104
|
+
* of operator decision 4.
|
|
105
|
+
* `browser-handoff` → null. The credential was minted seconds ago and control has
|
|
106
|
+
* already refused it; signing in again would mint another one
|
|
107
|
+
* from the same route and hand back the same refusal, one
|
|
108
|
+
* browser round-trip later. A loop that ends where it started
|
|
109
|
+
* is worse than a message, so this reports rather than retries.
|
|
110
|
+
*/
|
|
111
|
+
export declare function replaceRefusedCredential(serviceUrl: string, refused: ResolvedCredential, opts?: ResolveOptions): Promise<ResolvedCredential | null>;
|
|
112
|
+
/**
|
|
113
|
+
* The sentence for a refusal this CLI cannot recover from, chosen by where the
|
|
114
|
+
* credential came from.
|
|
115
|
+
*
|
|
116
|
+
* ONE HOME for that selection, and it exists because the alternative already shipped
|
|
117
|
+
* once in this repository: a single remedy written for all three situations, printed
|
|
118
|
+
* to whoever hit any of them. `bin.ts` already selects its guard-off remedy this way
|
|
119
|
+
* for the same reason (walk 9 `F-2` — a line that told a developer to set a variable
|
|
120
|
+
* that was already set, on the invocation that printed it).
|
|
121
|
+
*
|
|
122
|
+
* The `message` control sent is carried through in every arm, because control's
|
|
123
|
+
* refusal is the fact and this is the local advice on top of it.
|
|
124
|
+
*/
|
|
125
|
+
export declare function refusedCredentialRemedy(source: CredentialSource): string;
|
|
126
|
+
//# sourceMappingURL=resolve-credential.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve-credential.d.ts","sourceRoot":"","sources":["../src/resolve-credential.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AAOH,OAAO,EAAU,KAAK,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAE7D,wDAAwD;AACxD,MAAM,MAAM,gBAAgB;AAC1B,+EAA+E;AAC7E,aAAa;AACf,uEAAuE;GACrE,QAAQ;AACV,oEAAoE;GAClE,iBAAiB,CAAC;AAEtB,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,gBAAgB,CAAC;IACzB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,cAAe,SAAQ,UAAU;IAChD,uEAAuE;IACvE,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,qFAAqF;IACrF,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;;GASG;AACH,wBAAsB,gBAAgB,CACpC,UAAU,EAAE,MAAM,EAClB,IAAI,GAAE,cAAmB,GACxB,OAAO,CAAC,kBAAkB,CAAC,CA0B7B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAsB,wBAAwB,CAC5C,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,kBAAkB,EAC3B,IAAI,GAAE,cAAmB,GACxB,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAmBpC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,CAqBxE"}
|
package/dist/scan.d.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bootstrap scan — INV-021's one permitted repository read.
|
|
3
|
+
*
|
|
4
|
+
* Writes `.halfcycle/state.json`, a client-side, gitignored cache of DERIVED
|
|
5
|
+
* position. It is not a state record and it is not a record store: the
|
|
6
|
+
* authoritative position is computed entirely hosted-side, in-process, from
|
|
7
|
+
* the one satisfaction oracle over the state-record table. This module holds
|
|
8
|
+
* no database client, no store handle and no import path to any hosted
|
|
9
|
+
* package, so it cannot reach that oracle — see the import-boundary test,
|
|
10
|
+
* `packages/bundle/test/scan-import-boundary.test.ts`.
|
|
11
|
+
*
|
|
12
|
+
* What it may write, and the whole of what it may write (INV-021):
|
|
13
|
+
* - a **second-class** value, `'asserted'`, set per LAYER (never per step) when
|
|
14
|
+
* the repository already holds that layer's artefact(s) on disk;
|
|
15
|
+
* - never `'green'`; never anything that satisfies a `requires` edge or counts
|
|
16
|
+
* toward a gate — those are hosted-side concepts this file never touches;
|
|
17
|
+
* - carrying its own provenance (`source: 'bootstrap-scan'`, the paths found,
|
|
18
|
+
* and when the scan ran) so a reader can never mistake it for a record —
|
|
19
|
+
* PLUS a file-level `note` saying the same of the file as a whole, because
|
|
20
|
+
* an EMPTY `layers` has no entry to carry provenance, and emptiness is
|
|
21
|
+
* exactly the case that misled a cold session (W3-F-1, walk 4);
|
|
22
|
+
* - superseded by the first REAL record for that layer — this module has no
|
|
23
|
+
* opinion on that; a caller merges/overlays hosted records over this file's
|
|
24
|
+
* `asserted` rows and MUST prefer the real record. See `mergeWithRecords`.
|
|
25
|
+
*
|
|
26
|
+
* Runs ONCE, during onboarding, and never again (INV-021's reworded clause —
|
|
27
|
+
* decision (bb) names no command). `runBootstrapScan` is idempotent-refusing:
|
|
28
|
+
* it does nothing if `.halfcycle/state.json` already exists, so re-running the
|
|
29
|
+
* installer or resolving `/halfcycle-setup` a second time cannot re-scan.
|
|
30
|
+
*
|
|
31
|
+
* INV-001: this file imports only @halfcycle/events and Node builtins.
|
|
32
|
+
*/
|
|
33
|
+
import { methodLayerSchema, type MethodLayer } from '@halfcycle/events';
|
|
34
|
+
/** The state file's format tag — bumped only on a breaking shape change. */
|
|
35
|
+
export declare const HALFCYCLE_STATE_FORMAT: "halfcycle-state-file/v1";
|
|
36
|
+
/** Re-exported so a consumer can validate a layer key without a second import. */
|
|
37
|
+
export { methodLayerSchema };
|
|
38
|
+
/**
|
|
39
|
+
* The only non-absent value this module ever writes — there is no
|
|
40
|
+
* `'green'`/`'complete'` member because INV-021 forbids this file from ever
|
|
41
|
+
* claiming one. A layer with no matching artefact on disk simply has no
|
|
42
|
+
* entry (absence, not a `'not-instrumented'` literal — that rendering is the
|
|
43
|
+
* hosted board's, per `state-contract.md` Non-goals, and this file does not
|
|
44
|
+
* reproduce it).
|
|
45
|
+
*/
|
|
46
|
+
export type ScannedLayerValue = 'asserted';
|
|
47
|
+
/** One layer's scanned entry, with its own provenance. */
|
|
48
|
+
export interface ScannedLayerEntry {
|
|
49
|
+
value: ScannedLayerValue;
|
|
50
|
+
/** Always 'bootstrap-scan' — the one emitter this file ever has. */
|
|
51
|
+
source: 'bootstrap-scan';
|
|
52
|
+
/** Repo-relative paths whose presence caused this layer to be asserted. */
|
|
53
|
+
foundPaths: string[];
|
|
54
|
+
/** When the (one, onboarding-time) scan ran. */
|
|
55
|
+
scannedAt: string;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* What the file says about itself, in the file, for a reader who has only the
|
|
59
|
+
* file (W3-F-1). Every per-layer entry already carries provenance so it "can
|
|
60
|
+
* never be mistaken for a record" — but an EMPTY `layers` has no entry to carry
|
|
61
|
+
* it, and emptiness is the state a greenfield install writes and then keeps for
|
|
62
|
+
* the life of the engagement. Walk 4's cold session read `{"layers":{}}`,
|
|
63
|
+
* concluded "literally nothing recorded", and told the developer to redo an
|
|
64
|
+
* approval gate they had already passed. The provenance rule was sound; it just
|
|
65
|
+
* had nowhere to live in the one case that misleads.
|
|
66
|
+
*
|
|
67
|
+
* This is deliberately prose rather than a flag: the reader that gets this
|
|
68
|
+
* wrong is a language model reading JSON it did not expect, and a `false` on a
|
|
69
|
+
* key it has never seen carries no instruction. Kept to one sentence per claim.
|
|
70
|
+
*/
|
|
71
|
+
export declare const HALFCYCLE_STATE_NOTE: string;
|
|
72
|
+
/** The whole of `.halfcycle/state.json`. */
|
|
73
|
+
export interface HalfcycleStateFile {
|
|
74
|
+
format: typeof HALFCYCLE_STATE_FORMAT;
|
|
75
|
+
/**
|
|
76
|
+
* Self-description, always written, always the same string. Present so that
|
|
77
|
+
* an empty `layers` still tells a reader what it is and where to ask instead.
|
|
78
|
+
*/
|
|
79
|
+
note: typeof HALFCYCLE_STATE_NOTE;
|
|
80
|
+
/** Per-layer scan results. Only layers with a hit are present. */
|
|
81
|
+
layers: Partial<Record<MethodLayer, ScannedLayerEntry>>;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The repo-relative artefact paths that, if present, assert a layer. Kept
|
|
85
|
+
* narrow and file-existence-only — this is a presence check, not a content
|
|
86
|
+
* judgement, and it never reads a record store (there is none to read from a
|
|
87
|
+
* client machine; see the module docblock).
|
|
88
|
+
*
|
|
89
|
+
* Layers with no local artefact convention (`l3-l4`, `xl`) are deliberately
|
|
90
|
+
* absent — the scan has nothing on disk to look for and asserts nothing.
|
|
91
|
+
*
|
|
92
|
+
* **Exported so there is ONE home for it.** The discover phase
|
|
93
|
+
* (`src/setup/discovery.ts`) needs the same "which document belongs to which
|
|
94
|
+
* layer" table to report which layer documents already exist, and a second copy
|
|
95
|
+
* there would drift the moment either moved. It consumes the CONSTANT and never
|
|
96
|
+
* `scanLayers`, so no second position value is constructed: discovery reports a
|
|
97
|
+
* file's existence with its path as the anchor, and derives no `asserted`
|
|
98
|
+
* (INV-021 — position is computed from records, and the one permitted scan is
|
|
99
|
+
* this module's).
|
|
100
|
+
*/
|
|
101
|
+
export declare const LAYER_ARTEFACT_PATHS: Partial<Record<MethodLayer, string[]>>;
|
|
102
|
+
/** Scan `targetRepo` for each layer's artefact convention. Pure filesystem read. */
|
|
103
|
+
export declare function scanLayers(targetRepo: string, scannedAt?: string): HalfcycleStateFile;
|
|
104
|
+
/**
|
|
105
|
+
* Run the bootstrap scan ONCE. If `.halfcycle/state.json` already exists this
|
|
106
|
+
* is a no-op (returns the existing file unchanged) — the "never again" half of
|
|
107
|
+
* INV-021's onboarding clause, enforced here rather than left to caller
|
|
108
|
+
* discipline. Returns the file that is now on disk either way.
|
|
109
|
+
*/
|
|
110
|
+
export declare function runBootstrapScan(targetRepo: string): {
|
|
111
|
+
state: HalfcycleStateFile;
|
|
112
|
+
ran: boolean;
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* What a layer reads as, once real records are known. This is the reader's
|
|
116
|
+
* side of the "superseded by the first real record" rule — it takes the local
|
|
117
|
+
* scan file and a set of layers the HOSTED oracle already holds a satisfying
|
|
118
|
+
* record for (computed elsewhere, over HTTP, never read from disk here), and
|
|
119
|
+
* returns a merged view where any layer with a real record reads `'complete'`
|
|
120
|
+
* regardless of what the scan asserted, and every other layer reads the scan's
|
|
121
|
+
* `'asserted'` or is absent.
|
|
122
|
+
*
|
|
123
|
+
* This function takes the real-record set as a plain `Set<MethodLayer>`
|
|
124
|
+
* parameter — never a store handle, a pool, a URL, or anything else that could
|
|
125
|
+
* let a caller wire this file to a live query. That is the structural half of
|
|
126
|
+
* criterion 3: nothing in `packages/bundle` can reach the hosted oracle, so the
|
|
127
|
+
* only way "real records" enter this function is a value the CALLER already
|
|
128
|
+
* computed and handed in.
|
|
129
|
+
*/
|
|
130
|
+
export declare function mergeWithRecords(state: HalfcycleStateFile, layersWithRealRecords: ReadonlySet<MethodLayer>): Partial<Record<MethodLayer, 'asserted' | 'complete'>>;
|
|
131
|
+
//# sourceMappingURL=scan.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scan.d.ts","sourceRoot":"","sources":["../src/scan.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAIH,OAAO,EAAE,iBAAiB,EAAE,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAMxE,4EAA4E;AAC5E,eAAO,MAAM,sBAAsB,EAAG,yBAAkC,CAAC;AAEzE,kFAAkF;AAClF,OAAO,EAAE,iBAAiB,EAAE,CAAC;AAE7B;;;;;;;GAOG;AACH,MAAM,MAAM,iBAAiB,GAAG,UAAU,CAAC;AAE3C,0DAA0D;AAC1D,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,iBAAiB,CAAC;IACzB,oEAAoE;IACpE,MAAM,EAAE,gBAAgB,CAAC;IACzB,2EAA2E;IAC3E,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,gDAAgD;IAChD,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,oBAAoB,QAStB,CAAC;AAEZ,4CAA4C;AAC5C,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,OAAO,sBAAsB,CAAC;IACtC;;;OAGG;IACH,IAAI,EAAE,OAAO,oBAAoB,CAAC;IAClC,kEAAkE;IAClE,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,iBAAiB,CAAC,CAAC,CAAC;CACzD;AAMD;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,oBAAoB,EAAE,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,CAAC,CAOvE,CAAC;AAMF,oFAAoF;AACpF,wBAAgB,UAAU,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,GAAE,MAAiC,GAAG,kBAAkB,CAc/G;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,kBAAkB,CAAC;IAAC,GAAG,EAAE,OAAO,CAAA;CAAE,CAUhG;AAMD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,kBAAkB,EACzB,qBAAqB,EAAE,WAAW,CAAC,WAAW,CAAC,GAC9C,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,UAAU,GAAG,UAAU,CAAC,CAAC,CAcvD"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The discover phase — everything discoverable is discovered before anything is
|
|
3
|
+
* asked, and every discovered value carries an anchor (feature `halfcycle-setup`,
|
|
4
|
+
* criterion 1; task T-17).
|
|
5
|
+
*
|
|
6
|
+
* **The rule this module exists to make unbreakable.** A discovered value with no
|
|
7
|
+
* `file:line` or command output is NOT discovered — it is an assumption, and it
|
|
8
|
+
* moves into the confirm phase as a decision rather than being presented as a
|
|
9
|
+
* finding. That is what stops the discover phase becoming a plausible guess the
|
|
10
|
+
* developer is invited to rubber-stamp. It is enforced by construction here: a
|
|
11
|
+
* finding cannot be built without an anchor, and the one function that takes a
|
|
12
|
+
* possibly-anchorless value returns a QUESTION when the anchor is missing.
|
|
13
|
+
*
|
|
14
|
+
* **What it reads and what it does not.** Plain filesystem reads of the project's
|
|
15
|
+
* own files. It computes no position, writes nothing, and constructs no `asserted`
|
|
16
|
+
* value — the one permitted position-setting read of the repository is the
|
|
17
|
+
* bootstrap scan's (`src/scan.ts`, INV-021), and this module borrows only its
|
|
18
|
+
* layer→document table so there is one home for it.
|
|
19
|
+
*
|
|
20
|
+
* INV-001: Node builtins and `./scan.js` (same package) only.
|
|
21
|
+
*/
|
|
22
|
+
/** Where a discovered value came from. There is no third form: if neither a file
|
|
23
|
+
* line nor a command's output can be pointed at, the value is not a finding. */
|
|
24
|
+
export type FactAnchor = {
|
|
25
|
+
readonly kind: 'file';
|
|
26
|
+
readonly path: string;
|
|
27
|
+
readonly line: number;
|
|
28
|
+
} | {
|
|
29
|
+
readonly kind: 'command';
|
|
30
|
+
readonly command: string;
|
|
31
|
+
readonly output: string;
|
|
32
|
+
};
|
|
33
|
+
/** One thing the repository answered, and where it said so. */
|
|
34
|
+
export interface DiscoveredFact {
|
|
35
|
+
/** A stable key so the confirm phase can tell which questions are already answered. */
|
|
36
|
+
readonly key: string;
|
|
37
|
+
/** What the developer is told, in their language. */
|
|
38
|
+
readonly value: string;
|
|
39
|
+
readonly anchor: FactAnchor;
|
|
40
|
+
}
|
|
41
|
+
/** One thing the repository could not answer, so it is asked. */
|
|
42
|
+
export interface OpenQuestion {
|
|
43
|
+
readonly key: string;
|
|
44
|
+
/** The question, in plain words. */
|
|
45
|
+
readonly question: string;
|
|
46
|
+
/** Why it is being asked rather than reported — always the same shape: nothing
|
|
47
|
+
* in the repository anchors an answer. */
|
|
48
|
+
readonly why: string;
|
|
49
|
+
}
|
|
50
|
+
export interface DiscoveryResult {
|
|
51
|
+
readonly findings: readonly DiscoveredFact[];
|
|
52
|
+
readonly questions: readonly OpenQuestion[];
|
|
53
|
+
}
|
|
54
|
+
/** Render one anchor the way the discover phase prints it: every line carries the
|
|
55
|
+
* file or command it came from. */
|
|
56
|
+
export declare function renderAnchor(anchor: FactAnchor): string;
|
|
57
|
+
/**
|
|
58
|
+
* Read a project repository and report what it answers, each line carrying the file
|
|
59
|
+
* it came from — and what it does not answer, as questions for the confirm phase.
|
|
60
|
+
*
|
|
61
|
+
* Nothing is asked here and nothing is written. The caller puts the questions.
|
|
62
|
+
*/
|
|
63
|
+
export declare function discoverRepository(targetRepo: string): DiscoveryResult;
|
|
64
|
+
/**
|
|
65
|
+
* The question set the confirm phase actually puts: every question discovery could
|
|
66
|
+
* not settle. A key that appears as a finding never appears here — that is the
|
|
67
|
+
* "omits every fact those files answer" property, stated as one function so a
|
|
68
|
+
* caller cannot re-derive it differently.
|
|
69
|
+
*/
|
|
70
|
+
export declare function questionsToPut(result: DiscoveryResult): readonly OpenQuestion[];
|
|
71
|
+
/** The discover phase's report: one line per finding, each carrying its anchor. */
|
|
72
|
+
export declare function renderDiscovery(result: DiscoveryResult): string;
|
|
73
|
+
//# sourceMappingURL=discovery.d.ts.map
|