Skip to content

Commit f3073b6

Browse files
fix(engine-bootstrap): route doc-site URLs to kind: web-doc (#4)
PR #3 introduced web-doc as the fourth source kind across the artifact contract, REFRESH/DISCOVER probes, verify.sh enum, recipes, and the Step 3.6 cache-seed flow — but never updated bootstrap's Step 1 intake classifier. URL inputs continued to fall through to `kind: external-doc`, producing structurally invalid entries (external-doc is path-addressed local markdown, not URL-addressed) and making Step 3.6 unreachable. Re-route the intake table, repo/doc disambiguator, bare-org guardrail, Step 2 slug table, and Step 3 stamping to produce kind: web-doc with default crawl_mode: "sitemap". Document that bootstrap never produces external-doc. Harden verify.sh with per-kind url/path required-field checks so a malformed entry is rejected at validation time (defense-in-depth for the silent corruption hand-edits could still produce). Add fixtures pinning the rejected shapes and the expected post-fix bootstrap output.
1 parent 13078bc commit f3073b6

9 files changed

Lines changed: 144 additions & 29 deletions

File tree

plugin/skill-engine/docs/02-artifact-contract.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ The `<area-domain>-` prefix in `references/<area-domain>-*.md` is a routing sign
8484

8585
The bijection invariant (see "The bijection invariant" below) is unaffected by prefix choice - that contract holds regardless of how `<area-domain>-*` is filled in. The guidance here is about routing UX, not contract compliance.
8686

87-
### External-doc sources on source-paths.json
87+
### `kind: "external-doc"`
8888

8989
A `research/source-paths.json` entry can carry an optional `kind` discriminator that names the harvest treatment the engine applies to a source root. When `kind` is absent — the existing-corpus state for every contextualizer that has shipped before this addition — the entry receives the **git-managed source-root** treatment documented in [`03-engine.md`](03-engine.md): per-source SHA via `git rev-parse HEAD`, then sparse-clone or shallow-clone crawl when the SHA has changed. Back-compat is total — every existing source-paths.json entry continues to behave exactly as it did before the discriminator was introduced; `kind` is purely additive. Entries that explicitly set `kind: "external-doc"` receive the external-doc treatment described below.
9090

@@ -96,7 +96,9 @@ A `research/source-paths.json` entry can carry an optional `kind` discriminator
9696
}
9797
```
9898

99-
**What `kind: "external-doc"` means.** The `path` field points at pre-curated markdown content that lives outside any code repository — for example, a generic accessibility reference, a SharePoint-style compliance snapshot, an authored markdown sourced outside the navigated code repos. The engine treats this content as a first-class source for [DISCOVER](08-discover-pipeline.md) without applying the git-managed SHA-then-clone flow. Harvest semantics:
99+
**What `kind: "external-doc"` means.** The `path` field points at pre-curated markdown content that lives outside any code repository — for example, a generic accessibility reference, a SharePoint-style compliance snapshot, an authored markdown sourced outside the navigated code repos. The engine treats this content as a first-class source for [DISCOVER](08-discover-pipeline.md) without applying the git-managed SHA-then-clone flow. external-doc is **not a bootstrap-intake kind** — it carries a contextualizer-internal `path`, not a URL. Entries of this kind arrive in `research/source-paths.json` via DISCOVER, hand-edit, or a future workflow; the engine-bootstrap scaffolder produces `kind: "web-doc"` for doc-site URLs (see [`kind: "web-doc"`](#kind-web-doc)).
100+
101+
Harvest semantics:
100102

101103
* **Directory `path`.** The `path` resolves to a directory containing one-or-more `.md` files, scanned **recursively** so nested subdirectories are included (the exact walk recipe — `find -L`, the `-type f -o -type l` filter, and symlink handling — is canonicalized in the Symlink containment paragraph below and the `external-doc-frontmatter` named check in the contextualizer-side `verify.sh` the plugin stamps at bootstrap). Recursion is deliberate — external-doc directories typically mirror upstream wiki or SharePoint hierarchies the maintainer has not flattened; a shallow scan would silently skip the bulk of the content.
102104
* **Single-file `path`.** A contextualizer that ships exactly one external-doc `.md` file (the canonical example: one accessibility best-practices markdown injected at a known path) supports `kind: "external-doc"` with `path` resolving directly to the `.md` file rather than wrapping it in a directory. The verify check handles both shapes uniformly.

plugin/skill-engine/docs/09-discover-config.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ values are accepted; the canonical schema for each lives in
5555
| `kind` | What it harvests | Required schema fields beyond the base | Canonical doctrine |
5656
|---|---|---|---|
5757
| `git-managed` | Git-hosted source code. `url` required. | optional `branch` | [`02-artifact-contract.md` §"source-paths.json entry shape"](02-artifact-contract.md#source-pathsjson-entry-shape) |
58-
| `external-doc` | Pre-curated `.md` content outside any code repo. `path` required (directory or single file). | `.md` files carry provenance frontmatter (`source_url`, `crawl_date`, `decay`) | [`02-artifact-contract.md` §"`kind: "external-doc"`"](02-artifact-contract.md#external-doc-sources-on-source-pathsjson) |
58+
| `external-doc` | Pre-curated `.md` content outside any code repo. `path` required (directory or single file). | `.md` files carry provenance frontmatter (`source_url`, `crawl_date`, `decay`) | [`02-artifact-contract.md` §"`kind: "external-doc"`"](02-artifact-contract.md#kind-external-doc) |
5959
| `web-doc` | Documentation-site content acquired via WebFetch or MCP fetch. `url` and `crawl_mode` required. | `sitemap_url` (sitemap mode, optional) or `page_list` (list mode, required); optional `crawl_filters`, `crawl_budget`; `branch` rejected | [`02-artifact-contract.md` §"`kind: "web-doc"`"](02-artifact-contract.md#kind-web-doc) |
6060
| `local-path` | Non-git local-filesystem source. `path` required. | none | [`02-artifact-contract.md` §"source-paths.json entry shape"](02-artifact-contract.md#source-pathsjson-entry-shape) |
6161

plugin/skill-engine/engine-bootstrap-templates/verify.sh

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -213,10 +213,44 @@ else
213213
*) fail "sources[$idx] ($id): status '$status' not in {intake, proposed, confirmed, rejected}"; entries_ok=0 ;;
214214
esac
215215
fi
216-
if [ -z "$url" ] && [ -z "$path" ]; then
217-
fail "sources[$idx] ($id): neither url nor path is set — at least one is required"
218-
entries_ok=0
219-
fi
216+
case "$kind" in
217+
git-managed)
218+
if [ -z "$url" ]; then
219+
fail "sources[$idx] ($id): url is required when kind is git-managed"
220+
entries_ok=0
221+
fi
222+
;;
223+
web-doc)
224+
if [ -z "$url" ]; then
225+
fail "sources[$idx] ($id): url is required when kind is web-doc"
226+
entries_ok=0
227+
fi
228+
if [ -n "$path" ]; then
229+
fail "sources[$idx] ($id): path '$path' set on kind 'web-doc' — web-doc sources are URL-addressed, not path-addressed"
230+
entries_ok=0
231+
fi
232+
;;
233+
external-doc)
234+
if [ -z "$path" ]; then
235+
fail "sources[$idx] ($id): path is required when kind is external-doc"
236+
entries_ok=0
237+
fi
238+
if [ -n "$url" ]; then
239+
fail "sources[$idx] ($id): url '$url' set on kind 'external-doc' — external-doc sources are path-addressed (pre-curated local markdown), not URL-addressed"
240+
entries_ok=0
241+
fi
242+
;;
243+
local-path)
244+
if [ -z "$path" ]; then
245+
fail "sources[$idx] ($id): path is required when kind is local-path"
246+
entries_ok=0
247+
fi
248+
if [ -n "$url" ]; then
249+
fail "sources[$idx] ($id): url '$url' set on kind 'local-path' — local-path sources are filesystem-addressed, not URL-addressed"
250+
entries_ok=0
251+
fi
252+
;;
253+
esac
220254
if [ -n "$branch" ]; then
221255
if [ "$kind" != "git-managed" ]; then
222256
fail "sources[$idx] ($id): branch '$branch' set on kind '$kind' — branch is git-managed only"

plugin/skill-engine/skills/engine-bootstrap/SKILL.md

Lines changed: 56 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,7 @@ Accept all of these source-input shapes:
8888
| `git@github.com:<org>/<repo>.git` | git-managed source on GitHub (SSH form) |
8989
| `git+ssh://...` | git-managed source (generic SSH) |
9090
| `https://gitlab.com/<group>/<repo>`, `https://bitbucket.org/<user>/<repo>` | git-managed source (other hosts) |
91-
| `https://<host>/<path>/<page>.html` | external-doc source (single doc) |
92-
| `https://<host>/<path>/docs/...` (no git-host signal) | external-doc source (doc site) |
91+
| `https://<host>/<path...>` (any HTTP/HTTPS URL with no git-host signal) | web-doc source (default `crawl_mode: sitemap`) |
9392
| Absolute local path (`/Users/...`, `~/...`, `/home/...`) | local-path source |
9493
| Relative local path (`./foo`, `../bar`, bare `foo` referencing an existing dir) | local-path source (resolved to absolute at intake) |
9594

@@ -104,11 +103,13 @@ either a git source or a doc site), ask one targeted question in
104103
user-language — never the engine's `kind` value directly. The canonical
105104
disambiguator:
106105

107-
> Is `<input>` a source-code repo or a standalone document?
106+
> Is `<input>` a source-code repo or a documentation site?
108107
109108
Accept `repo` / `doc` (or full words) and map internally: `repo` → `kind:
110-
git-managed`, `doc``kind: external-doc`. **On any other response**
111-
(blank `<Enter>`, `local`, `quit`, typo) re-prompt with:
109+
git-managed`, `doc``kind: web-doc` (the engine never produces
110+
`kind: external-doc` from URL intake — see *What this skill does NOT do*
111+
below). **On any other response** (blank `<Enter>`, `local`, `quit`,
112+
typo) re-prompt with:
112113

113114
> Please answer `repo` or `doc` — or enter `q` to skip just this entry and
114115
> continue with the rest of the intake.
@@ -122,9 +123,9 @@ land in `source-paths.json`.
122123

123124
A URL of the form `https://github.com/<org>` (no `<repo>` segment) is
124125
neither a recognizable git source nor a docs page — it points at an org
125-
landing page. **Don't fall through to the external-doc catch-all**; that
126-
would silently stamp `kind: external-doc` against a URL the engine
127-
cannot meaningfully crawl. Instead, re-prompt:
126+
landing page. **Don't fall through to the web-doc catch-all**; that
127+
would silently stamp `kind: web-doc` against a URL whose sitemap and
128+
page list the engine cannot meaningfully resolve. Instead, re-prompt:
128129

129130
> `<url>` looks like a GitHub org landing page, not a specific repo or
130131
> doc. Paste the URL of a specific repo (e.g., `https://github.com/<org>/<repo>`)
@@ -145,7 +146,7 @@ For each accepted source, compute the following without prompting the user:
145146
|---|---|
146147
| `https://github.com/<org>/<repo>` | `<org>-<repo>` (lowercase; non-alphanumerics → hyphen; collapse runs) |
147148
| `git@github.com:<org>/<repo>.git` | `<org>-<repo>` (same rule, drop `.git`) |
148-
| `https://<host>/<path...>` (external-doc) | last meaningful path segment, lowercased; if it's a file, drop the extension |
149+
| `https://<host>/<path...>` (web-doc) | last meaningful path segment, lowercased; if it's a file, drop the extension. If the URL has no path segments (host-root like `https://docs.example.com/`), fall back to the host with non-alphanumerics → hyphen (e.g., `docs-example-com`). |
149150
| Local absolute or relative path | basename of the resolved absolute path, lowercased |
150151

151152
On collision (two sources slug to the same id), append `-2`, `-3`, ... to the
@@ -263,31 +264,60 @@ locates the root itself from the project working directory.
263264
### Stamping `research/source-paths.json`
264265

265266
Replace the empty `"sources": []` from the template with one entry per
266-
intaken source, in the order supplied. Each entry:
267+
intaken source, in the order supplied. The per-entry shape depends on
268+
`kind`:
269+
270+
**`kind: "git-managed"`** — set `url`; add `"branch": "<name>"` only if
271+
Step 2.4 recorded a non-default branch:
272+
273+
```json
274+
{
275+
"id": "<computed-slug>",
276+
"kind": "git-managed",
277+
"url": "<original-url>",
278+
"status": "intake",
279+
"archived": false,
280+
"lifecycle": { "state": "unknown", "last_checked": null, "last_checked_sha": null, "proposed_url": null },
281+
"discovered_via": null
282+
}
283+
```
284+
285+
**`kind: "web-doc"`** — set `url`; default `crawl_mode` to `"sitemap"`.
286+
Bootstrap does not resolve the sitemap or page list here; Step 3.6
287+
populates the cache and the optional `sitemap_url` / `page_list` fields
288+
remain absent until the user edits them (or DISCOVER proposes them):
267289

268290
```json
269291
{
270292
"id": "<computed-slug>",
271-
"kind": "<git-managed | external-doc | local-path | web-doc>",
272-
"url": "<original-url-if-url>",
273-
"path": "<resolved-absolute-path-if-local>",
293+
"kind": "web-doc",
294+
"url": "<original-url>",
295+
"crawl_mode": "sitemap",
274296
"status": "intake",
275297
"archived": false,
276298
"lifecycle": { "state": "unknown", "last_checked": null, "last_checked_sha": null, "proposed_url": null },
277299
"discovered_via": null
278300
}
279301
```
280302

281-
Set `url` only on URL-shaped sources; set `path` only on local-path sources.
282-
The fields are not exclusive at the schema level (a git-managed source the
283-
user has cloned locally could carry both), but at intake only one is set.
303+
**`kind: "local-path"`** — set `path` to the resolved absolute path:
304+
305+
```json
306+
{
307+
"id": "<computed-slug>",
308+
"kind": "local-path",
309+
"path": "<resolved-absolute-path>",
310+
"status": "intake",
311+
"archived": false,
312+
"lifecycle": { "state": "unknown", "last_checked": null, "last_checked_sha": null, "proposed_url": null },
313+
"discovered_via": null
314+
}
315+
```
284316

285-
If the user supplied a non-default branch in Step 2.4 for this source,
286-
add a `"branch": "<name>"` key alongside `url`. Omit the key entirely
287-
when the user accepted the default — downstream code defaults to HEAD
288-
when the field is absent. Never record an explicit `"branch": "main"`
289-
or `"branch": "master"` from Step 2.4; the absent-field convention is
290-
load-bearing for the future-proofing reason documented in Step 2.4.
317+
Bootstrap does **not** produce `kind: "external-doc"` entries: that kind
318+
is for pre-curated local `.md` content addressed by a contextualizer-
319+
internal `path`, not for a URL the user pastes at intake. External-doc
320+
entries land in `source-paths.json` via DISCOVER or hand-edit.
291321

292322
`schema_version: 1` from the template stays as-is. The schema is additive;
293323
existing v1 files continue to parse cleanly.
@@ -641,3 +671,7 @@ the cache explicitly via `/skill-engine:clean-cache`.
641671
intake would force a multi-source intake to abort halfway; failing on
642672
DISCOVER lets the user paste the whole list and address broken entries
643673
in batch.
674+
- It does not produce `kind: "external-doc"` entries. external-doc
675+
sources are pre-curated local markdown addressed by a contextualizer-
676+
internal `path` (see [`02-artifact-contract.md`](https://github.com/nick-railsback/skill-engine/blob/main/plugin/skill-engine/docs/02-artifact-contract.md#kind-external-doc)); they arrive in `source-paths.json`
677+
via DISCOVER or hand-edit, not via URL intake.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
path is required when kind is external-doc
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"schema_version": 1,
3+
"sources": [
4+
{
5+
"id": "no-path-doc",
6+
"kind": "external-doc",
7+
"status": "intake",
8+
"archived": false,
9+
"lifecycle": { "state": "unknown", "last_checked": null, "last_checked_sha": null, "proposed_url": null },
10+
"discovered_via": null
11+
}
12+
]
13+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
url 'https://docs.example.com/' set on kind 'external-doc'
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"schema_version": 1,
3+
"sources": [
4+
{
5+
"id": "url-on-external-doc",
6+
"kind": "external-doc",
7+
"url": "https://docs.example.com/",
8+
"path": "external-fixtures",
9+
"status": "intake",
10+
"archived": false,
11+
"lifecycle": { "state": "unknown", "last_checked": null, "last_checked_sha": null, "proposed_url": null },
12+
"discovered_via": null
13+
}
14+
]
15+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"schema_version": 1,
3+
"sources": [
4+
{
5+
"id": "docs-example-com",
6+
"kind": "web-doc",
7+
"url": "https://docs.example.com/",
8+
"crawl_mode": "sitemap",
9+
"status": "intake",
10+
"archived": false,
11+
"lifecycle": { "state": "unknown", "last_checked": null, "last_checked_sha": null, "proposed_url": null },
12+
"discovered_via": null
13+
}
14+
]
15+
}

0 commit comments

Comments
 (0)